diff --git a/core/java/android/app/timezone/RulesManager.java b/core/java/android/app/timezone/RulesManager.java index 0a38eb9ae7772..dc79256941760 100644 --- a/core/java/android/app/timezone/RulesManager.java +++ b/core/java/android/app/timezone/RulesManager.java @@ -68,6 +68,23 @@ public final class RulesManager { 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. + * + *

See {@link #EXTRA_OPERATION_STAGED} + * + *

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 e5ba6d76578dc..0bfeae3bd0868 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -571,6 +571,10 @@ + + + + diff --git a/services/core/java/com/android/server/timezone/PackageTracker.java b/services/core/java/com/android/server/timezone/PackageTracker.java index 0e8d8bc8e411e..8f4cada0d0ba2 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 @@ public class PackageTracker { 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 @@ public class PackageTracker { 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 5de5432130094..3753ece03bb0b 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 @@ package com.android.server.timezone; * 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 6e6259d902dc0..4110d881f3fb4 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.os.UserHandle; 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 0000000000000..bb317cf2f9887 --- /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 be9b204721dbb..872d7237f4f11 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 @@ public final class RulesManagerService extends IRulesManager.Stub { 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 @@ public final class RulesManagerService extends IRulesManager.Stub { 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 @@ public final class RulesManagerService extends IRulesManager.Stub { 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 @@ public final class RulesManagerService extends IRulesManager.Stub { } } + 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 @@ public final class RulesManagerService extends IRulesManager.Stub { 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 @@ public final class RulesManagerService extends IRulesManager.Stub { 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 e8a401e792350..8f5c7a783304b 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 @@ package com.android.server.timezone; 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 @@ final class RulesManagerServiceHelperImpl implements PermissionHelper, Executor 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 9cf6392cab971..d9f4adfb5e063 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.InstrumentationRegistry; 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 @@ public class PackageTrackerTest { /** * 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 1cfae1ef1a6d8..f5969f39e87c2 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 @@ public class RulesManagerServiceTest { private FakeExecutor mFakeExecutor; private PermissionHelper mMockPermissionHelper; + private RulesManagerIntentHelper mMockIntentHelper; private PackageTracker mMockPackageTracker; private TimeZoneDistroInstaller mMockTimeZoneDistroInstaller; @@ -77,11 +78,13 @@ public class RulesManagerServiceTest { 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 @@ public class RulesManagerServiceTest { mFakeExecutor.assertNothingQueued(); verifyNoInstallerCallsMade(); verifyNoPackageTrackerCallsMade(); + verifyNoIntentsSent(); } @Test @@ -353,6 +357,7 @@ public class RulesManagerServiceTest { mFakeExecutor.assertNothingQueued(); verifyNoInstallerCallsMade(); verifyNoPackageTrackerCallsMade(); + verifyNoIntentsSent(); } @Test @@ -372,6 +377,7 @@ public class RulesManagerServiceTest { mFakeExecutor.assertNothingQueued(); verifyNoInstallerCallsMade(); verifyNoPackageTrackerCallsMade(); + verifyNoIntentsSent(); } @Test @@ -394,6 +400,7 @@ public class RulesManagerServiceTest { mFakeExecutor.assertNothingQueued(); verifyNoInstallerCallsMade(); verifyNoPackageTrackerCallsMade(); + verifyNoIntentsSent(); } @Test @@ -416,6 +423,7 @@ public class RulesManagerServiceTest { callback.assertNoResultReceived(); verifyNoInstallerCallsMade(); verifyNoPackageTrackerCallsMade(); + verifyNoIntentsSent(); // Set up the installer. configureStageInstallExpectation(TimeZoneDistroInstaller.INSTALL_SUCCESS); @@ -428,6 +436,7 @@ public class RulesManagerServiceTest { // 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 @@ public class RulesManagerServiceTest { // Assert nothing has happened yet. verifyNoInstallerCallsMade(); callback.assertNoResultReceived(); + verifyNoIntentsSent(); // Set up the installer. configureStageInstallExpectation(TimeZoneDistroInstaller.INSTALL_SUCCESS); @@ -462,6 +472,7 @@ public class RulesManagerServiceTest { // 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 @@ public class RulesManagerServiceTest { // Assert nothing has happened yet. verifyNoInstallerCallsMade(); callback.assertNoResultReceived(); + verifyNoIntentsSent(); // Set up the installer. configureStageInstallExpectation(TimeZoneDistroInstaller.INSTALL_FAIL_VALIDATION_ERROR); @@ -502,6 +514,9 @@ public class RulesManagerServiceTest { 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 @@ public class RulesManagerServiceTest { mFakeExecutor.assertNothingQueued(); verifyNoInstallerCallsMade(); verifyNoPackageTrackerCallsMade(); + verifyNoIntentsSent(); } @Test @@ -548,6 +564,7 @@ public class RulesManagerServiceTest { mFakeExecutor.assertNothingQueued(); verifyNoInstallerCallsMade(); verifyNoPackageTrackerCallsMade(); + verifyNoIntentsSent(); } @Test @@ -566,6 +583,7 @@ public class RulesManagerServiceTest { mFakeExecutor.assertNothingQueued(); verifyNoInstallerCallsMade(); verifyNoPackageTrackerCallsMade(); + verifyNoIntentsSent(); } @Test @@ -585,6 +603,7 @@ public class RulesManagerServiceTest { callback.assertNoResultReceived(); verifyNoInstallerCallsMade(); verifyNoPackageTrackerCallsMade(); + verifyNoIntentsSent(); // Set up the installer. configureStageUninstallExpectation(TimeZoneDistroInstaller.UNINSTALL_SUCCESS); @@ -595,6 +614,7 @@ public class RulesManagerServiceTest { // 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 @@ public class RulesManagerServiceTest { callback.assertNoResultReceived(); verifyNoInstallerCallsMade(); verifyNoPackageTrackerCallsMade(); + verifyNoIntentsSent(); // Set up the installer. configureStageUninstallExpectation(TimeZoneDistroInstaller.UNINSTALL_NOTHING_INSTALLED); @@ -627,6 +648,7 @@ public class RulesManagerServiceTest { // 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 @@ public class RulesManagerServiceTest { // Assert nothing has happened yet. verifyNoInstallerCallsMade(); callback.assertNoResultReceived(); + verifyNoIntentsSent(); // Set up the installer. configureStageUninstallExpectation(TimeZoneDistroInstaller.UNINSTALL_SUCCESS); @@ -655,6 +678,7 @@ public class RulesManagerServiceTest { // 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 @@ public class RulesManagerServiceTest { // Assert nothing has happened yet. verifyNoInstallerCallsMade(); callback.assertNoResultReceived(); + verifyNoIntentsSent(); // Set up the installer. configureStageUninstallExpectation(TimeZoneDistroInstaller.UNINSTALL_FAIL); @@ -686,6 +711,7 @@ public class RulesManagerServiceTest { // 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 @@ public class RulesManagerServiceTest { // Verify the expected calls were made to other components. verifyPackageTrackerCalled(token, true /* success */); verifyNoInstallerCallsMade(); + verifyNoIntentsSent(); } @Test @@ -734,6 +761,7 @@ public class RulesManagerServiceTest { // Assert no other calls were made. verifyNoInstallerCallsMade(); verifyNoPackageTrackerCallsMade(); + verifyNoIntentsSent(); } @Test @@ -749,6 +777,7 @@ public class RulesManagerServiceTest { // Assert everything required was done. verifyNoInstallerCallsMade(); verifyPackageTrackerCalled(token, false /* success */); + verifyNoIntentsSent(); } @Test @@ -761,6 +790,7 @@ public class RulesManagerServiceTest { // Assert everything required was done. verifyNoInstallerCallsMade(); verifyPackageTrackerCalled(null /* token */, true /* success */); + verifyNoIntentsSent(); } @Test @@ -865,6 +895,21 @@ public class RulesManagerServiceTest { 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)