Add support for time zone notifications

Add support for time zone notifications to RulesManagerService.

This change adds new intents that are broadcast after
a time zone rules update is staged / unstaged. This will allow
another component on the device to handle notifications telling
the user to restart the device.

Bug: 69443060
Test: atest FrameworksServicesTests
Test: Manual install
Change-Id: I4fc4b5715380d84d87cdb43c05aee2f8f9c5d277
diff --git a/core/java/android/app/timezone/RulesManager.java b/core/java/android/app/timezone/RulesManager.java
index 0a38eb9..dc79256 100644
--- a/core/java/android/app/timezone/RulesManager.java
+++ b/core/java/android/app/timezone/RulesManager.java
@@ -68,6 +68,23 @@
     private static final String TAG = "timezone.RulesManager";
     private static final boolean DEBUG = false;
 
+    /**
+     * The action of the intent that the Android system will broadcast when a time zone rules update
+     * operation has been successfully staged  (i.e. to be applied next reboot) or unstaged.
+     *
+     * <p>See {@link #EXTRA_OPERATION_STAGED}
+     *
+     * <p>This is a protected intent that can only be sent by the system.
+     */
+    public static final String ACTION_RULES_UPDATE_OPERATION =
+            "com.android.intent.action.timezone.RULES_UPDATE_OPERATION";
+
+    /**
+     * The key for a boolean extra for the {@link #ACTION_RULES_UPDATE_OPERATION} intent used to
+     * indicate whether the operation was a "stage" or an "unstage".
+     */
+    public static final String EXTRA_OPERATION_STAGED = "staged";
+
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = { "SUCCESS", "ERROR_" }, value = {
             SUCCESS,
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index e5ba6d7..0bfeae3 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -571,6 +571,10 @@
     <protected-broadcast android:name="android.media.tv.action.CHANNEL_BROWSABLE_REQUESTED" />
     <protected-broadcast android:name="com.android.server.InputMethodManagerService.SHOW_INPUT_METHOD_PICKER" />
 
+    <!-- Time zone rules update intents fired by the system server -->
+    <protected-broadcast android:name="com.android.intent.action.timezone.RULES_UPDATE_OPERATION" />
+    <protected-broadcast android:name="com.android.intent.action.timezone.TRIGGER_RULES_UPDATE_CHECK" />
+
     <!-- Made protected in P (was introduced in JB-MR2) -->
     <protected-broadcast android:name="android.intent.action.GET_RESTRICTION_ENTRIES" />
     <protected-broadcast android:name="android.telephony.euicc.action.OTA_STATUS_CHANGED" />
diff --git a/services/core/java/com/android/server/timezone/PackageTracker.java b/services/core/java/com/android/server/timezone/PackageTracker.java
index 0e8d8bc..8f4cada 100644
--- a/services/core/java/com/android/server/timezone/PackageTracker.java
+++ b/services/core/java/com/android/server/timezone/PackageTracker.java
@@ -59,7 +59,7 @@
     private static final String TAG = "timezone.PackageTracker";
 
     private final PackageManagerHelper mPackageManagerHelper;
-    private final IntentHelper mIntentHelper;
+    private final PackageTrackerIntentHelper mIntentHelper;
     private final ConfigHelper mConfigHelper;
     private final PackageStatusStorage mPackageStatusStorage;
     private final Clock mElapsedRealtimeClock;
@@ -103,13 +103,13 @@
                 helperImpl /* configHelper */,
                 helperImpl /* packageManagerHelper */,
                 new PackageStatusStorage(storageDir),
-                new IntentHelperImpl(context));
+                new PackageTrackerIntentHelperImpl(context));
     }
 
     // A constructor that can be used by tests to supply mocked / faked dependencies.
     PackageTracker(Clock elapsedRealtimeClock, ConfigHelper configHelper,
             PackageManagerHelper packageManagerHelper, PackageStatusStorage packageStatusStorage,
-            IntentHelper intentHelper) {
+            PackageTrackerIntentHelper intentHelper) {
         mElapsedRealtimeClock = elapsedRealtimeClock;
         mConfigHelper = configHelper;
         mPackageManagerHelper = packageManagerHelper;
diff --git a/services/core/java/com/android/server/timezone/IntentHelper.java b/services/core/java/com/android/server/timezone/PackageTrackerIntentHelper.java
similarity index 97%
rename from services/core/java/com/android/server/timezone/IntentHelper.java
rename to services/core/java/com/android/server/timezone/PackageTrackerIntentHelper.java
index 5de5432..3753ece 100644
--- a/services/core/java/com/android/server/timezone/IntentHelper.java
+++ b/services/core/java/com/android/server/timezone/PackageTrackerIntentHelper.java
@@ -21,7 +21,7 @@
  * it is not possible to test various cases with the real one because of the need to simulate
  * receiving and broadcasting intents.
  */
-interface IntentHelper {
+interface PackageTrackerIntentHelper {
 
     void initialize(String updateAppPackageName, String dataAppPackageName,
             PackageTracker packageTracker);
diff --git a/services/core/java/com/android/server/timezone/IntentHelperImpl.java b/services/core/java/com/android/server/timezone/PackageTrackerIntentHelperImpl.java
similarity index 93%
rename from services/core/java/com/android/server/timezone/IntentHelperImpl.java
rename to services/core/java/com/android/server/timezone/PackageTrackerIntentHelperImpl.java
index 6e6259d..4110d88 100644
--- a/services/core/java/com/android/server/timezone/IntentHelperImpl.java
+++ b/services/core/java/com/android/server/timezone/PackageTrackerIntentHelperImpl.java
@@ -28,16 +28,16 @@
 import android.util.Slog;
 
 /**
- * The bona fide implementation of {@link IntentHelper}.
+ * The bona fide implementation of {@link PackageTrackerIntentHelper}.
  */
-final class IntentHelperImpl implements IntentHelper {
+final class PackageTrackerIntentHelperImpl implements PackageTrackerIntentHelper {
 
-    private final static String TAG = "timezone.IntentHelperImpl";
+    private final static String TAG = "timezone.PackageTrackerIntentHelperImpl";
 
     private final Context mContext;
     private String mUpdaterAppPackageName;
 
-    IntentHelperImpl(Context context) {
+    PackageTrackerIntentHelperImpl(Context context) {
         mContext = context;
     }
 
diff --git a/services/core/java/com/android/server/timezone/RulesManagerIntentHelper.java b/services/core/java/com/android/server/timezone/RulesManagerIntentHelper.java
new file mode 100644
index 0000000..bb317cf
--- /dev/null
+++ b/services/core/java/com/android/server/timezone/RulesManagerIntentHelper.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/**
+ * An easy-to-mock interface around intent sending / receiving for use by
+ * {@link RulesManagerService}; it is not possible to test various cases with the real one because
+ * of the need to simulate broadcasting intents.
+ */
+interface RulesManagerIntentHelper {
+
+    /**
+     * Send a broadcast informing listeners that a time zone operation is staged.
+     */
+    void sendTimeZoneOperationStaged();
+
+    /**
+     * Send a broadcast informing listeners that a time zone operation is no longer staged.
+     */
+    void sendTimeZoneOperationUnstaged();
+}
diff --git a/services/core/java/com/android/server/timezone/RulesManagerService.java b/services/core/java/com/android/server/timezone/RulesManagerService.java
index be9b204..872d723 100644
--- a/services/core/java/com/android/server/timezone/RulesManagerService.java
+++ b/services/core/java/com/android/server/timezone/RulesManagerService.java
@@ -99,6 +99,7 @@
     private final PermissionHelper mPermissionHelper;
     private final PackageTracker mPackageTracker;
     private final Executor mExecutor;
+    private final RulesManagerIntentHelper mIntentHelper;
     private final TimeZoneDistroInstaller mInstaller;
 
     private static RulesManagerService create(Context context) {
@@ -106,16 +107,19 @@
         return new RulesManagerService(
                 helper /* permissionHelper */,
                 helper /* executor */,
+                helper /* intentHelper */,
                 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, PackageTracker packageTracker,
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    RulesManagerService(PermissionHelper permissionHelper, Executor executor,
+            RulesManagerIntentHelper intentHelper, PackageTracker packageTracker,
             TimeZoneDistroInstaller timeZoneDistroInstaller) {
         mPermissionHelper = permissionHelper;
         mExecutor = executor;
+        mIntentHelper = intentHelper;
         mPackageTracker = packageTracker;
         mInstaller = timeZoneDistroInstaller;
     }
@@ -271,6 +275,10 @@
 
                 TimeZoneDistro distro = new TimeZoneDistro(is);
                 int installerResult = mInstaller.stageInstallWithErrorCode(distro);
+
+                // Notify interested parties that something is staged.
+                sendInstallNotificationIntentIfRequired(installerResult);
+
                 int resultCode = mapInstallerResultToApiCode(installerResult);
                 EventLogTags.writeTimezoneInstallComplete(toStringOrNull(mCheckToken), resultCode);
                 sendFinishedStatus(mCallback, resultCode);
@@ -291,6 +299,12 @@
             }
         }
 
+        private void sendInstallNotificationIntentIfRequired(int installerResult) {
+            if (installerResult == TimeZoneDistroInstaller.INSTALL_SUCCESS) {
+                mIntentHelper.sendTimeZoneOperationStaged();
+            }
+        }
+
         private int mapInstallerResultToApiCode(int installerResult) {
             switch (installerResult) {
                 case TimeZoneDistroInstaller.INSTALL_SUCCESS:
@@ -351,6 +365,10 @@
             boolean packageTrackerStatus = false;
             try {
                 int uninstallResult = mInstaller.stageUninstall();
+
+                // Notify interested parties that something is staged.
+                sendUninstallNotificationIntentIfRequired(uninstallResult);
+
                 packageTrackerStatus = (uninstallResult == TimeZoneDistroInstaller.UNINSTALL_SUCCESS
                         || uninstallResult == TimeZoneDistroInstaller.UNINSTALL_NOTHING_INSTALLED);
 
@@ -374,6 +392,20 @@
                 mOperationInProgress.set(false);
             }
         }
+
+        private void sendUninstallNotificationIntentIfRequired(int uninstallResult) {
+            switch (uninstallResult) {
+                case TimeZoneDistroInstaller.UNINSTALL_SUCCESS:
+                    mIntentHelper.sendTimeZoneOperationStaged();
+                    break;
+                case TimeZoneDistroInstaller.UNINSTALL_NOTHING_INSTALLED:
+                    mIntentHelper.sendTimeZoneOperationUnstaged();
+                    break;
+                case TimeZoneDistroInstaller.UNINSTALL_FAIL:
+                default:
+                    // No-op - unknown or nothing to notify about.
+            }
+        }
     }
 
     private void sendFinishedStatus(ICallback callback, int resultCode) {
diff --git a/services/core/java/com/android/server/timezone/RulesManagerServiceHelperImpl.java b/services/core/java/com/android/server/timezone/RulesManagerServiceHelperImpl.java
index e8a401e..8f5c7a7 100644
--- a/services/core/java/com/android/server/timezone/RulesManagerServiceHelperImpl.java
+++ b/services/core/java/com/android/server/timezone/RulesManagerServiceHelperImpl.java
@@ -18,22 +18,20 @@
 
 import com.android.internal.util.DumpUtils;
 
+import android.app.timezone.RulesManager;
 import android.content.Context;
-import android.content.pm.PackageManager;
+import android.content.Intent;
 import android.os.AsyncTask;
-import android.os.Binder;
-import android.os.ParcelFileDescriptor;
+import android.os.UserHandle;
 
-import java.io.FileInputStream;
-import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.concurrent.Executor;
-import libcore.io.Streams;
 
 /**
  * A single class that implements multiple helper interfaces for use by {@link RulesManagerService}.
  */
-final class RulesManagerServiceHelperImpl implements PermissionHelper, Executor {
+final class RulesManagerServiceHelperImpl
+        implements PermissionHelper, Executor, RulesManagerIntentHelper {
 
     private final Context mContext;
 
@@ -55,4 +53,22 @@
     public void execute(Runnable runnable) {
         AsyncTask.execute(runnable);
     }
+
+    @Override
+    public void sendTimeZoneOperationStaged() {
+        sendOperationIntent(true /* staged */);
+    }
+
+    @Override
+    public void sendTimeZoneOperationUnstaged() {
+        sendOperationIntent(false /* staged */);
+    }
+
+    private void sendOperationIntent(boolean staged) {
+        Intent intent = new Intent(RulesManager.ACTION_RULES_UPDATE_OPERATION);
+        intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+        intent.putExtra(RulesManager.EXTRA_OPERATION_STAGED, staged);
+        mContext.sendBroadcastAsUser(intent, UserHandle.SYSTEM);
+    }
+
 }
diff --git a/services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java b/services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java
index 9cf6392..d9f4adf 100644
--- a/services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java
@@ -31,7 +31,6 @@
 import android.support.test.filters.SmallTest;
 
 import java.io.File;
-import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.time.Clock;
@@ -1400,7 +1399,7 @@
     /**
      * A fake IntentHelper implementation for use in tests.
      */
-    private static class FakeIntentHelper implements IntentHelper {
+    private static class FakeIntentHelper implements PackageTrackerIntentHelper {
 
         private PackageTracker mPackageTracker;
         private String mUpdateAppPackageName;
diff --git a/services/tests/servicestests/src/com/android/server/timezone/RulesManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/timezone/RulesManagerServiceTest.java
index 1cfae1e..f5969f3 100644
--- a/services/tests/servicestests/src/com/android/server/timezone/RulesManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezone/RulesManagerServiceTest.java
@@ -68,6 +68,7 @@
 
     private FakeExecutor mFakeExecutor;
     private PermissionHelper mMockPermissionHelper;
+    private RulesManagerIntentHelper mMockIntentHelper;
     private PackageTracker mMockPackageTracker;
     private TimeZoneDistroInstaller mMockTimeZoneDistroInstaller;
 
@@ -77,11 +78,13 @@
 
         mMockPackageTracker = mock(PackageTracker.class);
         mMockPermissionHelper = mock(PermissionHelper.class);
+        mMockIntentHelper = mock(RulesManagerIntentHelper.class);
         mMockTimeZoneDistroInstaller = mock(TimeZoneDistroInstaller.class);
 
         mRulesManagerService = new RulesManagerService(
                 mMockPermissionHelper,
                 mFakeExecutor,
+                mMockIntentHelper,
                 mMockPackageTracker,
                 mMockTimeZoneDistroInstaller);
     }
@@ -329,6 +332,7 @@
         mFakeExecutor.assertNothingQueued();
         verifyNoInstallerCallsMade();
         verifyNoPackageTrackerCallsMade();
+        verifyNoIntentsSent();
     }
 
     @Test
@@ -353,6 +357,7 @@
         mFakeExecutor.assertNothingQueued();
         verifyNoInstallerCallsMade();
         verifyNoPackageTrackerCallsMade();
+        verifyNoIntentsSent();
     }
 
     @Test
@@ -372,6 +377,7 @@
         mFakeExecutor.assertNothingQueued();
         verifyNoInstallerCallsMade();
         verifyNoPackageTrackerCallsMade();
+        verifyNoIntentsSent();
     }
 
     @Test
@@ -394,6 +400,7 @@
         mFakeExecutor.assertNothingQueued();
         verifyNoInstallerCallsMade();
         verifyNoPackageTrackerCallsMade();
+        verifyNoIntentsSent();
     }
 
     @Test
@@ -416,6 +423,7 @@
         callback.assertNoResultReceived();
         verifyNoInstallerCallsMade();
         verifyNoPackageTrackerCallsMade();
+        verifyNoIntentsSent();
 
         // Set up the installer.
         configureStageInstallExpectation(TimeZoneDistroInstaller.INSTALL_SUCCESS);
@@ -428,6 +436,7 @@
         // Verify the expected calls were made to other components.
         verifyStageInstallCalled();
         verifyPackageTrackerCalled(token, true /* success */);
+        verifyStagedOperationIntentSent();
 
         // Check the callback was called.
         callback.assertResultReceived(Callback.SUCCESS);
@@ -450,6 +459,7 @@
         // Assert nothing has happened yet.
         verifyNoInstallerCallsMade();
         callback.assertNoResultReceived();
+        verifyNoIntentsSent();
 
         // Set up the installer.
         configureStageInstallExpectation(TimeZoneDistroInstaller.INSTALL_SUCCESS);
@@ -462,6 +472,7 @@
         // Verify the expected calls were made to other components.
         verifyStageInstallCalled();
         verifyPackageTrackerCalled(null /* expectedToken */, true /* success */);
+        verifyStagedOperationIntentSent();
 
         // Check the callback was received.
         callback.assertResultReceived(Callback.SUCCESS);
@@ -486,6 +497,7 @@
         // Assert nothing has happened yet.
         verifyNoInstallerCallsMade();
         callback.assertNoResultReceived();
+        verifyNoIntentsSent();
 
         // Set up the installer.
         configureStageInstallExpectation(TimeZoneDistroInstaller.INSTALL_FAIL_VALIDATION_ERROR);
@@ -502,6 +514,9 @@
         boolean expectedSuccess = true;
         verifyPackageTrackerCalled(token, expectedSuccess);
 
+        // Nothing should be staged, so no intents sent.
+        verifyNoIntentsSent();
+
         // Check the callback was received.
         callback.assertResultReceived(Callback.ERROR_INSTALL_VALIDATION_ERROR);
     }
@@ -529,6 +544,7 @@
         mFakeExecutor.assertNothingQueued();
         verifyNoInstallerCallsMade();
         verifyNoPackageTrackerCallsMade();
+        verifyNoIntentsSent();
     }
 
     @Test
@@ -548,6 +564,7 @@
         mFakeExecutor.assertNothingQueued();
         verifyNoInstallerCallsMade();
         verifyNoPackageTrackerCallsMade();
+        verifyNoIntentsSent();
     }
 
     @Test
@@ -566,6 +583,7 @@
         mFakeExecutor.assertNothingQueued();
         verifyNoInstallerCallsMade();
         verifyNoPackageTrackerCallsMade();
+        verifyNoIntentsSent();
     }
 
     @Test
@@ -585,6 +603,7 @@
         callback.assertNoResultReceived();
         verifyNoInstallerCallsMade();
         verifyNoPackageTrackerCallsMade();
+        verifyNoIntentsSent();
 
         // Set up the installer.
         configureStageUninstallExpectation(TimeZoneDistroInstaller.UNINSTALL_SUCCESS);
@@ -595,6 +614,7 @@
         // Verify the expected calls were made to other components.
         verifyStageUninstallCalled();
         verifyPackageTrackerCalled(token, true /* success */);
+        verifyStagedOperationIntentSent();
 
         // Check the callback was called.
         callback.assertResultReceived(Callback.SUCCESS);
@@ -617,6 +637,7 @@
         callback.assertNoResultReceived();
         verifyNoInstallerCallsMade();
         verifyNoPackageTrackerCallsMade();
+        verifyNoIntentsSent();
 
         // Set up the installer.
         configureStageUninstallExpectation(TimeZoneDistroInstaller.UNINSTALL_NOTHING_INSTALLED);
@@ -627,6 +648,7 @@
         // Verify the expected calls were made to other components.
         verifyStageUninstallCalled();
         verifyPackageTrackerCalled(token, true /* success */);
+        verifyUnstagedOperationIntentSent();
 
         // Check the callback was called.
         callback.assertResultReceived(Callback.SUCCESS);
@@ -645,6 +667,7 @@
         // Assert nothing has happened yet.
         verifyNoInstallerCallsMade();
         callback.assertNoResultReceived();
+        verifyNoIntentsSent();
 
         // Set up the installer.
         configureStageUninstallExpectation(TimeZoneDistroInstaller.UNINSTALL_SUCCESS);
@@ -655,6 +678,7 @@
         // Verify the expected calls were made to other components.
         verifyStageUninstallCalled();
         verifyPackageTrackerCalled(null /* expectedToken */, true /* success */);
+        verifyStagedOperationIntentSent();
 
         // Check the callback was received.
         callback.assertResultReceived(Callback.SUCCESS);
@@ -676,6 +700,7 @@
         // Assert nothing has happened yet.
         verifyNoInstallerCallsMade();
         callback.assertNoResultReceived();
+        verifyNoIntentsSent();
 
         // Set up the installer.
         configureStageUninstallExpectation(TimeZoneDistroInstaller.UNINSTALL_FAIL);
@@ -686,6 +711,7 @@
         // Verify the expected calls were made to other components.
         verifyStageUninstallCalled();
         verifyPackageTrackerCalled(token, false /* success */);
+        verifyNoIntentsSent();
 
         // Check the callback was received.
         callback.assertResultReceived(Callback.ERROR_UNKNOWN_FAILURE);
@@ -714,6 +740,7 @@
         // Verify the expected calls were made to other components.
         verifyPackageTrackerCalled(token, true /* success */);
         verifyNoInstallerCallsMade();
+        verifyNoIntentsSent();
     }
 
     @Test
@@ -734,6 +761,7 @@
         // Assert no other calls were made.
         verifyNoInstallerCallsMade();
         verifyNoPackageTrackerCallsMade();
+        verifyNoIntentsSent();
     }
 
     @Test
@@ -749,6 +777,7 @@
         // Assert everything required was done.
         verifyNoInstallerCallsMade();
         verifyPackageTrackerCalled(token, false /* success */);
+        verifyNoIntentsSent();
     }
 
     @Test
@@ -761,6 +790,7 @@
         // Assert everything required was done.
         verifyNoInstallerCallsMade();
         verifyPackageTrackerCalled(null /* token */, true /* success */);
+        verifyNoIntentsSent();
     }
 
     @Test
@@ -865,6 +895,21 @@
         reset(mMockPackageTracker);
     }
 
+    private void verifyNoIntentsSent() {
+        verifyNoMoreInteractions(mMockIntentHelper);
+        reset(mMockIntentHelper);
+    }
+
+    private void verifyStagedOperationIntentSent() {
+        verify(mMockIntentHelper).sendTimeZoneOperationStaged();
+        reset(mMockIntentHelper);
+    }
+
+    private void verifyUnstagedOperationIntentSent() {
+        verify(mMockIntentHelper).sendTimeZoneOperationUnstaged();
+        reset(mMockIntentHelper);
+    }
+
     private void configureCallerHasPermission() throws Exception {
         doNothing()
                 .when(mMockPermissionHelper)