Add (disabled) time zone update system server impl

This commit builds on top of prior API commits. It
adds code to the system server, but in a disabled way.

The system server is responsible for monitoring two
(configured) package names: one for the "updater app"
(provided by the platform) and one for the "data app"
(provided by the OEM). When either package changes
the updater app is triggered via a privileged
intent.

The updater is then required to communicate with the
data app and report back to the system server.

Unit tests are included for the major components.

To run:
make -j30 FrameworksServicesTests
adb install -r -g "out/target/product/angler/data/app/FrameworksServicesTests/FrameworksServicesTests.apk"
adb shell am instrument -e package com.android.server.timezone -w com.android.frameworks.servicestests \
    "com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner"

Test: See above.
Bug: 31008728
Merged-In: I8f82cdcc2b574778a7e0d0184270f305b69ee17b
Change-Id: I8f82cdcc2b574778a7e0d0184270f305b69ee17b
diff --git a/services/core/java/com/android/server/timezone/RulesManagerService.java b/services/core/java/com/android/server/timezone/RulesManagerService.java
new file mode 100644
index 0000000..82bd356
--- /dev/null
+++ b/services/core/java/com/android/server/timezone/RulesManagerService.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.timezone;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.SystemService;
+
+import android.app.timezone.Callback;
+import android.app.timezone.DistroFormatVersion;
+import android.app.timezone.DistroRulesVersion;
+import android.app.timezone.ICallback;
+import android.app.timezone.IRulesManager;
+import android.app.timezone.RulesManager;
+import android.app.timezone.RulesState;
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+import libcore.tzdata.shared2.DistroException;
+import libcore.tzdata.shared2.DistroVersion;
+import libcore.tzdata.shared2.StagedDistroOperation;
+import libcore.tzdata.update2.TimeZoneDistroInstaller;
+
+// TODO(nfuller) Add EventLog calls where useful in the system server.
+// TODO(nfuller) Check logging best practices in the system server.
+// TODO(nfuller) Check error handling best practices in the system server.
+public final class RulesManagerService extends IRulesManager.Stub {
+
+    private static final String TAG = "timezone.RulesManagerService";
+
+    /** The distro format supported by this device. */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    static final DistroFormatVersion DISTRO_FORMAT_VERSION_SUPPORTED =
+            new DistroFormatVersion(
+                    DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
+                    DistroVersion.CURRENT_FORMAT_MINOR_VERSION);
+
+    public static class Lifecycle extends SystemService {
+        private RulesManagerService mService;
+
+        public Lifecycle(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onStart() {
+            mService = RulesManagerService.create(getContext());
+            mService.start();
+
+            publishBinderService(Context.TIME_ZONE_RULES_MANAGER_SERVICE, mService);
+        }
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    static final String REQUIRED_UPDATER_PERMISSION =
+            android.Manifest.permission.UPDATE_TIME_ZONE_RULES;
+    private static final File SYSTEM_TZ_DATA_FILE = new File("/system/usr/share/zoneinfo/tzdata");
+    private static final File TZ_DATA_DIR = new File("/data/misc/zoneinfo");
+
+    private final AtomicBoolean mOperationInProgress = new AtomicBoolean(false);
+    private final PermissionHelper mPermissionHelper;
+    private final PackageTracker mPackageTracker;
+    private final Executor mExecutor;
+    private final TimeZoneDistroInstaller mInstaller;
+    private final FileDescriptorHelper mFileDescriptorHelper;
+
+    private static RulesManagerService create(Context context) {
+        RulesManagerServiceHelperImpl helper = new RulesManagerServiceHelperImpl(context);
+        return new RulesManagerService(
+                helper /* permissionHelper */,
+                helper /* executor */,
+                helper /* fileDescriptorHelper */,
+                PackageTracker.create(context),
+                new TimeZoneDistroInstaller(TAG, SYSTEM_TZ_DATA_FILE, TZ_DATA_DIR));
+    }
+
+    // A constructor that can be used by tests to supply mocked / faked dependencies.
+    RulesManagerService(PermissionHelper permissionHelper,
+            Executor executor,
+            FileDescriptorHelper fileDescriptorHelper, PackageTracker packageTracker,
+            TimeZoneDistroInstaller timeZoneDistroInstaller) {
+        mPermissionHelper = permissionHelper;
+        mExecutor = executor;
+        mFileDescriptorHelper = fileDescriptorHelper;
+        mPackageTracker = packageTracker;
+        mInstaller = timeZoneDistroInstaller;
+    }
+
+    public void start() {
+        mPackageTracker.start();
+    }
+
+    @Override // Binder call
+    public RulesState getRulesState() {
+        mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION);
+
+        synchronized(this) {
+            String systemRulesVersion;
+            try {
+                systemRulesVersion = mInstaller.getSystemRulesVersion();
+            } catch (IOException e) {
+                Slog.w(TAG, "Failed to read system rules", e);
+                return null;
+            }
+
+            boolean operationInProgress = this.mOperationInProgress.get();
+
+            // Determine the staged operation status, if possible.
+            DistroRulesVersion stagedDistroRulesVersion = null;
+            int stagedOperationStatus = RulesState.STAGED_OPERATION_UNKNOWN;
+            if (!operationInProgress) {
+                StagedDistroOperation stagedDistroOperation;
+                try {
+                    stagedDistroOperation = mInstaller.getStagedDistroOperation();
+                    if (stagedDistroOperation == null) {
+                        stagedOperationStatus = RulesState.STAGED_OPERATION_NONE;
+                    } else if (stagedDistroOperation.isUninstall) {
+                        stagedOperationStatus = RulesState.STAGED_OPERATION_UNINSTALL;
+                    } else {
+                        // Must be an install.
+                        stagedOperationStatus = RulesState.STAGED_OPERATION_INSTALL;
+                        DistroVersion stagedDistroVersion = stagedDistroOperation.distroVersion;
+                        stagedDistroRulesVersion = new DistroRulesVersion(
+                                stagedDistroVersion.rulesVersion,
+                                stagedDistroVersion.revision);
+                    }
+                } catch (DistroException | IOException e) {
+                    Slog.w(TAG, "Failed to read staged distro.", e);
+                }
+            }
+
+            // Determine the installed distro state, if possible.
+            DistroVersion installedDistroVersion;
+            int distroStatus = RulesState.DISTRO_STATUS_UNKNOWN;
+            DistroRulesVersion installedDistroRulesVersion = null;
+            if (!operationInProgress) {
+                try {
+                    installedDistroVersion = mInstaller.getInstalledDistroVersion();
+                    if (installedDistroVersion == null) {
+                        distroStatus = RulesState.DISTRO_STATUS_NONE;
+                        installedDistroRulesVersion = null;
+                    } else {
+                        distroStatus = RulesState.DISTRO_STATUS_INSTALLED;
+                        installedDistroRulesVersion = new DistroRulesVersion(
+                                installedDistroVersion.rulesVersion,
+                                installedDistroVersion.revision);
+                    }
+                } catch (DistroException | IOException e) {
+                    Slog.w(TAG, "Failed to read installed distro.", e);
+                }
+            }
+            return new RulesState(systemRulesVersion, DISTRO_FORMAT_VERSION_SUPPORTED,
+                    operationInProgress, stagedOperationStatus, stagedDistroRulesVersion,
+                    distroStatus, installedDistroRulesVersion);
+        }
+    }
+
+    @Override
+    public int requestInstall(
+            ParcelFileDescriptor timeZoneDistro, byte[] checkTokenBytes, ICallback callback) {
+        mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION);
+
+        CheckToken checkToken = null;
+        if (checkTokenBytes != null) {
+            checkToken = createCheckTokenOrThrow(checkTokenBytes);
+        }
+        synchronized (this) {
+            if (timeZoneDistro == null) {
+                throw new NullPointerException("timeZoneDistro == null");
+            }
+            if (callback == null) {
+                throw new NullPointerException("observer == null");
+            }
+            if (mOperationInProgress.get()) {
+                return RulesManager.ERROR_OPERATION_IN_PROGRESS;
+            }
+            mOperationInProgress.set(true);
+
+            // Execute the install asynchronously.
+            mExecutor.execute(new InstallRunnable(timeZoneDistro, checkToken, callback));
+
+            return RulesManager.SUCCESS;
+        }
+    }
+
+    private class InstallRunnable implements Runnable {
+
+        private final ParcelFileDescriptor mTimeZoneDistro;
+        private final CheckToken mCheckToken;
+        private final ICallback mCallback;
+
+        InstallRunnable(
+                ParcelFileDescriptor timeZoneDistro, CheckToken checkToken, ICallback callback) {
+            mTimeZoneDistro = timeZoneDistro;
+            mCheckToken = checkToken;
+            mCallback = callback;
+        }
+
+        @Override
+        public void run() {
+            // Adopt the ParcelFileDescriptor into this try-with-resources so it is closed
+            // when we are done.
+            boolean success = false;
+            try {
+                byte[] distroBytes =
+                        RulesManagerService.this.mFileDescriptorHelper.readFully(mTimeZoneDistro);
+                int installerResult = mInstaller.stageInstallWithErrorCode(distroBytes);
+                int resultCode = mapInstallerResultToApiCode(installerResult);
+                sendFinishedStatus(mCallback, resultCode);
+
+                // All the installer failure modes are currently non-recoverable and won't be
+                // improved by trying again. Therefore success = true.
+                success = true;
+            } catch (Exception e) {
+                Slog.w(TAG, "Failed to install distro.", e);
+                sendFinishedStatus(mCallback, Callback.ERROR_UNKNOWN_FAILURE);
+            } finally {
+                // Notify the package tracker that the operation is now complete.
+                mPackageTracker.recordCheckResult(mCheckToken, success);
+
+                mOperationInProgress.set(false);
+            }
+        }
+
+        private int mapInstallerResultToApiCode(int installerResult) {
+            switch (installerResult) {
+                case TimeZoneDistroInstaller.INSTALL_SUCCESS:
+                    return Callback.SUCCESS;
+                case TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE:
+                    return Callback.ERROR_INSTALL_BAD_DISTRO_STRUCTURE;
+                case TimeZoneDistroInstaller.INSTALL_FAIL_RULES_TOO_OLD:
+                    return Callback.ERROR_INSTALL_RULES_TOO_OLD;
+                case TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_FORMAT_VERSION:
+                    return Callback.ERROR_INSTALL_BAD_DISTRO_FORMAT_VERSION;
+                case TimeZoneDistroInstaller.INSTALL_FAIL_VALIDATION_ERROR:
+                    return Callback.ERROR_INSTALL_VALIDATION_ERROR;
+                default:
+                    return Callback.ERROR_UNKNOWN_FAILURE;
+            }
+        }
+    }
+
+    @Override
+    public int requestUninstall(byte[] checkTokenBytes, ICallback callback) {
+        mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION);
+
+        CheckToken checkToken = null;
+        if (checkTokenBytes != null) {
+            checkToken = createCheckTokenOrThrow(checkTokenBytes);
+        }
+        synchronized(this) {
+            if (callback == null) {
+                throw new NullPointerException("callback == null");
+            }
+
+            if (mOperationInProgress.get()) {
+                return RulesManager.ERROR_OPERATION_IN_PROGRESS;
+            }
+            mOperationInProgress.set(true);
+
+            // Execute the uninstall asynchronously.
+            mExecutor.execute(new UninstallRunnable(checkToken, callback));
+
+            return RulesManager.SUCCESS;
+        }
+    }
+
+    private class UninstallRunnable implements Runnable {
+
+        private final CheckToken mCheckToken;
+        private final ICallback mCallback;
+
+        public UninstallRunnable(CheckToken checkToken, ICallback callback) {
+            mCheckToken = checkToken;
+            mCallback = callback;
+        }
+
+        @Override
+        public void run() {
+            boolean success = false;
+            try {
+                success = mInstaller.stageUninstall();
+                // Right now we just have success (0) / failure (1). All clients should be checking
+                // against SUCCESS. More granular failures may be added in future.
+                int resultCode = success ? Callback.SUCCESS
+                        : Callback.ERROR_UNKNOWN_FAILURE;
+                sendFinishedStatus(mCallback, resultCode);
+            } catch (Exception e) {
+                Slog.w(TAG, "Failed to uninstall distro.", e);
+                sendFinishedStatus(mCallback, Callback.ERROR_UNKNOWN_FAILURE);
+            } finally {
+                // Notify the package tracker that the operation is now complete.
+                mPackageTracker.recordCheckResult(mCheckToken, success);
+
+                mOperationInProgress.set(false);
+            }
+        }
+    }
+
+    private void sendFinishedStatus(ICallback callback, int resultCode) {
+        try {
+            callback.onFinished(resultCode);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Unable to notify observer of result", e);
+        }
+    }
+
+    @Override
+    public void requestNothing(byte[] checkTokenBytes, boolean success) {
+        mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION);
+        CheckToken checkToken = null;
+        if (checkTokenBytes != null) {
+            checkToken = createCheckTokenOrThrow(checkTokenBytes);
+        }
+        mPackageTracker.recordCheckResult(checkToken, success);
+    }
+
+    private static CheckToken createCheckTokenOrThrow(byte[] checkTokenBytes) {
+        CheckToken checkToken;
+        try {
+            checkToken = CheckToken.fromByteArray(checkTokenBytes);
+        } catch (IOException e) {
+            throw new IllegalArgumentException("Unable to read token bytes "
+                    + Arrays.toString(checkTokenBytes), e);
+        }
+        return checkToken;
+    }
+}