From cd101fa5b972ba92e834197d6685fc652d32ee34 Mon Sep 17 00:00:00 2001 From: Neil Fuller Date: Sat, 2 Jan 2021 13:27:51 +0000 Subject: [PATCH] Add start / stop to location_time_zone_manager Implement a path for reconfiguration of provider behavior at runtime. Now the location_time_zone_manager can be stopped / started from the command line with: $ adb shell location_time_zone_manager stop $ adb shell location_time_zone_manager start This is useful during manual and automated (i.e. CTS) tests. This change introduces LocationTimeZoneProviderController.destroy() and all the associated plumbing / a new state needed to support "stop". This commit removes NullLocationTimeZoneProvider and replaces it with a LocationTimeZoneProviderProxy that does a similar thing. It adds a LocationTimeZoneProviderTest to replace the lost coverage. Bug: 172934905 Bug: 152746105 Test: atest services/tests/servicestests/src/com/android/server/location/timezone/ Change-Id: I1ef7813ff2011251b2d8231a4e80f0374ee32c50 --- .../BinderLocationTimeZoneProvider.java | 16 +- .../timezone/ControllerEnvironmentImpl.java | 12 +- .../location/timezone/ControllerImpl.java | 73 +++-- .../timezone/HandlerThreadingDomain.java | 36 +++ .../LocationTimeZoneManagerService.java | 95 +++++-- .../LocationTimeZoneManagerShellCommand.java | 39 ++- .../timezone/LocationTimeZoneProvider.java | 87 +++++- .../LocationTimeZoneProviderController.java | 6 + .../LocationTimeZoneProviderProxy.java | 22 +- .../NullLocationTimeZoneProvider.java | 93 ------- .../NullLocationTimeZoneProviderProxy.java | 86 ++++++ .../RealLocationTimeZoneProviderProxy.java | 6 + ...imulatedLocationTimeZoneProviderProxy.java | 6 + .../server/location/timezone/TestCommand.java | 8 + .../location/timezone/ThreadingDomain.java | 37 ++- .../TimeZoneDetectorInternal.java | 6 + .../TimeZoneDetectorInternalImpl.java | 7 + .../location/timezone/ControllerImplTest.java | 11 + .../timezone/HandlerThreadingDomainTest.java | 57 +++- .../LocationTimeZoneProviderTest.java | 261 ++++++++++++++++++ .../NullLocationTimeZoneProviderTest.java | 151 ---------- .../timezone/TestThreadingDomain.java | 10 + 22 files changed, 804 insertions(+), 321 deletions(-) delete mode 100644 services/core/java/com/android/server/location/timezone/NullLocationTimeZoneProvider.java create mode 100644 services/core/java/com/android/server/location/timezone/NullLocationTimeZoneProviderProxy.java create mode 100644 services/tests/servicestests/src/com/android/server/location/timezone/LocationTimeZoneProviderTest.java delete mode 100644 services/tests/servicestests/src/com/android/server/location/timezone/NullLocationTimeZoneProviderTest.java diff --git a/services/core/java/com/android/server/location/timezone/BinderLocationTimeZoneProvider.java b/services/core/java/com/android/server/location/timezone/BinderLocationTimeZoneProvider.java index 5f744fed13000..210fb5c0a1ab9 100644 --- a/services/core/java/com/android/server/location/timezone/BinderLocationTimeZoneProvider.java +++ b/services/core/java/com/android/server/location/timezone/BinderLocationTimeZoneProvider.java @@ -17,6 +17,7 @@ package com.android.server.location.timezone; import static com.android.server.location.timezone.LocationTimeZoneManagerService.debugLog; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED; import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED; import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_CERTAIN; import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING; @@ -71,6 +72,11 @@ class BinderLocationTimeZoneProvider extends LocationTimeZoneProvider { }); } + @Override + void onDestroy() { + mProxy.destroy(); + } + private void handleProviderLost(String reason) { mThreadingDomain.assertCurrentThread(); @@ -100,11 +106,12 @@ class BinderLocationTimeZoneProvider extends LocationTimeZoneProvider { + ": No state change required, provider is stopped."); break; } - case PROVIDER_STATE_PERM_FAILED: { + case PROVIDER_STATE_PERM_FAILED: + case PROVIDER_STATE_DESTROYED: { debugLog("handleProviderLost reason=" + reason + ", mProviderName=" + mProviderName + ", currentState=" + currentState - + ": No state change required, provider is perm failed."); + + ": No state change required, provider is terminated."); break; } default: { @@ -132,11 +139,12 @@ class BinderLocationTimeZoneProvider extends LocationTimeZoneProvider { + ", currentState=" + currentState + ": Provider is stopped."); break; } - case PROVIDER_STATE_PERM_FAILED: { + case PROVIDER_STATE_PERM_FAILED: + case PROVIDER_STATE_DESTROYED: { debugLog("handleOnProviderBound" + ", mProviderName=" + mProviderName + ", currentState=" + currentState - + ": No state change required, provider is perm failed."); + + ": No state change required, provider is terminated."); break; } default: { diff --git a/services/core/java/com/android/server/location/timezone/ControllerEnvironmentImpl.java b/services/core/java/com/android/server/location/timezone/ControllerEnvironmentImpl.java index b1e3306681944..d896f6e441d8b 100644 --- a/services/core/java/com/android/server/location/timezone/ControllerEnvironmentImpl.java +++ b/services/core/java/com/android/server/location/timezone/ControllerEnvironmentImpl.java @@ -19,6 +19,7 @@ package com.android.server.location.timezone; import android.annotation.NonNull; import com.android.server.LocalServices; +import com.android.server.timezonedetector.ConfigurationChangeListener; import com.android.server.timezonedetector.ConfigurationInternal; import com.android.server.timezonedetector.TimeZoneDetectorInternal; @@ -37,6 +38,7 @@ class ControllerEnvironmentImpl extends LocationTimeZoneProviderController.Envir @NonNull private final TimeZoneDetectorInternal mTimeZoneDetectorInternal; @NonNull private final LocationTimeZoneProviderController mController; + @NonNull private final ConfigurationChangeListener mConfigurationChangeListener; ControllerEnvironmentImpl(@NonNull ThreadingDomain threadingDomain, @NonNull LocationTimeZoneProviderController controller) { @@ -45,8 +47,14 @@ class ControllerEnvironmentImpl extends LocationTimeZoneProviderController.Envir mTimeZoneDetectorInternal = LocalServices.getService(TimeZoneDetectorInternal.class); // Listen for configuration changes. - mTimeZoneDetectorInternal.addConfigurationListener( - () -> mThreadingDomain.post(mController::onConfigChanged)); + mConfigurationChangeListener = () -> mThreadingDomain.post(mController::onConfigChanged); + mTimeZoneDetectorInternal.addConfigurationListener(mConfigurationChangeListener); + } + + + @Override + void destroy() { + mTimeZoneDetectorInternal.removeConfigurationListener(mConfigurationChangeListener); } @Override diff --git a/services/core/java/com/android/server/location/timezone/ControllerImpl.java b/services/core/java/com/android/server/location/timezone/ControllerImpl.java index 396e5b029e9cc..03ce8ecd1cc08 100644 --- a/services/core/java/com/android/server/location/timezone/ControllerImpl.java +++ b/services/core/java/com/android/server/location/timezone/ControllerImpl.java @@ -19,6 +19,7 @@ package com.android.server.location.timezone; import static com.android.server.location.timezone.LocationTimeZoneManagerService.debugLog; import static com.android.server.location.timezone.LocationTimeZoneManagerService.warnLog; import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED; import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED; import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_CERTAIN; import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING; @@ -151,6 +152,23 @@ class ControllerImpl extends LocationTimeZoneProviderController { return mUncertaintyTimeoutQueue.getQueuedDelayMillis(); } + @Override + void destroy() { + mThreadingDomain.assertCurrentThread(); + + synchronized (mSharedLock) { + stopProviders(); + mPrimaryProvider.destroy(); + mSecondaryProvider.destroy(); + + // If the controller has made a "certain" suggestion, it should make an uncertain + // suggestion to cancel it. + if (mLastSuggestion != null && mLastSuggestion.getZoneIds() != null) { + makeSuggestion(createUncertainSuggestion("Controller is destroyed")); + } + } + } + @GuardedBy("mSharedLock") private void stopProviders() { stopProviderIfStarted(mPrimaryProvider); @@ -182,8 +200,9 @@ class ControllerImpl extends LocationTimeZoneProviderController { provider.stopUpdates(); break; } - case PROVIDER_STATE_PERM_FAILED: { - debugLog("Unable to stop " + provider + ": it is perm failed"); + case PROVIDER_STATE_PERM_FAILED: + case PROVIDER_STATE_DESTROYED: { + debugLog("Unable to stop " + provider + ": it is terminated."); break; } default: { @@ -285,8 +304,9 @@ class ControllerImpl extends LocationTimeZoneProviderController { debugLog("No need to start " + provider + ": already started"); break; } - case PROVIDER_STATE_PERM_FAILED: { - debugLog("Unable to start " + provider + ": it is perm failed"); + case PROVIDER_STATE_PERM_FAILED: + case PROVIDER_STATE_DESTROYED: { + debugLog("Unable to start " + provider + ": it is terminated"); break; } default: { @@ -303,17 +323,20 @@ class ControllerImpl extends LocationTimeZoneProviderController { synchronized (mSharedLock) { switch (providerState.stateEnum) { - case PROVIDER_STATE_STOPPED: { - // This should never happen: entering stopped does not trigger a state change. - warnLog("onProviderStateChange: Unexpected state change for stopped provider," + case PROVIDER_STATE_STARTED_INITIALIZING: + case PROVIDER_STATE_STOPPED: + case PROVIDER_STATE_DESTROYED: { + // This should never happen: entering initializing, stopped or destroyed are + // triggered by the controller so and should not trigger a state change + // callback. + warnLog("onProviderStateChange: Unexpected state change for provider," + " provider=" + provider); break; } - case PROVIDER_STATE_STARTED_INITIALIZING: case PROVIDER_STATE_STARTED_CERTAIN: case PROVIDER_STATE_STARTED_UNCERTAIN: { - // Entering started does not trigger a state change, so this only happens if an - // event is received while the provider is started. + // These are valid and only happen if an event is received while the provider is + // started. debugLog("onProviderStateChange: Received notification of a state change while" + " started, provider=" + provider); handleProviderStartedStateChange(providerState); @@ -349,17 +372,18 @@ class ControllerImpl extends LocationTimeZoneProviderController { // If a provider has failed, the other may need to be started. if (failedProvider == mPrimaryProvider) { - if (secondaryCurrentState.stateEnum != PROVIDER_STATE_PERM_FAILED) { - // The primary must have failed. Try to start the secondary. This does nothing if - // the provider is already started, and will leave the provider in - // {started initializing} if the provider is stopped. + if (!secondaryCurrentState.isTerminated()) { + // Try to start the secondary. This does nothing if the provider is already + // started, and will leave the provider in {started initializing} if the provider is + // stopped. tryStartProvider(mSecondaryProvider, mCurrentUserConfiguration); } } else if (failedProvider == mSecondaryProvider) { - // No-op: The secondary will only be active if the primary is uncertain or is failed. - // So, there the primary should not need to be started when the secondary fails. + // No-op: The secondary will only be active if the primary is uncertain or is + // terminated. So, there the primary should not need to be started when the secondary + // fails. if (primaryCurrentState.stateEnum != PROVIDER_STATE_STARTED_UNCERTAIN - && primaryCurrentState.stateEnum != PROVIDER_STATE_PERM_FAILED) { + && !primaryCurrentState.isTerminated()) { warnLog("Secondary provider unexpected reported a failure:" + " failed provider=" + failedProvider.getName() + ", primary provider=" + mPrimaryProvider @@ -367,19 +391,18 @@ class ControllerImpl extends LocationTimeZoneProviderController { } } - // If both providers are now failed, the controller needs to tell the next component in the - // time zone detection process. - if (primaryCurrentState.stateEnum == PROVIDER_STATE_PERM_FAILED - && secondaryCurrentState.stateEnum == PROVIDER_STATE_PERM_FAILED) { + // If both providers are now terminated, the controller needs to tell the next component in + // the time zone detection process. + if (primaryCurrentState.isTerminated() && secondaryCurrentState.isTerminated()) { - // If both providers are newly failed then the controller is uncertain by definition + // If both providers are newly terminated then the controller is uncertain by definition // and it will never recover so it can send a suggestion immediately. cancelUncertaintyTimeout(); - // If both providers are now failed, then a suggestion must be sent informing the time - // zone detector that there are no further updates coming in future. + // If both providers are now terminated, then a suggestion must be sent informing the + // time zone detector that there are no further updates coming in future. GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion( - "Both providers are permanently failed:" + "Both providers are terminated:" + " primary=" + primaryCurrentState.provider + ", secondary=" + secondaryCurrentState.provider); makeSuggestion(suggestion); diff --git a/services/core/java/com/android/server/location/timezone/HandlerThreadingDomain.java b/services/core/java/com/android/server/location/timezone/HandlerThreadingDomain.java index b59898adc340b..3055ff8a2b596 100644 --- a/services/core/java/com/android/server/location/timezone/HandlerThreadingDomain.java +++ b/services/core/java/com/android/server/location/timezone/HandlerThreadingDomain.java @@ -21,6 +21,10 @@ import android.annotation.NonNull; import android.os.Handler; import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; /** * The real implementation of {@link ThreadingDomain} that uses a {@link Handler}. @@ -57,6 +61,38 @@ final class HandlerThreadingDomain extends ThreadingDomain { getHandler().post(r); } + @Override + V postAndWait(@NonNull Callable callable, @DurationMillisLong long durationMillis) + throws Exception { + // Calling this on this domain's thread would lead to deadlock. + assertNotCurrentThread(); + + AtomicReference resultReference = new AtomicReference<>(); + AtomicReference exceptionReference = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + post(() -> { + try { + resultReference.set(callable.call()); + } catch (Exception e) { + exceptionReference.set(e); + } finally { + latch.countDown(); + } + }); + + try { + if (!latch.await(durationMillis, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("Timed out"); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + if (exceptionReference.get() != null) { + throw exceptionReference.get(); + } + return resultReference.get(); + } + @Override void postDelayed(@NonNull Runnable r, @DurationMillisLong long delayMillis) { getHandler().postDelayed(r, delayMillis); diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerService.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerService.java index 880dddf3d91bc..78e7f6b2bb4b1 100644 --- a/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerService.java +++ b/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerService.java @@ -153,10 +153,14 @@ public class LocationTimeZoneManagerService extends Binder { /** The shared lock from {@link #mThreadingDomain}. */ @NonNull private final Object mSharedLock; - // Lazily initialized. Non-null and effectively final after onSystemThirdPartyAppsCanStart(). + // Lazily initialized. Can be null if the service has been stopped. @GuardedBy("mSharedLock") private ControllerImpl mLocationTimeZoneDetectorController; + // Lazily initialized. Can be null if the service has been stopped. + @GuardedBy("mSharedLock") + private ControllerEnvironmentImpl mEnvironment; + LocationTimeZoneManagerService(Context context) { mContext = context.createAttributionContext(ATTRIBUTION_TAG); mHandler = FgThread.getHandler(); @@ -178,31 +182,59 @@ public class LocationTimeZoneManagerService extends Binder { } void onSystemThirdPartyAppsCanStart() { - // Called on an arbitrary thread during initialization. - synchronized (mSharedLock) { - LocationTimeZoneProvider primary = createPrimaryProvider(); - LocationTimeZoneProvider secondary = createSecondaryProvider(); - mLocationTimeZoneDetectorController = - new ControllerImpl(mThreadingDomain, primary, secondary); - ControllerCallbackImpl callback = new ControllerCallbackImpl(mThreadingDomain); - ControllerEnvironmentImpl environment = new ControllerEnvironmentImpl( - mThreadingDomain, mLocationTimeZoneDetectorController); + // Called on an arbitrary thread during initialization. We do not want to wait for + // completion as it would delay boot. + final boolean waitForCompletion = false; + startInternal(waitForCompletion); + } - // Initialize the controller on the mThreadingDomain thread: this ensures that the - // ThreadingDomain requirements for the controller / environment methods are honored. - mThreadingDomain.post(() -> - mLocationTimeZoneDetectorController.initialize(environment, callback)); + /** + * Starts the service during server initialization or during tests after a call to + * {@link #stop()}. + */ + void start() { + enforceManageTimeZoneDetectorPermission(); + + final boolean waitForCompletion = true; + startInternal(waitForCompletion); + } + + /** + * Starts the service during server initialization or during tests after a call to + * {@link #stop()}. + * + *

To avoid tests needing to sleep, when {@code waitForCompletion} is {@code true}, this + * method will not return until all the system server components have started. + */ + private void startInternal(boolean waitForCompletion) { + Runnable runnable = () -> { + synchronized (mSharedLock) { + if (mLocationTimeZoneDetectorController == null) { + LocationTimeZoneProvider primary = createPrimaryProvider(); + LocationTimeZoneProvider secondary = createSecondaryProvider(); + mLocationTimeZoneDetectorController = + new ControllerImpl(mThreadingDomain, primary, secondary); + ControllerCallbackImpl callback = new ControllerCallbackImpl( + mThreadingDomain); + mEnvironment = new ControllerEnvironmentImpl( + mThreadingDomain, mLocationTimeZoneDetectorController); + mLocationTimeZoneDetectorController.initialize(mEnvironment, callback); + } + } + }; + if (waitForCompletion) { + mThreadingDomain.postAndWait(runnable, BLOCKING_OP_WAIT_DURATION_MILLIS); + } else { + mThreadingDomain.post(runnable); } } private LocationTimeZoneProvider createPrimaryProvider() { - if (isDisabled(PRIMARY_PROVIDER_NAME)) { - return new NullLocationTimeZoneProvider(mThreadingDomain, PRIMARY_PROVIDER_NAME); - } - LocationTimeZoneProviderProxy proxy; if (isInSimulationMode(PRIMARY_PROVIDER_NAME)) { proxy = new SimulatedLocationTimeZoneProviderProxy(mContext, mThreadingDomain); + } else if (isDisabled(PRIMARY_PROVIDER_NAME)) { + proxy = new NullLocationTimeZoneProviderProxy(mContext, mThreadingDomain); } else { proxy = new RealLocationTimeZoneProviderProxy( mContext, @@ -217,13 +249,11 @@ public class LocationTimeZoneManagerService extends Binder { } private LocationTimeZoneProvider createSecondaryProvider() { - if (isDisabled(SECONDARY_PROVIDER_NAME)) { - return new NullLocationTimeZoneProvider(mThreadingDomain, SECONDARY_PROVIDER_NAME); - } - LocationTimeZoneProviderProxy proxy; if (isInSimulationMode(SECONDARY_PROVIDER_NAME)) { proxy = new SimulatedLocationTimeZoneProviderProxy(mContext, mThreadingDomain); + } else if (isDisabled(SECONDARY_PROVIDER_NAME)) { + proxy = new NullLocationTimeZoneProviderProxy(mContext, mThreadingDomain); } else { proxy = new RealLocationTimeZoneProviderProxy( mContext, @@ -274,6 +304,25 @@ public class LocationTimeZoneManagerService extends Binder { return Objects.equals(systemPropertyProviderMode, mode); } + /** + * Stops the service for tests. To avoid tests needing to sleep, this method will not return + * until all the system server components have stopped. + */ + void stop() { + enforceManageTimeZoneDetectorPermission(); + + mThreadingDomain.postAndWait(() -> { + synchronized (mSharedLock) { + if (mLocationTimeZoneDetectorController != null) { + mLocationTimeZoneDetectorController.destroy(); + mLocationTimeZoneDetectorController = null; + mEnvironment.destroy(); + mEnvironment = null; + } + } + }, BLOCKING_OP_WAIT_DURATION_MILLIS); + } + @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, @@ -335,7 +384,7 @@ public class LocationTimeZoneManagerService extends Binder { ipw.println("LocationTimeZoneManagerService:"); ipw.increaseIndent(); if (mLocationTimeZoneDetectorController == null) { - ipw.println("{Uninitialized}"); + ipw.println("{Stopped}"); } else { mLocationTimeZoneDetectorController.dump(ipw, args); } diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerShellCommand.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerShellCommand.java index 9b8863f741b70..b6fa1102c172f 100644 --- a/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerShellCommand.java +++ b/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerShellCommand.java @@ -32,6 +32,8 @@ class LocationTimeZoneManagerShellCommand extends ShellCommand { private static final List VALID_PROVIDER_NAMES = Arrays.asList(PRIMARY_PROVIDER_NAME, SECONDARY_PROVIDER_NAME); + private static final String CMD_START = "start"; + private static final String CMD_STOP = "stop"; private static final String CMD_SEND_PROVIDER_TEST_COMMAND = "send_provider_test_command"; private final LocationTimeZoneManagerService mService; @@ -47,6 +49,12 @@ class LocationTimeZoneManagerShellCommand extends ShellCommand { } switch (cmd) { + case CMD_START: { + return runStart(); + } + case CMD_STOP: { + return runStop(); + } case CMD_SEND_PROVIDER_TEST_COMMAND: { return runSendProviderTestCommand(); } @@ -62,7 +70,12 @@ class LocationTimeZoneManagerShellCommand extends ShellCommand { pw.println("Location Time Zone Manager (location_time_zone_manager) commands:"); pw.println(" help"); pw.println(" Print this help text."); - pw.printf(" %s \n", CMD_SEND_PROVIDER_TEST_COMMAND); + pw.printf(" %s\n", CMD_START); + pw.println(" Starts the location_time_zone_manager, creating time zone providers."); + pw.printf(" %s\n", CMD_STOP); + pw.println(" Stops the location_time_zone_manager, destroying time zone providers."); + pw.printf(" %s \n", + CMD_SEND_PROVIDER_TEST_COMMAND); pw.println(" Passes a test command to the named provider."); pw.println(); pw.printf("%s details:\n", CMD_SEND_PROVIDER_TEST_COMMAND); @@ -91,6 +104,30 @@ class LocationTimeZoneManagerShellCommand extends ShellCommand { pw.println(); } + private int runStart() { + try { + mService.start(); + } catch (RuntimeException e) { + reportError(e); + return 1; + } + PrintWriter outPrintWriter = getOutPrintWriter(); + outPrintWriter.println("Service started"); + return 0; + } + + private int runStop() { + try { + mService.stop(); + } catch (RuntimeException e) { + reportError(e); + return 1; + } + PrintWriter outPrintWriter = getOutPrintWriter(); + outPrintWriter.println("Service stopped"); + return 0; + } + private int runSendProviderTestCommand() { PrintWriter outPrintWriter = getOutPrintWriter(); diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProvider.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProvider.java index d25e4a866a398..132c1671f725b 100644 --- a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProvider.java +++ b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProvider.java @@ -21,6 +21,7 @@ import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESU import static com.android.server.location.timezone.LocationTimeZoneManagerService.debugLog; import static com.android.server.location.timezone.LocationTimeZoneManagerService.warnLog; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED; import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED; import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_CERTAIN; import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING; @@ -52,8 +53,7 @@ import java.util.Objects; /** * A facade used by the {@link LocationTimeZoneProviderController} to interact with a location time - * zone provider. The provider could have a binder implementation with logic running in another - * process, or could be a stubbed instance when no real provider is registered. + * zone provider. The provider implementation will typically have logic running in another process. * *

The provider is supplied with a {@link ProviderListener} via {@link * #initialize(ProviderListener)}. This starts communication of asynchronous detection / error @@ -61,6 +61,10 @@ import java.util.Objects; * ProviderListener#onProviderStateChange} method. This call must be made on the * {@link Handler} thread from the {@link ThreadingDomain} passed to the constructor. * + *

This class is also responsible for monitoring the initialization timeout for a provider. i.e. + * if the provider fails to send its first suggestion within a certain time, this is the component + * responsible for generating the necessary "uncertain" event. + * *

All incoming calls from the controller except for {@link * LocationTimeZoneProvider#dump(android.util.IndentingPrintWriter, String[])} will be made on the * {@link Handler} thread of the {@link ThreadingDomain} passed to the constructor. @@ -86,7 +90,7 @@ abstract class LocationTimeZoneProvider implements Dumpable { @IntDef(prefix = "PROVIDER_STATE_", value = { PROVIDER_STATE_UNKNOWN, PROVIDER_STATE_STARTED_INITIALIZING, PROVIDER_STATE_STARTED_CERTAIN, PROVIDER_STATE_STARTED_UNCERTAIN, - PROVIDER_STATE_STOPPED, PROVIDER_STATE_PERM_FAILED }) + PROVIDER_STATE_STOPPED, PROVIDER_STATE_PERM_FAILED, PROVIDER_STATE_DESTROYED }) @interface ProviderStateEnum {} /** @@ -117,12 +121,19 @@ abstract class LocationTimeZoneProvider implements Dumpable { static final int PROVIDER_STATE_STOPPED = 4; /** - * The provider has failed and cannot be re-started. + * The provider has failed and cannot be restarted. This is a terminated state triggered by + * the provider itself. * - * Providers may enter this state after a provider is started. + * Providers may enter this state any time after a provider is started. */ static final int PROVIDER_STATE_PERM_FAILED = 5; + /** + * The provider has been destroyed by the controller and cannot be restarted. Similar to + * {@link #PROVIDER_STATE_PERM_FAILED} except that a provider is set into this state. + */ + static final int PROVIDER_STATE_DESTROYED = 6; + /** The {@link LocationTimeZoneProvider} the state is for. */ public final @NonNull LocationTimeZoneProvider provider; @@ -201,12 +212,14 @@ abstract class LocationTimeZoneProvider implements Dumpable { case PROVIDER_STATE_STARTED_INITIALIZING: case PROVIDER_STATE_STARTED_CERTAIN: case PROVIDER_STATE_STARTED_UNCERTAIN: { - // These can go to each other or PROVIDER_STATE_PERM_FAILED. + // These can go to each other or either of PROVIDER_STATE_PERM_FAILED and + // PROVIDER_STATE_DESTROYED. break; } - case PROVIDER_STATE_PERM_FAILED: { + case PROVIDER_STATE_PERM_FAILED: + case PROVIDER_STATE_DESTROYED: { throw new IllegalArgumentException("Illegal transition out of " - + prettyPrintStateEnum(PROVIDER_STATE_UNKNOWN)); + + prettyPrintStateEnum(this.stateEnum)); } default: { throw new IllegalArgumentException("Invalid this.stateEnum=" + this.stateEnum); @@ -237,10 +250,12 @@ abstract class LocationTimeZoneProvider implements Dumpable { } break; } - case PROVIDER_STATE_PERM_FAILED: { + case PROVIDER_STATE_PERM_FAILED: + case PROVIDER_STATE_DESTROYED: { if (event != null || currentUserConfig != null) { throw new IllegalArgumentException( - "Perf failed state: event and currentUserConfig must be null" + "Terminal state: event and currentUserConfig must be null" + + ", newStateEnum=" + newStateEnum + ", event=" + event + ", currentUserConfig=" + currentUserConfig); } @@ -260,6 +275,12 @@ abstract class LocationTimeZoneProvider implements Dumpable { || stateEnum == PROVIDER_STATE_STARTED_UNCERTAIN; } + /** Returns {@code true} if {@link #stateEnum} is one of the terminated states. */ + boolean isTerminated() { + return stateEnum == PROVIDER_STATE_PERM_FAILED + || stateEnum == PROVIDER_STATE_DESTROYED; + } + @Override public String toString() { // this.provider is omitted deliberately to avoid recursion, since the provider holds @@ -304,6 +325,8 @@ abstract class LocationTimeZoneProvider implements Dumpable { return "Started uncertain (" + PROVIDER_STATE_STARTED_UNCERTAIN + ")"; case PROVIDER_STATE_PERM_FAILED: return "Perm failure (" + PROVIDER_STATE_PERM_FAILED + ")"; + case PROVIDER_STATE_DESTROYED: + return "Destroyed (" + PROVIDER_STATE_DESTROYED + ")"; case PROVIDER_STATE_UNKNOWN: default: return "Unknown (" + state + ")"; @@ -340,7 +363,7 @@ abstract class LocationTimeZoneProvider implements Dumpable { } /** - * Called before the provider is first used. + * Initializes the provider. Called before the provider is first used. */ final void initialize(@NonNull ProviderListener providerListener) { mThreadingDomain.assertCurrentThread(); @@ -372,8 +395,33 @@ abstract class LocationTimeZoneProvider implements Dumpable { /** * Implemented by subclasses to do work during {@link #initialize}. */ + @GuardedBy("mSharedLock") abstract void onInitialize(); + /** + * Destroys the provider. Called after the provider is stopped. This instance will not be called + * again by the {@link LocationTimeZoneProviderController}. + */ + final void destroy() { + mThreadingDomain.assertCurrentThread(); + + synchronized (mSharedLock) { + ProviderState currentState = mCurrentState.get(); + if (!currentState.isTerminated()) { + ProviderState destroyedState = currentState + .newState(PROVIDER_STATE_DESTROYED, null, null, "destroy() called"); + setCurrentState(destroyedState, false); + onDestroy(); + } + } + } + + /** + * Implemented by subclasses to do work during {@link #destroy()}. + */ + @GuardedBy("mSharedLock") + abstract void onDestroy(); + /** * Set the current state, for use by this class and subclasses only. If {@code #notifyChanges} * is {@code true} and {@code newState} is not equal to the old state, then {@link @@ -460,17 +508,23 @@ abstract class LocationTimeZoneProvider implements Dumpable { PROVIDER_STATE_STARTED_UNCERTAIN, null /* event */, currentState.currentUserConfiguration, "initialization timeout"); setCurrentState(newState, true); + } else { + warnLog("handleInitializationTimeout: Initialization timeout triggered when in" + + " an unexpected state=" + currentState); } } } /** - * Implemented by subclasses to do work during {@link #startUpdates}. + * Implemented by subclasses to do work during {@link #startUpdates}. This is where the logic + * to start the real provider should be implemented. + * + * @param initializationTimeout the initialization timeout to pass to the real provider */ abstract void onStartUpdates(@NonNull Duration initializationTimeout); /** - * Stops the provider. It is an error* to call this method except when the {@link + * Stops the provider. It is an error to call this method except when the {@link * #getCurrentState()} is one of the started states. This method must be * called using the handler thread from the {@link ThreadingDomain}. */ @@ -505,6 +559,8 @@ abstract class LocationTimeZoneProvider implements Dumpable { * {@code false} and a "Not implemented" error message. */ void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) { + Objects.requireNonNull(testCommand); + if (callback != null) { Bundle result = new Bundle(); result.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, false); @@ -525,11 +581,12 @@ abstract class LocationTimeZoneProvider implements Dumpable { ProviderState currentState = mCurrentState.get(); int eventType = timeZoneProviderEvent.getType(); switch (currentState.stateEnum) { + case PROVIDER_STATE_DESTROYED: case PROVIDER_STATE_PERM_FAILED: { - // After entering perm failed, there is nothing to do. The remote peer is + // After entering a terminated state, there is nothing to do. The remote peer is // supposed to stop sending events after it has reported perm failure. warnLog("handleTimeZoneProviderEvent: Event=" + timeZoneProviderEvent - + " received for provider=" + this + " when in failed state"); + + " received for provider=" + this + " when in terminated state"); return; } case PROVIDER_STATE_STOPPED: { diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderController.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderController.java index 45ec400aa4002..ec2bc13b8a16e 100644 --- a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderController.java +++ b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderController.java @@ -94,6 +94,9 @@ abstract class LocationTimeZoneProviderController implements Dumpable { @DurationMillisLong abstract long getUncertaintyTimeoutDelayMillis(); + /** Called if the geolocation time zone detection is being reconfigured. */ + abstract void destroy(); + /** * Used by {@link LocationTimeZoneProviderController} to obtain information from the surrounding * service. It can easily be faked for tests. @@ -108,6 +111,9 @@ abstract class LocationTimeZoneProviderController implements Dumpable { mSharedLock = threadingDomain.getLockObject(); } + /** Destroys the environment, i.e. deregisters listeners, etc. */ + abstract void destroy(); + /** Returns the {@link ConfigurationInternal} for the current user of the device. */ abstract ConfigurationInternal getCurrentUserConfigurationInternal(); diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderProxy.java index 0937b3eb46bdd..8368b5ed5d75a 100644 --- a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderProxy.java +++ b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderProxy.java @@ -70,7 +70,7 @@ abstract class LocationTimeZoneProviderProxy implements Dumpable { /** * Initializes the proxy. The supplied listener can expect to receive all events after this - * point. This method also calls {@link #onInitialize()} for subclasses to handle their own + * point. This method calls {@link #onInitialize()} for subclasses to handle their own * initialization. */ void initialize(@NonNull Listener listener) { @@ -85,10 +85,28 @@ abstract class LocationTimeZoneProviderProxy implements Dumpable { } /** - * Initializes the proxy. This is called after {@link #mListener} is set. + * Implemented by subclasses to initializes the proxy. This is called after {@link #mListener} + * is set. */ + @GuardedBy("mSharedLock") abstract void onInitialize(); + /** + * Destroys the proxy. This method calls {@link #onDestroy()} for subclasses to handle their own + * destruction. + */ + void destroy() { + synchronized (mSharedLock) { + onDestroy(); + } + } + + /** + * Implemented by subclasses to destroy the proxy. + */ + @GuardedBy("mSharedLock") + abstract void onDestroy(); + /** * Sets a new request for the provider. */ diff --git a/services/core/java/com/android/server/location/timezone/NullLocationTimeZoneProvider.java b/services/core/java/com/android/server/location/timezone/NullLocationTimeZoneProvider.java deleted file mode 100644 index 4b321e628e43a..0000000000000 --- a/services/core/java/com/android/server/location/timezone/NullLocationTimeZoneProvider.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2020 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.location.timezone; - -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.util.IndentingPrintWriter; - -import java.time.Duration; - -/** - * A {@link LocationTimeZoneProvider} that provides minimal responses needed for the {@link - * LocationTimeZoneProviderController} to operate correctly when there is no "real" provider - * configured. This can be used during development / testing, or in a production build when the - * platform supports more providers than are needed for an Android deployment. - * - *

For example, if the {@link LocationTimeZoneProviderController} supports a primary - * and a secondary {@link LocationTimeZoneProvider}, but only a primary is configured, the secondary - * config will be left null and the {@link LocationTimeZoneProvider} implementation will be - * defaulted to a {@link NullLocationTimeZoneProvider}. The {@link NullLocationTimeZoneProvider} - * enters a {@link ProviderState#PROVIDER_STATE_PERM_FAILED} state immediately after being started - * for the first time and sends the appropriate event, which ensures the {@link - * LocationTimeZoneProviderController} won't expect any further {@link - * TimeZoneProviderEvent}s to come from it, and won't attempt to use it - * again. - */ -class NullLocationTimeZoneProvider extends LocationTimeZoneProvider { - - private static final String TAG = "NullLocationTimeZoneProvider"; - - /** Creates the instance. */ - NullLocationTimeZoneProvider(@NonNull ThreadingDomain threadingDomain, - @NonNull String providerName) { - super(threadingDomain, providerName); - } - - @Override - void onInitialize() { - // No-op - } - - @Override - void onStartUpdates(@NonNull Duration initializationTimeout) { - // Report a failure (asynchronously using the mThreadingDomain thread to avoid recursion). - mThreadingDomain.post(()-> { - // Enter the perm-failed state. - ProviderState currentState = mCurrentState.get(); - ProviderState failedState = currentState.newState( - PROVIDER_STATE_PERM_FAILED, null, null, "Stubbed provider"); - setCurrentState(failedState, true); - }); - } - - @Override - void onStopUpdates() { - // Ignored - this implementation is always permanently failed. - } - - @Override - public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) { - synchronized (mSharedLock) { - ipw.println("{Stubbed LocationTimeZoneProvider}"); - ipw.println("mProviderName=" + mProviderName); - ipw.println("mCurrentState=" + mCurrentState); - } - } - - @Override - public String toString() { - synchronized (mSharedLock) { - return "NullLocationTimeZoneProvider{" - + "mProviderName='" + mProviderName + '\'' - + ", mCurrentState='" + mCurrentState + '\'' - + '}'; - } - } -} diff --git a/services/core/java/com/android/server/location/timezone/NullLocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/location/timezone/NullLocationTimeZoneProviderProxy.java new file mode 100644 index 0000000000000..c2abbf9a1b8c7 --- /dev/null +++ b/services/core/java/com/android/server/location/timezone/NullLocationTimeZoneProviderProxy.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 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.location.timezone; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.os.Bundle; +import android.os.RemoteCallback; +import android.service.timezone.TimeZoneProviderService; +import android.util.IndentingPrintWriter; + +/** + * A {@link LocationTimeZoneProviderProxy} that provides minimal responses needed for the {@link + * BinderLocationTimeZoneProvider} to operate correctly when there is no "real" provider + * configured / enabled. This can be used during development / testing, or in a production build + * when the platform supports more providers than are needed for an Android deployment. + * + *

For example, if the {@link LocationTimeZoneProviderController} supports a primary + * and a secondary {@link LocationTimeZoneProvider}, but only a primary is configured, the secondary + * config will be left null and the {@link LocationTimeZoneProviderProxy} implementation will be + * defaulted to a {@link NullLocationTimeZoneProviderProxy}. The {@link + * NullLocationTimeZoneProviderProxy} sends a "permanent failure" event immediately after being + * started for the first time, which ensures the {@link LocationTimeZoneProviderController} won't + * expect any further {@link TimeZoneProviderEvent}s to come from it, and won't attempt to use it + * again. + */ +class NullLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy { + + /** Creates the instance. */ + NullLocationTimeZoneProviderProxy( + @NonNull Context context, @NonNull ThreadingDomain threadingDomain) { + super(context, threadingDomain); + } + + @Override + void onInitialize() { + // No-op + } + + @Override + void onDestroy() { + // No-op + } + + @Override + void setRequest(@NonNull TimeZoneProviderRequest request) { + if (request.sendUpdates()) { + TimeZoneProviderEvent event = TimeZoneProviderEvent.createPermanentFailureEvent( + "Provider is disabled"); + handleTimeZoneProviderEvent(event); + } + } + + @Override + void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) { + if (callback != null) { + Bundle result = new Bundle(); + result.putBoolean(TimeZoneProviderService.TEST_COMMAND_RESULT_SUCCESS_KEY, false); + result.putString(TimeZoneProviderService.TEST_COMMAND_RESULT_ERROR_KEY, + "Provider is disabled"); + callback.sendResult(result); + } + } + + @Override + public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) { + synchronized (mSharedLock) { + ipw.println("{NullLocationTimeZoneProviderProxy}"); + } + } +} diff --git a/services/core/java/com/android/server/location/timezone/RealLocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/location/timezone/RealLocationTimeZoneProviderProxy.java index ca8109586f45d..0904ba419b3d6 100644 --- a/services/core/java/com/android/server/location/timezone/RealLocationTimeZoneProviderProxy.java +++ b/services/core/java/com/android/server/location/timezone/RealLocationTimeZoneProviderProxy.java @@ -110,6 +110,11 @@ class RealLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy { } } + @Override + void onDestroy() { + mServiceWatcher.unregister(); + } + private boolean register() { boolean resolves = mServiceWatcher.checkServiceResolves(); if (resolves) { @@ -192,6 +197,7 @@ class RealLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy { @Override public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) { synchronized (mSharedLock) { + ipw.println("{RealLocationTimeZoneProviderProxy}"); ipw.println("mRequest=" + mRequest); mServiceWatcher.dump(null, ipw, args); } diff --git a/services/core/java/com/android/server/location/timezone/SimulatedLocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/location/timezone/SimulatedLocationTimeZoneProviderProxy.java index d06438e7ac408..1b63a5c6227b7 100644 --- a/services/core/java/com/android/server/location/timezone/SimulatedLocationTimeZoneProviderProxy.java +++ b/services/core/java/com/android/server/location/timezone/SimulatedLocationTimeZoneProviderProxy.java @@ -69,6 +69,11 @@ class SimulatedLocationTimeZoneProviderProxy extends LocationTimeZoneProviderPro // No-op - nothing to do for the simulated provider. } + @Override + void onDestroy() { + // No-op - nothing to do for the simulated provider. + } + void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) { mThreadingDomain.assertCurrentThread(); @@ -152,6 +157,7 @@ class SimulatedLocationTimeZoneProviderProxy extends LocationTimeZoneProviderPro @Override public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) { synchronized (mSharedLock) { + ipw.println("{SimulatedLocationTimeZoneProviderProxy}"); ipw.println("mRequest=" + mRequest); ipw.println("mLastEvent=" + mLastEvent); diff --git a/services/core/java/com/android/server/location/timezone/TestCommand.java b/services/core/java/com/android/server/location/timezone/TestCommand.java index 70113b1c06c7d..0df3ca087fc70 100644 --- a/services/core/java/com/android/server/location/timezone/TestCommand.java +++ b/services/core/java/com/android/server/location/timezone/TestCommand.java @@ -21,6 +21,8 @@ import android.net.Uri; import android.os.Bundle; import android.os.ShellCommand; +import com.android.internal.annotations.VisibleForTesting; + import java.io.PrintWriter; import java.util.Arrays; import java.util.Objects; @@ -50,6 +52,12 @@ final class TestCommand { mArgs = Objects.requireNonNull(args); } + @VisibleForTesting + @NonNull + public static TestCommand createForTests(@NonNull String type, @NonNull Bundle args) { + return new TestCommand(type, args); + } + /** * Creates a {@link TestCommand} from a {@link ShellCommand}'s remaining arguments. * diff --git a/services/core/java/com/android/server/location/timezone/ThreadingDomain.java b/services/core/java/com/android/server/location/timezone/ThreadingDomain.java index d04d4a62aa581..4ada6f50b40e3 100644 --- a/services/core/java/com/android/server/location/timezone/ThreadingDomain.java +++ b/services/core/java/com/android/server/location/timezone/ThreadingDomain.java @@ -21,6 +21,8 @@ import android.annotation.NonNull; import com.android.internal.util.Preconditions; +import java.util.concurrent.Callable; + /** * A class that can be used to enforce / indicate a set of components that need to share threading * behavior such as a shared lock object and a common thread, with async execution support. @@ -58,7 +60,7 @@ abstract class ThreadingDomain { * being used within a set of components, a lot of races can be avoided. */ void assertCurrentThread() { - Preconditions.checkArgument(Thread.currentThread() == getThread()); + Preconditions.checkState(Thread.currentThread() == getThread()); } /** @@ -66,7 +68,7 @@ abstract class ThreadingDomain { * Generally useful for documenting expectations in the code and avoiding deadlocks. */ void assertNotCurrentThread() { - Preconditions.checkArgument(Thread.currentThread() != getThread()); + Preconditions.checkState(Thread.currentThread() != getThread()); } /** @@ -74,6 +76,37 @@ abstract class ThreadingDomain { */ abstract void post(@NonNull Runnable runnable); + /** + * Executes the supplied runnable and waits for up to the duration specified for it to be + * executed. This is only intended for use by test and/or shell command code as it consumes + * multiple threads and could lead to deadlocks. + * + *

An {@link IllegalStateException} will be thrown if calling this method would cause a + * deadlock, e.g. if it is called using the threading domain's own thread. + */ + final void postAndWait(@NonNull Runnable runnable, @DurationMillisLong long durationMillis) { + try { + postAndWait(() -> { + runnable.run(); + return null; + }, durationMillis); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Executes the supplied callable and waits for up to the duration specified for it to be + * executed. This is only intended for use by test and/or shell command code as it consumes + * multiple threads and could lead to deadlocks. + * + *

An {@link IllegalStateException} will be thrown if calling this method would cause a + * deadlock, e.g. if it is called using the threading domain's own thread. + */ + abstract V postAndWait( + @NonNull Callable callable, @DurationMillisLong long durationMillis) + throws Exception; + /** * Execute the supplied runnable on the threading domain's thread with a delay. */ diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java index 2d50390c27a9c..203a8a4e02ccf 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java @@ -30,6 +30,12 @@ public interface TimeZoneDetectorInternal extends Dumpable.Container { /** Adds a listener that will be invoked when time zone detection configuration is changed. */ void addConfigurationListener(ConfigurationChangeListener listener); + /** + * Removes a listener previously added via {@link + * #addConfigurationListener(ConfigurationChangeListener)}. + */ + void removeConfigurationListener(ConfigurationChangeListener listener); + /** Returns the {@link ConfigurationInternal} for the current user. */ ConfigurationInternal getCurrentUserConfigurationInternal(); diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java index f0ce827cec5e9..2d5dacdd6acca 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java @@ -68,6 +68,13 @@ public final class TimeZoneDetectorInternalImpl implements TimeZoneDetectorInter } } + @Override + public void removeConfigurationListener(ConfigurationChangeListener listener) { + synchronized (mConfigurationListeners) { + mConfigurationListeners.remove(Objects.requireNonNull(listener)); + } + } + @Override @NonNull public ConfigurationInternal getCurrentUserConfigurationInternal() { diff --git a/services/tests/servicestests/src/com/android/server/location/timezone/ControllerImplTest.java b/services/tests/servicestests/src/com/android/server/location/timezone/ControllerImplTest.java index 4de4d95004ddf..23365f70b2f42 100644 --- a/services/tests/servicestests/src/com/android/server/location/timezone/ControllerImplTest.java +++ b/services/tests/servicestests/src/com/android/server/location/timezone/ControllerImplTest.java @@ -960,6 +960,11 @@ public class ControllerImplTest { mConfigurationInternal = Objects.requireNonNull(configurationInternal); } + @Override + void destroy() { + // No-op test impl. + } + @Override ConfigurationInternal getCurrentUserConfigurationInternal() { return mConfigurationInternal; @@ -1024,6 +1029,7 @@ public class ControllerImplTest { /** Used to track historic provider states for tests. */ private final TestState mTestProviderState = new TestState<>(); private boolean mInitialized; + private boolean mDestroyed; /** * Creates the instance. @@ -1037,6 +1043,11 @@ public class ControllerImplTest { mInitialized = true; } + @Override + void onDestroy() { + mDestroyed = true; + } + @Override void onSetCurrentState(ProviderState newState) { mTestProviderState.set(newState); diff --git a/services/tests/servicestests/src/com/android/server/location/timezone/HandlerThreadingDomainTest.java b/services/tests/servicestests/src/com/android/server/location/timezone/HandlerThreadingDomainTest.java index c36812c3af645..02de24de435e7 100644 --- a/services/tests/servicestests/src/com/android/server/location/timezone/HandlerThreadingDomainTest.java +++ b/services/tests/servicestests/src/com/android/server/location/timezone/HandlerThreadingDomainTest.java @@ -30,10 +30,12 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import java.time.Duration; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; /** Tests for {@link HandlerThreadingDomain}. */ @Presubmit @@ -115,6 +117,7 @@ public class HandlerThreadingDomainTest { }); domain.post(testLogic); testLogic.assertCompletesWithin(60, TimeUnit.SECONDS); + assertTrue(testLogic.isComplete()); assertTrue(ranOnExpectedThread.get()); } @@ -123,17 +126,65 @@ public class HandlerThreadingDomainTest { ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler); long beforeExecutionNanos = System.nanoTime(); + Duration executionDelay = Duration.ofSeconds(5); + + AtomicReference executionNanosHolder = new AtomicReference<>(); AtomicBoolean ranOnExpectedThread = new AtomicBoolean(false); LatchedRunnable testLogic = new LatchedRunnable(() -> { ranOnExpectedThread.set(Thread.currentThread() == mTestHandler.getLooper().getThread()); + executionNanosHolder.set(System.nanoTime()); }); - domain.postDelayed(testLogic, 5000); - testLogic.assertCompletesWithin(60, TimeUnit.SECONDS); + domain.postDelayed(testLogic, executionDelay.toMillis()); + long afterPostNanos = System.nanoTime(); + + testLogic.assertCompletesWithin( + executionDelay.multipliedBy(10).toMillis(), TimeUnit.MILLISECONDS); + long afterWaitNanos = System.nanoTime(); + + assertTrue(testLogic.isComplete()); assertTrue(ranOnExpectedThread.get()); + // The execution should not take place until at least delayDuration after postDelayed(). + Duration actualExecutionDelay = + Duration.ofNanos(executionNanosHolder.get() - beforeExecutionNanos); + assertTrue(actualExecutionDelay.compareTo(executionDelay) >= 0); + + // The time taken in postDelayed() should be negligible. Certainly less than the + // executionDelay. + Duration postDuration = Duration.ofNanos(afterPostNanos - beforeExecutionNanos); + assertTrue(postDuration.compareTo(executionDelay) < 0); + + // The result should not be ready until at least executionDelay has elapsed. + Duration delayBeforeExecuted = Duration.ofNanos(afterWaitNanos - beforeExecutionNanos); + assertTrue(delayBeforeExecuted.compareTo(executionDelay) >= 0); + } + + @Test + public void postAndWait() throws Exception { + ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler); + + Duration workDuration = Duration.ofSeconds(5); + AtomicBoolean ranOnExpectedThread = new AtomicBoolean(false); + LatchedRunnable testLogic = new LatchedRunnable(() -> { + ranOnExpectedThread.set(Thread.currentThread() == mTestHandler.getLooper().getThread()); + + // The work takes workDuration to complete. + try { + Thread.sleep(workDuration.toMillis()); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + }); + + long beforeExecutionNanos = System.nanoTime(); + domain.postAndWait(testLogic, workDuration.multipliedBy(10).toMillis()); long afterExecutionNanos = System.nanoTime(); - assertTrue(afterExecutionNanos - beforeExecutionNanos >= TimeUnit.SECONDS.toNanos(5)); + Duration waitDuration = Duration.ofNanos(afterExecutionNanos - beforeExecutionNanos); + + assertTrue(waitDuration.compareTo(workDuration) >= 0); + assertTrue(testLogic.isComplete()); + assertTrue(ranOnExpectedThread.get()); } @Test diff --git a/services/tests/servicestests/src/com/android/server/location/timezone/LocationTimeZoneProviderTest.java b/services/tests/servicestests/src/com/android/server/location/timezone/LocationTimeZoneProviderTest.java new file mode 100644 index 0000000000000..49c67ea1b8f14 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/location/timezone/LocationTimeZoneProviderTest.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2020 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.location.timezone; + +import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_ERROR_KEY; +import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_SUCCESS_KEY; + +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_CERTAIN; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_UNCERTAIN; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STOPPED; +import static com.android.server.location.timezone.TestSupport.USER1_CONFIG_GEO_DETECTION_ENABLED; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; +import android.os.RemoteCallback; +import android.platform.test.annotations.Presubmit; +import android.service.timezone.TimeZoneProviderSuggestion; +import android.util.IndentingPrintWriter; + +import com.android.server.location.timezone.LocationTimeZoneProvider.ProviderListener; +import com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState; +import com.android.server.timezonedetector.ConfigurationInternal; +import com.android.server.timezonedetector.TestState; + +import org.junit.Before; +import org.junit.Test; + +import java.time.Duration; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Tests for {@link LocationTimeZoneProvider}. + */ +@Presubmit +public class LocationTimeZoneProviderTest { + + private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 123456789L; + + private TestThreadingDomain mTestThreadingDomain; + + private TestProviderListener mProviderListener; + + @Before + public void setUp() { + mTestThreadingDomain = new TestThreadingDomain(); + mProviderListener = new TestProviderListener(); + } + + @Test + public void lifecycle() { + String providerName = "arbitrary"; + TestLocationTimeZoneProvider provider = + new TestLocationTimeZoneProvider(mTestThreadingDomain, providerName); + + // initialize() + provider.initialize(mProviderListener); + provider.assertOnInitializeCalled(); + + ProviderState currentState = provider.getCurrentState(); + assertEquals(PROVIDER_STATE_STOPPED, currentState.stateEnum); + assertNull(currentState.currentUserConfiguration); + assertSame(provider, currentState.provider); + mTestThreadingDomain.assertQueueEmpty(); + + // startUpdates() + ConfigurationInternal config = USER1_CONFIG_GEO_DETECTION_ENABLED; + Duration arbitraryInitializationTimeout = Duration.ofMinutes(5); + Duration arbitraryInitializationTimeoutFuzz = Duration.ofMinutes(2); + provider.startUpdates(config, arbitraryInitializationTimeout, + arbitraryInitializationTimeoutFuzz); + + provider.assertOnStartCalled(arbitraryInitializationTimeout); + + currentState = provider.getCurrentState(); + assertSame(provider, currentState.provider); + assertEquals(PROVIDER_STATE_STARTED_INITIALIZING, currentState.stateEnum); + assertEquals(config, currentState.currentUserConfiguration); + assertNull(currentState.event); + // The initialization timeout should be queued. + Duration expectedInitializationTimeout = + arbitraryInitializationTimeout.plus(arbitraryInitializationTimeoutFuzz); + mTestThreadingDomain.assertSingleDelayedQueueItem(expectedInitializationTimeout); + // We don't intend to trigger the timeout, so clear it. + mTestThreadingDomain.removeAllQueuedRunnables(); + + // Entering started does not trigger an onProviderStateChanged() as it is requested by the + // controller. + mProviderListener.assertProviderChangeNotReported(); + + // Simulate a suggestion event being received. + TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder() + .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS) + .setTimeZoneIds(Arrays.asList("Europe/London")) + .build(); + TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(suggestion); + provider.simulateProviderEventReceived(event); + + currentState = provider.getCurrentState(); + assertSame(provider, currentState.provider); + assertEquals(PROVIDER_STATE_STARTED_CERTAIN, currentState.stateEnum); + assertEquals(event, currentState.event); + assertEquals(config, currentState.currentUserConfiguration); + mTestThreadingDomain.assertQueueEmpty(); + mProviderListener.assertProviderChangeReported(PROVIDER_STATE_STARTED_CERTAIN); + + // Simulate an uncertain event being received. + event = TimeZoneProviderEvent.createUncertainEvent(); + provider.simulateProviderEventReceived(event); + + currentState = provider.getCurrentState(); + assertSame(provider, currentState.provider); + assertEquals(PROVIDER_STATE_STARTED_UNCERTAIN, currentState.stateEnum); + assertEquals(event, currentState.event); + assertEquals(config, currentState.currentUserConfiguration); + mTestThreadingDomain.assertQueueEmpty(); + mProviderListener.assertProviderChangeReported(PROVIDER_STATE_STARTED_UNCERTAIN); + + // stopUpdates() + provider.stopUpdates(); + provider.assertOnStopUpdatesCalled(); + + currentState = provider.getCurrentState(); + assertSame(provider, currentState.provider); + assertEquals(PROVIDER_STATE_STOPPED, currentState.stateEnum); + assertNull(currentState.event); + assertNull(currentState.currentUserConfiguration); + mTestThreadingDomain.assertQueueEmpty(); + // Entering stopped does not trigger an onProviderStateChanged() as it is requested by the + // controller. + mProviderListener.assertProviderChangeNotReported(); + + // destroy() + provider.destroy(); + provider.assertOnDestroyCalled(); + } + + @Test + public void defaultHandleTestCommandImpl() { + String providerName = "primary"; + TestLocationTimeZoneProvider provider = + new TestLocationTimeZoneProvider(mTestThreadingDomain, providerName); + + TestCommand testCommand = TestCommand.createForTests("test", new Bundle()); + AtomicReference resultReference = new AtomicReference<>(); + RemoteCallback callback = new RemoteCallback(resultReference::set); + provider.handleTestCommand(testCommand, callback); + + Bundle result = resultReference.get(); + assertNotNull(result); + assertFalse(result.getBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY)); + assertNotNull(result.getString(TEST_COMMAND_RESULT_ERROR_KEY)); + } + + /** A test stand-in for the real {@link LocationTimeZoneProviderController}'s listener. */ + private static class TestProviderListener implements ProviderListener { + + private final TestState mReportedProviderStateChanges = new TestState<>(); + + @Override + public void onProviderStateChange(ProviderState providerState) { + mReportedProviderStateChanges.set(providerState); + } + + void assertProviderChangeReported(int expectedStateEnum) { + mReportedProviderStateChanges.assertChangeCount(1); + + ProviderState latest = mReportedProviderStateChanges.getLatest(); + assertEquals(expectedStateEnum, latest.stateEnum); + mReportedProviderStateChanges.commitLatest(); + } + + public void assertProviderChangeNotReported() { + mReportedProviderStateChanges.assertHasNotBeenSet(); + } + } + + private static class TestLocationTimeZoneProvider extends LocationTimeZoneProvider { + + private boolean mOnInitializeCalled; + private boolean mOnDestroyCalled; + private boolean mOnStartUpdatesCalled; + private Duration mInitializationTimeout; + private boolean mOnStopUpdatesCalled; + + /** Creates the instance. */ + TestLocationTimeZoneProvider(@NonNull ThreadingDomain threadingDomain, + @NonNull String providerName) { + super(threadingDomain, providerName); + } + + @Override + void onInitialize() { + mOnInitializeCalled = true; + } + + @Override + void onDestroy() { + mOnDestroyCalled = true; + } + + @Override + void onStartUpdates(@NonNull Duration initializationTimeout) { + mOnStartUpdatesCalled = true; + mInitializationTimeout = initializationTimeout; + } + + @Override + void onStopUpdates() { + mOnStopUpdatesCalled = true; + } + + @Override + public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) { + // No-op for tests + } + + void assertOnInitializeCalled() { + assertTrue(mOnInitializeCalled); + } + + void assertOnStartCalled(Duration expectedInitializationTimeout) { + assertTrue(mOnStartUpdatesCalled); + assertEquals(expectedInitializationTimeout, mInitializationTimeout); + } + + void simulateProviderEventReceived(TimeZoneProviderEvent event) { + handleTimeZoneProviderEvent(event); + } + + void assertOnStopUpdatesCalled() { + assertTrue(mOnStopUpdatesCalled); + } + + void assertOnDestroyCalled() { + assertTrue(mOnDestroyCalled); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/location/timezone/NullLocationTimeZoneProviderTest.java b/services/tests/servicestests/src/com/android/server/location/timezone/NullLocationTimeZoneProviderTest.java deleted file mode 100644 index e4a3ebd7afb8f..0000000000000 --- a/services/tests/servicestests/src/com/android/server/location/timezone/NullLocationTimeZoneProviderTest.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (C) 2020 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.location.timezone; - -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STOPPED; -import static com.android.server.location.timezone.TestSupport.USER1_CONFIG_GEO_DETECTION_ENABLED; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; - -import android.platform.test.annotations.Presubmit; -import android.util.IndentingPrintWriter; - -import com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState; -import com.android.server.timezonedetector.ConfigurationInternal; -import com.android.server.timezonedetector.TestState; - -import org.junit.Before; -import org.junit.Test; - -import java.time.Duration; - -/** - * Tests for {@link NullLocationTimeZoneProvider} and, indirectly, the class it extends - * {@link LocationTimeZoneProvider}. - */ -@Presubmit -public class NullLocationTimeZoneProviderTest { - - private TestThreadingDomain mTestThreadingDomain; - - private TestController mTestController; - - @Before - public void setUp() { - mTestThreadingDomain = new TestThreadingDomain(); - mTestController = new TestController(mTestThreadingDomain); - } - - @Test - public void initialization() { - String providerName = "primary"; - NullLocationTimeZoneProvider provider = - new NullLocationTimeZoneProvider(mTestThreadingDomain, providerName); - provider.initialize(providerState -> mTestController.onProviderStateChange(providerState)); - - ProviderState currentState = provider.getCurrentState(); - assertEquals(PROVIDER_STATE_STOPPED, currentState.stateEnum); - assertNull(currentState.currentUserConfiguration); - assertSame(provider, currentState.provider); - mTestThreadingDomain.assertQueueEmpty(); - } - - @Test - public void startSchedulesPermFailure() { - String providerName = "primary"; - NullLocationTimeZoneProvider provider = - new NullLocationTimeZoneProvider(mTestThreadingDomain, providerName); - provider.initialize(providerState -> mTestController.onProviderStateChange(providerState)); - - ConfigurationInternal config = USER1_CONFIG_GEO_DETECTION_ENABLED; - Duration arbitraryInitializationTimeout = Duration.ofMinutes(5); - Duration arbitraryInitializationTimeoutFuzz = Duration.ofMinutes(2); - provider.startUpdates(config, arbitraryInitializationTimeout, - arbitraryInitializationTimeoutFuzz); - - // The NullProvider should enter the enabled state, but have schedule an immediate runnable - // to switch to perm failure. - ProviderState currentState = provider.getCurrentState(); - assertSame(provider, currentState.provider); - assertEquals(PROVIDER_STATE_STARTED_INITIALIZING, currentState.stateEnum); - assertEquals(config, currentState.currentUserConfiguration); - mTestThreadingDomain.assertNextQueueItemIsImmediate(); - // Entering enabled() does not trigger an onProviderStateChanged() as it is requested by the - // controller. - mTestController.assertProviderChangeNotTriggered(); - - // Check the queued runnable causes the provider to go into perm failed state. - mTestThreadingDomain.executeNext(); - - // Entering perm failed triggers an onProviderStateChanged() as it is asynchronously - // triggered. - mTestController.assertProviderChangeTriggered(PROVIDER_STATE_PERM_FAILED); - } - - /** A test stand-in for the {@link LocationTimeZoneProviderController}. */ - private static class TestController extends LocationTimeZoneProviderController { - - private TestState mProviderState = new TestState<>(); - - TestController(ThreadingDomain threadingDomain) { - super(threadingDomain); - } - - @Override - void initialize(Environment environment, Callback callback) { - // Not needed for provider testing. - } - - @Override - void onConfigChanged() { - // Not needed for provider testing. - } - - @Override - boolean isUncertaintyTimeoutSet() { - // Not needed for provider testing. - return false; - } - - @Override - long getUncertaintyTimeoutDelayMillis() { - // Not needed for provider testing. - return 0; - } - - void onProviderStateChange(ProviderState providerState) { - this.mProviderState.set(providerState); - } - - @Override - public void dump(IndentingPrintWriter pw, String[] args) { - // Not needed for provider testing. - } - - void assertProviderChangeTriggered(int expectedStateEnum) { - assertEquals(expectedStateEnum, mProviderState.getLatest().stateEnum); - mProviderState.commitLatest(); - } - - public void assertProviderChangeNotTriggered() { - mProviderState.assertHasNotBeenSet(); - } - } -} diff --git a/services/tests/servicestests/src/com/android/server/location/timezone/TestThreadingDomain.java b/services/tests/servicestests/src/com/android/server/location/timezone/TestThreadingDomain.java index 7359abd11c3a3..b1a5ff9b549cd 100644 --- a/services/tests/servicestests/src/com/android/server/location/timezone/TestThreadingDomain.java +++ b/services/tests/servicestests/src/com/android/server/location/timezone/TestThreadingDomain.java @@ -26,6 +26,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Comparator; import java.util.Objects; +import java.util.concurrent.Callable; /** * A ThreadingDomain that simulates idealized post() semantics. Execution takes place in zero time, @@ -78,6 +79,11 @@ class TestThreadingDomain extends ThreadingDomain { postDelayed(r, null, 0); } + @Override + V postAndWait(Callable callable, long durationMillis) { + throw new UnsupportedOperationException("Not implemented"); + } + @Override void postDelayed(Runnable r, long delayMillis) { postDelayed(r, null, delayMillis); @@ -94,6 +100,10 @@ class TestThreadingDomain extends ThreadingDomain { mQueue.removeIf(runnable -> runnable.token != null && runnable.token == token); } + void removeAllQueuedRunnables() { + mQueue.clear(); + } + void assertSingleDelayedQueueItem(Duration expectedDelay) { assertQueueLength(1); assertNextQueueItemIsDelayed(expectedDelay);