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 e28d73e8e0b98..5dc4318e58036 100644 --- a/services/core/java/com/android/server/location/timezone/BinderLocationTimeZoneProvider.java +++ b/services/core/java/com/android/server/location/timezone/BinderLocationTimeZoneProvider.java @@ -18,7 +18,9 @@ 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_DISABLED; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED_CERTAIN; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED_INITIALIZING; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED_UNCERTAIN; import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED; import android.annotation.NonNull; @@ -78,16 +80,19 @@ class BinderLocationTimeZoneProvider extends LocationTimeZoneProvider { synchronized (mSharedLock) { ProviderState currentState = mCurrentState.get(); switch (currentState.stateEnum) { - case PROVIDER_STATE_ENABLED: { + case PROVIDER_STATE_ENABLED_INITIALIZING: + case PROVIDER_STATE_ENABLED_UNCERTAIN: + case PROVIDER_STATE_ENABLED_CERTAIN: { // Losing a remote provider is treated as becoming uncertain. String msg = "handleProviderLost reason=" + reason + ", mProviderName=" + mProviderName + ", currentState=" + currentState; debugLog(msg); - // This is an unusual PROVIDER_STATE_ENABLED state because event == null + // This is an unusual PROVIDER_STATE_ENABLED_UNCERTAIN state because + // event == null ProviderState newState = currentState.newState( - PROVIDER_STATE_ENABLED, null, currentState.currentUserConfiguration, - msg); + PROVIDER_STATE_ENABLED_UNCERTAIN, null, + currentState.currentUserConfiguration, msg); setCurrentState(newState, true); break; } @@ -118,7 +123,9 @@ class BinderLocationTimeZoneProvider extends LocationTimeZoneProvider { synchronized (mSharedLock) { ProviderState currentState = mCurrentState.get(); switch (currentState.stateEnum) { - case PROVIDER_STATE_ENABLED: { + case PROVIDER_STATE_ENABLED_INITIALIZING: + case PROVIDER_STATE_ENABLED_CERTAIN: + case PROVIDER_STATE_ENABLED_UNCERTAIN: { debugLog("handleOnProviderBound mProviderName=" + mProviderName + ", currentState=" + currentState + ": Provider is enabled."); break; 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 bedaedacf0e3d..179ce750d1068 100644 --- a/services/core/java/com/android/server/location/timezone/ControllerImpl.java +++ b/services/core/java/com/android/server/location/timezone/ControllerImpl.java @@ -24,7 +24,9 @@ import static com.android.server.location.timezone.LocationTimeZoneManagerServic 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_DISABLED; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED_CERTAIN; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED_INITIALIZING; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED_UNCERTAIN; import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED; import android.annotation.NonNull; @@ -38,6 +40,7 @@ import com.android.server.timezonedetector.ConfigurationInternal; import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion; import java.time.Duration; +import java.util.List; import java.util.Objects; /** @@ -49,8 +52,7 @@ import java.util.Objects; */ class ControllerImpl extends LocationTimeZoneProviderController { - @NonNull private final LocationTimeZoneProvider mProvider; - @NonNull private final SingleRunnableQueue mDelayedSuggestionQueue; + @NonNull private final LocationTimeZoneProvider mPrimaryProvider; @GuardedBy("mSharedLock") // Non-null after initialize() @@ -65,12 +67,9 @@ class ControllerImpl extends LocationTimeZoneProviderController { private Callback mCallback; /** - * Contains any currently pending suggestion on {@link #mDelayedSuggestionQueue}, if there is - * one. + * Used for scheduling uncertainty timeouts, i.e after the provider has reported uncertainty. */ - @GuardedBy("mSharedLock") - @Nullable - private GeolocationTimeZoneSuggestion mPendingSuggestion; + @NonNull private final SingleRunnableQueue mUncertaintyTimeoutQueue; /** Contains the last suggestion actually made, if there is one. */ @GuardedBy("mSharedLock") @@ -78,10 +77,10 @@ class ControllerImpl extends LocationTimeZoneProviderController { private GeolocationTimeZoneSuggestion mLastSuggestion; ControllerImpl(@NonNull ThreadingDomain threadingDomain, - @NonNull LocationTimeZoneProvider provider) { + @NonNull LocationTimeZoneProvider primaryProvider) { super(threadingDomain); - mDelayedSuggestionQueue = threadingDomain.createSingleRunnableQueue(); - mProvider = Objects.requireNonNull(provider); + mUncertaintyTimeoutQueue = threadingDomain.createSingleRunnableQueue(); + mPrimaryProvider = Objects.requireNonNull(primaryProvider); } @Override @@ -94,8 +93,12 @@ class ControllerImpl extends LocationTimeZoneProviderController { mCallback = Objects.requireNonNull(callback); mCurrentUserConfiguration = environment.getCurrentUserConfigurationInternal(); - mProvider.initialize(ControllerImpl.this::onProviderStateChange); - enableOrDisableProvider(mCurrentUserConfiguration); + LocationTimeZoneProvider.ProviderListener providerListener = + ControllerImpl.this::onProviderStateChange; + mPrimaryProvider.initialize(providerListener); + + alterProviderEnabledStateIfRequired( + null /* oldConfiguration */, mCurrentUserConfiguration); } } @@ -115,92 +118,148 @@ class ControllerImpl extends LocationTimeZoneProviderController { // If the user changed, disable the provider if needed. It may be re-enabled for // the new user below if their settings allow. debugLog("User changed. old=" + oldConfig.getUserId() - + ", new=" + newConfig.getUserId()); - debugLog("Disabling LocationTimeZoneProviders as needed"); - if (mProvider.getCurrentState().stateEnum == PROVIDER_STATE_ENABLED) { - mProvider.disable(); - } - } + + ", new=" + newConfig.getUserId() + ": Disabling provider"); + disableProvider(); - enableOrDisableProvider(newConfig); + alterProviderEnabledStateIfRequired(null /* oldConfiguration */, newConfig); + } else { + alterProviderEnabledStateIfRequired(oldConfig, newConfig); + } } } } + @Override + boolean isUncertaintyTimeoutSet() { + return mUncertaintyTimeoutQueue.hasQueued(); + } + + @Override + long getUncertaintyTimeoutDelayMillis() { + return mUncertaintyTimeoutQueue.getQueuedDelayMillis(); + } + @GuardedBy("mSharedLock") - private void enableOrDisableProvider(@NonNull ConfigurationInternal configuration) { - ProviderState providerState = mProvider.getCurrentState(); - boolean geoDetectionEnabled = configuration.getGeoDetectionEnabledBehavior(); - boolean providerWasEnabled = providerState.stateEnum == PROVIDER_STATE_ENABLED; - if (geoDetectionEnabled) { - switch (providerState.stateEnum) { - case PROVIDER_STATE_DISABLED: { - debugLog("Enabling " + mProvider); - mProvider.enable( - configuration, mEnvironment.getProviderInitializationTimeout()); - break; - } - case PROVIDER_STATE_ENABLED: { - debugLog("No need to enable " + mProvider + ": already enabled"); - break; - } - case PROVIDER_STATE_PERM_FAILED: { - debugLog("Unable to enable " + mProvider + ": it is perm failed"); - break; - } - default: - warnLog("Unknown provider state: " + mProvider); - break; + private void disableProvider() { + disableProviderIfEnabled(mPrimaryProvider); + + // By definition, if the provider is disabled, the controller is uncertain. + cancelUncertaintyTimeout(); + } + + @GuardedBy("mSharedLock") + private void disableProviderIfEnabled(LocationTimeZoneProvider provider) { + if (provider.getCurrentState().isEnabled()) { + disableProvider(provider); + } + } + + @GuardedBy("mSharedLock") + private void disableProvider(LocationTimeZoneProvider provider) { + ProviderState providerState = provider.getCurrentState(); + switch (providerState.stateEnum) { + case PROVIDER_STATE_DISABLED: { + debugLog("No need to disable " + provider + ": already disabled"); + break; } - } else { - switch (providerState.stateEnum) { - case PROVIDER_STATE_DISABLED: { - debugLog("No need to disable " + mProvider + ": already enabled"); - break; - } - case PROVIDER_STATE_ENABLED: { - debugLog("Disabling " + mProvider); - mProvider.disable(); - break; - } - case PROVIDER_STATE_PERM_FAILED: { - debugLog("Unable to disable " + mProvider + ": it is perm failed"); - break; - } - default: { - warnLog("Unknown provider state: " + mProvider); - break; - } + case PROVIDER_STATE_ENABLED_INITIALIZING: + case PROVIDER_STATE_ENABLED_CERTAIN: + case PROVIDER_STATE_ENABLED_UNCERTAIN: { + debugLog("Disabling " + provider); + provider.disable(); + break; + } + case PROVIDER_STATE_PERM_FAILED: { + debugLog("Unable to disable " + provider + ": it is perm failed"); + break; + } + default: { + warnLog("Unknown provider state: " + provider); + break; } } + } - boolean isProviderEnabled = - mProvider.getCurrentState().stateEnum == PROVIDER_STATE_ENABLED; + /** + * Sets the provider into the correct enabled/disabled state for the {@code newConfiguration} + * and, if there is a provider state change, makes any suggestions required to inform the + * downstream time zone detection code. + * + *

This is a utility method that exists to avoid duplicated logic for the various cases when + * provider enabled / disabled state may need to be set or changed, e.g. during initialization + * or when a new configuration has been received. + */ + @GuardedBy("mSharedLock") + private void alterProviderEnabledStateIfRequired( + @Nullable ConfigurationInternal oldConfiguration, + @NonNull ConfigurationInternal newConfiguration) { - if (isProviderEnabled) { - if (!providerWasEnabled) { - // When a provider has first been enabled, we allow it some time for it to - // initialize before sending its first event. - Duration initializationTimeout = mEnvironment.getProviderInitializationTimeout() - .plus(mEnvironment.getProviderInitializationTimeoutFuzz()); - // This sets up an empty suggestion to trigger if no explicit "certain" or - // "uncertain" suggestion preempts it within initializationTimeout. If, for some - // reason, the provider does not produce any events then this scheduled suggestion - // will ensure the controller makes at least an "uncertain" suggestion. - suggestDelayed(createEmptySuggestion("No event received from provider in" - + " initializationTimeout=" + initializationTimeout), - initializationTimeout); + // Provider enabled / disabled states only need to be changed if geoDetectionEnabled has + // changed. + boolean oldGeoDetectionEnabled = oldConfiguration != null + && oldConfiguration.getGeoDetectionEnabledBehavior(); + boolean newGeoDetectionEnabled = newConfiguration.getGeoDetectionEnabledBehavior(); + if (oldGeoDetectionEnabled == newGeoDetectionEnabled) { + return; + } + + if (newGeoDetectionEnabled) { + // Try to enable the primary provider. + tryEnableProvider(mPrimaryProvider, newConfiguration); + + ProviderState newPrimaryState = mPrimaryProvider.getCurrentState(); + if (!newPrimaryState.isEnabled()) { + // If the provider is perm failed then the controller is immediately considered + // uncertain. + GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion( + "Provider is failed:" + + " primary=" + mPrimaryProvider.getCurrentState()); + makeSuggestion(suggestion); } } else { - // Clear any queued suggestions. - clearDelayedSuggestion(); + disableProvider(); - // If the provider is now not enabled, and a previous "certain" suggestion has been - // made, then a new "uncertain" suggestion must be made to indicate the provider no - // longer has an opinion and will not be sending updates. + // There can be an uncertainty timeout set if the controller most recently received + // an uncertain event. This is a no-op if there isn't a timeout set. + cancelUncertaintyTimeout(); + + // If a previous "certain" suggestion has been made, then a new "uncertain" + // suggestion must now be made to indicate the controller {does not / no longer has} + // an opinion and will not be sending further updates (until at least the config + // changes again and providers are re-enabled). if (mLastSuggestion != null && mLastSuggestion.getZoneIds() != null) { - suggestImmediate(createEmptySuggestion( - "Provider disabled, clearing previous suggestion")); + GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion( + "Provider is disabled:" + + " primary=" + mPrimaryProvider.getCurrentState()); + makeSuggestion(suggestion); + } + } + } + + private void tryEnableProvider(@NonNull LocationTimeZoneProvider provider, + @NonNull ConfigurationInternal configuration) { + ProviderState providerState = provider.getCurrentState(); + switch (providerState.stateEnum) { + case PROVIDER_STATE_DISABLED: { + debugLog("Enabling " + provider); + provider.enable(configuration, mEnvironment.getProviderInitializationTimeout(), + mEnvironment.getProviderInitializationTimeoutFuzz()); + break; + } + case PROVIDER_STATE_ENABLED_INITIALIZING: + case PROVIDER_STATE_ENABLED_CERTAIN: + case PROVIDER_STATE_ENABLED_UNCERTAIN: { + debugLog("No need to enable " + provider + ": already enabled"); + break; + } + case PROVIDER_STATE_PERM_FAILED: { + debugLog("Unable to enable " + provider + ": it is perm failed"); + break; + } + default: { + throw new IllegalStateException("Unknown provider state:" + + " provider=" + provider + + ", state=" + providerState.stateEnum); } } } @@ -217,7 +276,9 @@ class ControllerImpl extends LocationTimeZoneProviderController { + " providerState=" + providerState); break; } - case PROVIDER_STATE_ENABLED: { + case PROVIDER_STATE_ENABLED_INITIALIZING: + case PROVIDER_STATE_ENABLED_CERTAIN: + case PROVIDER_STATE_ENABLED_UNCERTAIN: { // Entering enabled does not trigger an event, so this only happens if an event // is received while the provider is enabled. debugLog("onProviderStateChange: Received notification of an event while" @@ -228,10 +289,7 @@ class ControllerImpl extends LocationTimeZoneProviderController { case PROVIDER_STATE_PERM_FAILED: { debugLog("Received notification of permanent failure for" + " provider=" + providerState); - GeolocationTimeZoneSuggestion suggestion = createEmptySuggestion( - "provider=" + providerState.provider - + " permanently failed: " + providerState); - suggestImmediate(suggestion); + providerFailedProcessEvent(); break; } default: { @@ -242,24 +300,46 @@ class ControllerImpl extends LocationTimeZoneProviderController { } private void assertProviderKnown(LocationTimeZoneProvider provider) { - if (provider != mProvider) { + if (provider != mPrimaryProvider) { throw new IllegalArgumentException("Unknown provider: " + provider); } } /** - * Called when a provider has changed state but just moved from a PROVIDER_STATE_ENABLED state - * to another PROVIDER_STATE_ENABLED state, usually as a result of a new {@link - * LocationTimeZoneEvent} being received. There are some cases where event can be null. + * Called when the provider has reported that it has failed permanently. */ + @GuardedBy("mSharedLock") + private void providerFailedProcessEvent() { + // If the provider is newly perm failed then the controller is uncertain by + // definition. + cancelUncertaintyTimeout(); + + // If the provider is now failed, then we must send a suggestion informing the time + // zone detector that there are no further updates coming in future. + + GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion( + "The provider is permanently failed:" + + " primary=" + mPrimaryProvider.getCurrentState()); + makeSuggestion(suggestion); + } + + /** + * Called when a provider has changed state but just moved from one enabled state to another + * enabled state, usually as a result of a new {@link LocationTimeZoneEvent} being received. + * However, there are rare cases where the event can be null. + */ + @GuardedBy("mSharedLock") private void providerEnabledProcessEvent(@NonNull ProviderState providerState) { + LocationTimeZoneProvider provider = providerState.provider; LocationTimeZoneEvent event = providerState.event; if (event == null) { // Implicit uncertainty, i.e. where the provider is enabled, but a problem has been // detected without having received an event. For example, if the process has detected - // the loss of a binder-based provider. This is treated like explicit uncertainty, i.e. - // where the provider has explicitly told this process it is uncertain. - scheduleUncertainSuggestionIfNeeded(null); + // the loss of a binder-based provider, or initialization took too long. This is treated + // the same as explicit uncertainty, i.e. where the provider has explicitly told this + // process it is uncertain. + handleProviderUncertainty(provider, "provider=" + provider + + ", implicit uncertainty, event=null"); return; } @@ -279,22 +359,19 @@ class ControllerImpl extends LocationTimeZoneProviderController { switch (event.getEventType()) { case EVENT_TYPE_PERMANENT_FAILURE: { - // This shouldn't happen. Providers cannot be enabled and have this event. + // This shouldn't happen. A provider cannot be enabled and have this event. warnLog("Provider=" + providerState + " is enabled, but event suggests it shouldn't be"); break; } case EVENT_TYPE_UNCERTAIN: { - scheduleUncertainSuggestionIfNeeded(event); + handleProviderUncertainty(provider, "provider=" + provider + + ", explicit uncertainty. event=" + event); break; } case EVENT_TYPE_SUCCESS: { - GeolocationTimeZoneSuggestion suggestion = - new GeolocationTimeZoneSuggestion(event.getTimeZoneIds()); - suggestion.addDebugInfo("Event received provider=" + mProvider.getName() - + ", event=" + event); - // Rely on the receiver to dedupe events. It is better to over-communicate. - suggestImmediate(suggestion); + handleProviderCertainty(provider, event.getTimeZoneIds(), + "Event received provider=" + provider.getName() + ", event=" + event); break; } default: { @@ -304,30 +381,19 @@ class ControllerImpl extends LocationTimeZoneProviderController { } } - /** - * Indicates a provider has become uncertain with the event (if any) received that indicates - * that. - * - *

Providers are expected to report their uncertainty as soon as they become uncertain, as - * this enables the most flexibility for the controller to enable other providers when there are - * multiple ones available. The controller is therefore responsible for deciding when to make a - * "uncertain" suggestion. - * - *

This method schedules an "uncertain" suggestion (if one isn't already scheduled) to be - * made later if nothing else preempts it. It can be preempted if the provider becomes certain - * (or does anything else that calls {@link #suggestImmediate(GeolocationTimeZoneSuggestion)}) - * within {@link Environment#getUncertaintyDelay()}. Preemption causes the scheduled - * "uncertain" event to be cancelled. If the provider repeatedly sends uncertainty events within - * the uncertainty delay period, those events are effectively ignored (i.e. the timer is not - * reset each time). - */ - private void scheduleUncertainSuggestionIfNeeded(@Nullable LocationTimeZoneEvent event) { - if (mPendingSuggestion == null || mPendingSuggestion.getZoneIds() != null) { - GeolocationTimeZoneSuggestion suggestion = createEmptySuggestion( - "provider=" + mProvider + " became uncertain, event=" + event); - // Only send the empty suggestion after the uncertainty delay. - suggestDelayed(suggestion, mEnvironment.getUncertaintyDelay()); - } + @GuardedBy("mSharedLock") + private void handleProviderCertainty( + @NonNull LocationTimeZoneProvider provider, + @Nullable List timeZoneIds, + @NonNull String reason) { + // By definition, the controller is now certain. + cancelUncertaintyTimeout(); + + GeolocationTimeZoneSuggestion suggestion = + new GeolocationTimeZoneSuggestion(timeZoneIds); + suggestion.addDebugInfo(reason); + // Rely on the receiver to dedupe events. It is better to over-communicate. + makeSuggestion(suggestion); } @Override @@ -342,66 +408,74 @@ class ControllerImpl extends LocationTimeZoneProviderController { ipw.println("providerInitializationTimeoutFuzz=" + mEnvironment.getProviderInitializationTimeoutFuzz()); ipw.println("uncertaintyDelay=" + mEnvironment.getUncertaintyDelay()); - ipw.println("mPendingSuggestion=" + mPendingSuggestion); ipw.println("mLastSuggestion=" + mLastSuggestion); - ipw.println("Provider:"); + ipw.println("Primary Provider:"); ipw.increaseIndent(); // level 2 - mProvider.dump(ipw, args); + mPrimaryProvider.dump(ipw, args); ipw.decreaseIndent(); // level 2 ipw.decreaseIndent(); // level 1 } } - /** Sends an immediate suggestion, cancelling any pending suggestion. */ + /** Sends an immediate suggestion, updating mLastSuggestion. */ @GuardedBy("mSharedLock") - private void suggestImmediate(@NonNull GeolocationTimeZoneSuggestion suggestion) { - debugLog("suggestImmediate: Executing suggestion=" + suggestion); - mDelayedSuggestionQueue.runSynchronously(() -> mCallback.suggest(suggestion)); - mPendingSuggestion = null; + private void makeSuggestion(@NonNull GeolocationTimeZoneSuggestion suggestion) { + debugLog("makeSuggestion: suggestion=" + suggestion); + mCallback.suggest(suggestion); mLastSuggestion = suggestion; } - /** Clears any pending suggestion. */ + /** Clears the uncertainty timeout. */ @GuardedBy("mSharedLock") - private void clearDelayedSuggestion() { - mDelayedSuggestionQueue.cancel(); - mPendingSuggestion = null; + private void cancelUncertaintyTimeout() { + mUncertaintyTimeoutQueue.cancel(); } - /** - * Schedules a delayed suggestion. There can only be one delayed suggestion at a time. - * If there is a pending scheduled suggestion equal to the one passed, it will not be replaced. - * Replacing a previous delayed suggestion has the effect of cancelling the timeout associated - * with that previous suggestion. + * Indicates a provider has become uncertain with the event (if any) received that indicates + * that. + * + *

A provider is expected to report its uncertainty as soon as it becomes uncertain, as + * this enables the most flexibility for the controller to enable other providers when there are + * multiple ones available. The controller is therefore responsible for deciding when to make a + * "uncertain" suggestion. + * + *

This method schedules an "uncertain" suggestion (if one isn't already scheduled) to be + * made later if nothing else preempts it. It can be preempted if the provider becomes certain + * (or does anything else that calls {@link #makeSuggestion(GeolocationTimeZoneSuggestion)}) + * within {@link Environment#getUncertaintyDelay()}. Preemption causes the scheduled + * "uncertain" event to be cancelled. If the provider repeatedly sends uncertainty events within + * the uncertainty delay period, those events are effectively ignored (i.e. the timer is not + * reset each time). */ @GuardedBy("mSharedLock") - private void suggestDelayed(@NonNull GeolocationTimeZoneSuggestion suggestion, - @NonNull Duration delay) { - Objects.requireNonNull(suggestion); - Objects.requireNonNull(delay); + void handleProviderUncertainty(@NonNull LocationTimeZoneProvider provider, String reason) { + Objects.requireNonNull(provider); - if (Objects.equals(mPendingSuggestion, suggestion)) { - // Do not reset the timer. - debugLog("suggestDelayed: Suggestion=" + suggestion + " is equal to existing." - + " Not scheduled."); - return; + // Start the uncertainty timeout if needed. + if (!mUncertaintyTimeoutQueue.hasQueued()) { + debugLog("Starting uncertainty timeout: reason=" + reason); + + Duration delay = mEnvironment.getUncertaintyDelay(); + mUncertaintyTimeoutQueue.runDelayed( + this::onProviderUncertaintyTimeout, delay.toMillis()); } - - debugLog("suggestDelayed: Scheduling suggestion=" + suggestion); - mPendingSuggestion = suggestion; - - mDelayedSuggestionQueue.runDelayed(() -> { - debugLog("suggestDelayed: Executing suggestion=" + suggestion); - mCallback.suggest(suggestion); - mPendingSuggestion = null; - mLastSuggestion = suggestion; - }, delay.toMillis()); } - private static GeolocationTimeZoneSuggestion createEmptySuggestion(String reason) { + private void onProviderUncertaintyTimeout() { + mThreadingDomain.assertCurrentThread(); + + synchronized (mSharedLock) { + GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion( + "Uncertainty timeout triggered:" + + " primary=" + mPrimaryProvider.getCurrentState()); + makeSuggestion(suggestion); + } + } + + private static GeolocationTimeZoneSuggestion createUncertainSuggestion(String reason) { GeolocationTimeZoneSuggestion suggestion = new GeolocationTimeZoneSuggestion(null); suggestion.addDebugInfo(reason); return suggestion; @@ -412,17 +486,22 @@ class ControllerImpl extends LocationTimeZoneProviderController { * If the provider name does not match a known provider, then the event is logged and discarded. */ void simulateBinderProviderEvent(SimulatedBinderProviderEvent event) { - if (!Objects.equals(mProvider.getName(), event.getProviderName())) { + String targetProviderName = event.getProviderName(); + LocationTimeZoneProvider targetProvider; + if (Objects.equals(mPrimaryProvider.getName(), targetProviderName)) { + targetProvider = mPrimaryProvider; + } else { warnLog("Unable to process simulated binder provider event," + " unknown providerName in event=" + event); return; } - if (!(mProvider instanceof BinderLocationTimeZoneProvider)) { + if (!(targetProvider instanceof BinderLocationTimeZoneProvider)) { warnLog("Unable to process simulated binder provider event," - + " provider is not a " + BinderLocationTimeZoneProvider.class + + " provider=" + targetProvider + + " is not a " + BinderLocationTimeZoneProvider.class + ", event=" + event); return; } - ((BinderLocationTimeZoneProvider) mProvider).simulateBinderProviderEvent(event); + ((BinderLocationTimeZoneProvider) targetProvider).simulateBinderProviderEvent(event); } } 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 238f999ff8a67..c9a211d96fc05 100644 --- a/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerService.java +++ b/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerService.java @@ -160,7 +160,8 @@ public class LocationTimeZoneManagerService extends Binder { // Called on an arbitrary thread during initialization. synchronized (mSharedLock) { LocationTimeZoneProvider primary = createPrimaryProvider(); - mLocationTimeZoneDetectorController = new ControllerImpl(mThreadingDomain, primary); + mLocationTimeZoneDetectorController = + new ControllerImpl(mThreadingDomain, primary); ControllerCallbackImpl callback = new ControllerCallbackImpl(mThreadingDomain); ControllerEnvironmentImpl environment = new ControllerEnvironmentImpl( mThreadingDomain, mLocationTimeZoneDetectorController); 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 abfa580b5c980..4b0b5a2cbbe43 100644 --- a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProvider.java +++ b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProvider.java @@ -22,7 +22,9 @@ import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_UNCERTA import static com.android.server.location.timezone.LocationTimeZoneManagerService.debugLog; import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DISABLED; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED_CERTAIN; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED_INITIALIZING; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED_UNCERTAIN; import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED; import android.annotation.IntDef; @@ -33,6 +35,9 @@ import android.os.Handler; import android.os.SystemClock; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.ProviderStateEnum; +import com.android.server.location.timezone.ThreadingDomain.SingleRunnableQueue; import com.android.server.timezonedetector.ConfigurationInternal; import com.android.server.timezonedetector.Dumpable; import com.android.server.timezonedetector.ReferenceWithHistory; @@ -73,8 +78,9 @@ abstract class LocationTimeZoneProvider implements Dumpable { */ static class ProviderState { - @IntDef({ PROVIDER_STATE_UNKNOWN, PROVIDER_STATE_ENABLED, PROVIDER_STATE_DISABLED, - PROVIDER_STATE_PERM_FAILED }) + @IntDef({ PROVIDER_STATE_UNKNOWN, PROVIDER_STATE_ENABLED_INITIALIZING, + PROVIDER_STATE_ENABLED_CERTAIN, PROVIDER_STATE_ENABLED_UNCERTAIN, + PROVIDER_STATE_DISABLED, PROVIDER_STATE_PERM_FAILED }) @interface ProviderStateEnum {} /** @@ -83,22 +89,33 @@ abstract class LocationTimeZoneProvider implements Dumpable { static final int PROVIDER_STATE_UNKNOWN = 0; /** - * The provider is currently enabled. + * The provider is enabled and has not reported its first event. */ - static final int PROVIDER_STATE_ENABLED = 1; + static final int PROVIDER_STATE_ENABLED_INITIALIZING = 1; /** - * The provider is currently disabled. + * The provider is enabled and most recently reported a "success" event. + */ + static final int PROVIDER_STATE_ENABLED_CERTAIN = 2; + + /** + * The provider is enabled and most recently reported an "uncertain" event. + */ + static final int PROVIDER_STATE_ENABLED_UNCERTAIN = 3; + + /** + * The provider is disabled. + * * This is the state after {@link #initialize} is called. */ - static final int PROVIDER_STATE_DISABLED = 2; + static final int PROVIDER_STATE_DISABLED = 4; /** * The provider has failed and cannot be re-enabled. * * Providers may enter this state after a provider is enabled. */ - static final int PROVIDER_STATE_PERM_FAILED = 3; + static final int PROVIDER_STATE_PERM_FAILED = 5; /** The {@link LocationTimeZoneProvider} the state is for. */ public final @NonNull LocationTimeZoneProvider provider; @@ -108,14 +125,15 @@ abstract class LocationTimeZoneProvider implements Dumpable { /** * The last {@link LocationTimeZoneEvent} received. Only populated when {@link #stateEnum} - * is {@link #PROVIDER_STATE_ENABLED}, but it can be {@code null} then too if no event has + * is either {@link #PROVIDER_STATE_ENABLED_CERTAIN} or {@link + * #PROVIDER_STATE_ENABLED_UNCERTAIN}, but it can be {@code null} then too if no event has * yet been received. */ @Nullable public final LocationTimeZoneEvent event; /** * The user configuration associated with the current state. Only and always present when - * {@link #stateEnum} is {@link #PROVIDER_STATE_ENABLED}. + * {@link #stateEnum} is one of the enabled states. */ @Nullable public final ConfigurationInternal currentUserConfiguration; @@ -133,7 +151,8 @@ abstract class LocationTimeZoneProvider implements Dumpable { private ProviderState(@NonNull LocationTimeZoneProvider provider, - @ProviderStateEnum int stateEnum, @Nullable LocationTimeZoneEvent event, + @ProviderStateEnum int stateEnum, + @Nullable LocationTimeZoneEvent event, @Nullable ConfigurationInternal currentUserConfiguration, @Nullable String debugInfo) { this.provider = Objects.requireNonNull(provider); @@ -172,7 +191,9 @@ abstract class LocationTimeZoneProvider implements Dumpable { break; } case PROVIDER_STATE_DISABLED: - case PROVIDER_STATE_ENABLED: { + case PROVIDER_STATE_ENABLED_INITIALIZING: + case PROVIDER_STATE_ENABLED_CERTAIN: + case PROVIDER_STATE_ENABLED_UNCERTAIN: { // These can go to each other or PROVIDER_STATE_PERM_FAILED. break; } @@ -200,7 +221,9 @@ abstract class LocationTimeZoneProvider implements Dumpable { } break; } - case PROVIDER_STATE_ENABLED: { + case PROVIDER_STATE_ENABLED_INITIALIZING: + case PROVIDER_STATE_ENABLED_CERTAIN: + case PROVIDER_STATE_ENABLED_UNCERTAIN: { if (currentUserConfig == null) { throw new IllegalArgumentException( "Enabled state: currentUserConfig must not be null"); @@ -223,6 +246,13 @@ abstract class LocationTimeZoneProvider implements Dumpable { return new ProviderState(provider, newStateEnum, event, currentUserConfig, debugInfo); } + /** Returns {@code true} if {@link #stateEnum} is one of the enabled states. */ + boolean isEnabled() { + return stateEnum == PROVIDER_STATE_ENABLED_INITIALIZING + || stateEnum == PROVIDER_STATE_ENABLED_CERTAIN + || stateEnum == PROVIDER_STATE_ENABLED_UNCERTAIN; + } + @Override public String toString() { return "State{" @@ -257,8 +287,12 @@ abstract class LocationTimeZoneProvider implements Dumpable { switch (state) { case PROVIDER_STATE_DISABLED: return "Disabled (" + PROVIDER_STATE_DISABLED + ")"; - case PROVIDER_STATE_ENABLED: - return "Enabled (" + PROVIDER_STATE_ENABLED + ")"; + case PROVIDER_STATE_ENABLED_INITIALIZING: + return "Enabled initializing (" + PROVIDER_STATE_ENABLED_INITIALIZING + ")"; + case PROVIDER_STATE_ENABLED_CERTAIN: + return "Enabled certain (" + PROVIDER_STATE_ENABLED_CERTAIN + ")"; + case PROVIDER_STATE_ENABLED_UNCERTAIN: + return "Enabled uncertain (" + PROVIDER_STATE_ENABLED_UNCERTAIN + ")"; case PROVIDER_STATE_PERM_FAILED: return "Perm failure (" + PROVIDER_STATE_PERM_FAILED + ")"; case PROVIDER_STATE_UNKNOWN: @@ -279,6 +313,11 @@ abstract class LocationTimeZoneProvider implements Dumpable { final ReferenceWithHistory mCurrentState = new ReferenceWithHistory<>(10); + /** + * Used for scheduling initialization timeouts, i.e. for providers that have just been enabled. + */ + @NonNull private final SingleRunnableQueue mInitializationTimeoutQueue; + // Non-null and effectively final after initialize() is called. ProviderListener mProviderListener; @@ -286,6 +325,7 @@ abstract class LocationTimeZoneProvider implements Dumpable { LocationTimeZoneProvider(@NonNull ThreadingDomain threadingDomain, @NonNull String providerName) { mThreadingDomain = Objects.requireNonNull(threadingDomain); + mInitializationTimeoutQueue = threadingDomain.createSingleRunnableQueue(); mSharedLock = threadingDomain.getLockObject(); mProviderName = Objects.requireNonNull(providerName); } @@ -303,7 +343,8 @@ abstract class LocationTimeZoneProvider implements Dumpable { mProviderListener = Objects.requireNonNull(providerListener); ProviderState currentState = ProviderState.createStartingState(this); ProviderState newState = currentState.newState( - PROVIDER_STATE_DISABLED, null, null, "initialize() called"); + PROVIDER_STATE_DISABLED, null, null, + "initialize() called"); setCurrentState(newState, false); onInitialize(); @@ -370,20 +411,41 @@ abstract class LocationTimeZoneProvider implements Dumpable { * called using the handler thread from the {@link ThreadingDomain}. */ final void enable(@NonNull ConfigurationInternal currentUserConfiguration, - @NonNull Duration initializationTimeout) { + @NonNull Duration initializationTimeout, @NonNull Duration initializationTimeoutFuzz) { mThreadingDomain.assertCurrentThread(); synchronized (mSharedLock) { assertCurrentState(PROVIDER_STATE_DISABLED); - ProviderState currentState = getCurrentState(); + ProviderState currentState = mCurrentState.get(); ProviderState newState = currentState.newState( - PROVIDER_STATE_ENABLED, null, currentUserConfiguration, "enable() called"); + PROVIDER_STATE_ENABLED_INITIALIZING, null /* event */, + currentUserConfiguration, "enable() called"); setCurrentState(newState, false); + + Duration delay = initializationTimeout.plus(initializationTimeoutFuzz); + mInitializationTimeoutQueue.runDelayed( + this::handleInitializationTimeout, delay.toMillis()); + onEnable(initializationTimeout); } } + private void handleInitializationTimeout() { + mThreadingDomain.assertCurrentThread(); + + synchronized (mSharedLock) { + ProviderState currentState = mCurrentState.get(); + if (currentState.stateEnum == PROVIDER_STATE_ENABLED_INITIALIZING) { + // On initialization timeout the provider becomes uncertain. + ProviderState newState = currentState.newState( + PROVIDER_STATE_ENABLED_UNCERTAIN, null /* event */, + currentState.currentUserConfiguration, "initialization timeout"); + setCurrentState(newState, true); + } + } + } + /** * Implemented by subclasses to do work during {@link #enable}. */ @@ -391,20 +453,24 @@ abstract class LocationTimeZoneProvider implements Dumpable { /** * Disables the provider. It is an error* to call this method except when the {@link - * #getCurrentState()} is at {@link ProviderState#PROVIDER_STATE_ENABLED}. This method must be + * #getCurrentState()} is one of the enabled states. This method must be * called using the handler thread from the {@link ThreadingDomain}. */ final void disable() { mThreadingDomain.assertCurrentThread(); synchronized (mSharedLock) { - assertCurrentState(PROVIDER_STATE_ENABLED); + assertIsEnabled(); - ProviderState currentState = getCurrentState(); - ProviderState newState = - currentState.newState(PROVIDER_STATE_DISABLED, null, null, "disable() called"); + ProviderState currentState = mCurrentState.get(); + ProviderState newState = currentState.newState( + PROVIDER_STATE_DISABLED, null, null, "disable() called"); setCurrentState(newState, false); + if (mInitializationTimeoutQueue.hasQueued()) { + mInitializationTimeoutQueue.cancel(); + } + onDisable(); } } @@ -424,7 +490,7 @@ abstract class LocationTimeZoneProvider implements Dumpable { debugLog("handleLocationTimeZoneEvent: mProviderName=" + mProviderName + ", locationTimeZoneEvent=" + locationTimeZoneEvent); - ProviderState currentState = getCurrentState(); + ProviderState currentState = mCurrentState.get(); int eventType = locationTimeZoneEvent.getEventType(); switch (currentState.stateEnum) { case PROVIDER_STATE_PERM_FAILED: { @@ -445,6 +511,9 @@ abstract class LocationTimeZoneProvider implements Dumpable { ProviderState newState = currentState.newState( PROVIDER_STATE_PERM_FAILED, null, null, msg); setCurrentState(newState, true); + if (mInitializationTimeoutQueue.hasQueued()) { + mInitializationTimeoutQueue.cancel(); + } return; } case EVENT_TYPE_SUCCESS: @@ -464,7 +533,9 @@ abstract class LocationTimeZoneProvider implements Dumpable { } } } - case PROVIDER_STATE_ENABLED: { + case PROVIDER_STATE_ENABLED_INITIALIZING: + case PROVIDER_STATE_ENABLED_CERTAIN: + case PROVIDER_STATE_ENABLED_UNCERTAIN: { switch (eventType) { case EVENT_TYPE_PERMANENT_FAILURE: { String msg = "handleLocationTimeZoneEvent:" @@ -475,14 +546,27 @@ abstract class LocationTimeZoneProvider implements Dumpable { ProviderState newState = currentState.newState( PROVIDER_STATE_PERM_FAILED, null, null, msg); setCurrentState(newState, true); + if (mInitializationTimeoutQueue.hasQueued()) { + mInitializationTimeoutQueue.cancel(); + } + return; } case EVENT_TYPE_UNCERTAIN: case EVENT_TYPE_SUCCESS: { - ProviderState newState = currentState.newState(PROVIDER_STATE_ENABLED, + @ProviderStateEnum int providerStateEnum; + if (eventType == EVENT_TYPE_UNCERTAIN) { + providerStateEnum = PROVIDER_STATE_ENABLED_UNCERTAIN; + } else { + providerStateEnum = PROVIDER_STATE_ENABLED_CERTAIN; + } + ProviderState newState = currentState.newState(providerStateEnum, locationTimeZoneEvent, currentState.currentUserConfiguration, "handleLocationTimeZoneEvent() when enabled"); setCurrentState(newState, true); + if (mInitializationTimeoutQueue.hasQueued()) { + mInitializationTimeoutQueue.cancel(); + } return; } default: { @@ -503,11 +587,34 @@ abstract class LocationTimeZoneProvider implements Dumpable { */ abstract void logWarn(String msg); - private void assertCurrentState(@ProviderState.ProviderStateEnum int requiredState) { - ProviderState currentState = getCurrentState(); + @GuardedBy("mSharedLock") + private void assertIsEnabled() { + ProviderState currentState = mCurrentState.get(); + if (!currentState.isEnabled()) { + throw new IllegalStateException("Required an enabled state, but was " + currentState); + } + } + + @GuardedBy("mSharedLock") + private void assertCurrentState(@ProviderStateEnum int requiredState) { + ProviderState currentState = mCurrentState.get(); if (currentState.stateEnum != requiredState) { throw new IllegalStateException( "Required stateEnum=" + requiredState + ", but was " + currentState); } } + + @VisibleForTesting + boolean isInitializationTimeoutSet() { + synchronized (mSharedLock) { + return mInitializationTimeoutQueue.hasQueued(); + } + } + + @VisibleForTesting + Duration getInitializationTimeoutDelay() { + synchronized (mSharedLock) { + return Duration.ofMillis(mInitializationTimeoutQueue.getQueuedDelayMillis()); + } + } } 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 88f0f00015c6f..ace066e12d3f3 100644 --- a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderController.java +++ b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderController.java @@ -19,6 +19,7 @@ package com.android.server.location.timezone; import android.annotation.NonNull; import android.os.Handler; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState; import com.android.server.timezonedetector.ConfigurationInternal; import com.android.server.timezonedetector.Dumpable; @@ -85,6 +86,12 @@ abstract class LocationTimeZoneProviderController implements Dumpable { */ abstract void onConfigChanged(); + @VisibleForTesting + abstract boolean isUncertaintyTimeoutSet(); + + @VisibleForTesting + abstract long getUncertaintyTimeoutDelayMillis(); + /** * Used by {@link LocationTimeZoneProviderController} to obtain information from the surrounding * service. It can easily be faked for tests. 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 9b9c823589749..d55d3edf5e03a 100644 --- a/services/core/java/com/android/server/location/timezone/ThreadingDomain.java +++ b/services/core/java/com/android/server/location/timezone/ThreadingDomain.java @@ -83,21 +83,14 @@ abstract class ThreadingDomain { } /** - * A class that allows up to one {@link Runnable} to be queued on the handler, i.e. calling any - * of the methods will cancel the execution of any previously queued / delayed runnable. All + * A class that allows up to one {@link Runnable} to be queued, i.e. calling {@link + * #runDelayed(Runnable, long)} will cancel the execution of any previously queued runnable. All * methods must be called from the {@link ThreadingDomain}'s thread. */ final class SingleRunnableQueue { - /** - * Runs the supplied {@link Runnable} synchronously on the threading domain's thread, - * cancelling any queued but not-yet-executed {@link Runnable} previously added by this. - * This method must be called from the threading domain's thread. - */ - void runSynchronously(Runnable r) { - cancel(); - r.run(); - } + private boolean mIsQueued; + private long mDelayMillis; /** * Posts the supplied {@link Runnable} asynchronously and delayed on the threading domain @@ -106,15 +99,48 @@ abstract class ThreadingDomain { */ void runDelayed(Runnable r, long delayMillis) { cancel(); - ThreadingDomain.this.postDelayed(r, this, delayMillis); + mIsQueued = true; + mDelayMillis = delayMillis; + ThreadingDomain.this.postDelayed(() -> { + mIsQueued = false; + mDelayMillis = -2; + r.run(); + }, this, delayMillis); + } + + /** + * Returns {@code true} if there is an item current queued. This method must be called from + * the threading domain's thread. + */ + boolean hasQueued() { + assertCurrentThread(); + return mIsQueued; + } + + /** + * Returns the delay in milliseconds for the currently queued item. Throws {@link + * IllegalStateException} if nothing is currently queued, see {@link #hasQueued()}. + * This method must be called from the threading domain's thread. + */ + long getQueuedDelayMillis() { + assertCurrentThread(); + if (!mIsQueued) { + throw new IllegalStateException("No item queued"); + } + return mDelayMillis; } /** * Cancels any queued but not-yet-executed {@link Runnable} previously added by this. + * This method must be called from the threading domain's thread. */ public void cancel() { assertCurrentThread(); - removeQueuedRunnables(this); + if (mIsQueued) { + removeQueuedRunnables(this); + } + mIsQueued = false; + mDelayMillis = -1; } } } 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 631b8d3cb1ea0..9ee9259a23c24 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 @@ -15,17 +15,22 @@ */ package com.android.server.location.timezone; +import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_PERMANENT_FAILURE; import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_SUCCESS; import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_UNCERTAIN; import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DISABLED; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED_CERTAIN; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED_INITIALIZING; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED_UNCERTAIN; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED; import static com.android.server.location.timezone.TestSupport.USER1_CONFIG_GEO_DETECTION_DISABLED; import static com.android.server.location.timezone.TestSupport.USER1_CONFIG_GEO_DETECTION_ENABLED; import static com.android.server.location.timezone.TestSupport.USER1_ID; import static com.android.server.location.timezone.TestSupport.USER2_CONFIG_GEO_DETECTION_ENABLED; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -39,6 +44,7 @@ import android.os.UserHandle; import android.platform.test.annotations.Presubmit; import android.util.IndentingPrintWriter; +import com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.ProviderStateEnum; import com.android.server.timezonedetector.ConfigurationInternal; import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion; import com.android.server.timezonedetector.TestState; @@ -65,10 +71,12 @@ public class ControllerImplTest { createLocationTimeZoneEvent(USER1_ID, EVENT_TYPE_SUCCESS, asList("Europe/Paris")); private static final LocationTimeZoneEvent USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT = createLocationTimeZoneEvent(USER1_ID, EVENT_TYPE_UNCERTAIN, null); + private static final LocationTimeZoneEvent USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT = + createLocationTimeZoneEvent(USER1_ID, EVENT_TYPE_PERMANENT_FAILURE, null); private TestThreadingDomain mTestThreadingDomain; private TestCallback mTestCallback; - private TestLocationTimeZoneProvider mTestLocationTimeZoneProvider; + private TestLocationTimeZoneProvider mTestPrimaryLocationTimeZoneProvider; @Before public void setUp() { @@ -77,276 +85,446 @@ public class ControllerImplTest { // will never get a chance to execute. mTestThreadingDomain = new TestThreadingDomain(); mTestCallback = new TestCallback(mTestThreadingDomain); - mTestLocationTimeZoneProvider = + mTestPrimaryLocationTimeZoneProvider = new TestLocationTimeZoneProvider(mTestThreadingDomain, "primary"); } @Test public void initialState_enabled() { - ControllerImpl controllerImpl = - new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider); + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider); TestEnvironment testEnvironment = new TestEnvironment( mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + Duration expectedInitTimeout = testEnvironment.getProviderInitializationTimeout() + .plus(testEnvironment.getProviderInitializationTimeoutFuzz()); + + // Initialize. After initialization the provider must be initialized and should be + // enabled. controllerImpl.initialize(testEnvironment, mTestCallback); - mTestLocationTimeZoneProvider.assertInitialized(); + mTestPrimaryLocationTimeZoneProvider.assertInitialized(); - mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED); - Duration expectedTimeout = expectedProviderInitializationTimeout(); - mTestThreadingDomain.assertSingleDelayedQueueItem(expectedTimeout); + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestPrimaryLocationTimeZoneProvider.assertInitializationTimeoutSet(expectedInitTimeout); mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); } @Test public void initialState_disabled() { - ControllerImpl controllerImpl = - new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider); + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider); TestEnvironment testEnvironment = new TestEnvironment( mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_DISABLED); + + // Initialize. After initialization the provider must be initialized but should not be + // enabled. controllerImpl.initialize(testEnvironment, mTestCallback); - mTestLocationTimeZoneProvider.assertInitialized(); + mTestPrimaryLocationTimeZoneProvider.assertInitialized(); - mTestLocationTimeZoneProvider.assertIsDisabled(); - mTestThreadingDomain.assertQueueEmpty(); + mTestPrimaryLocationTimeZoneProvider.assertIsDisabledAndCommit(); mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); } @Test public void enabled_uncertaintySuggestionSentIfNoEventReceived() { - ControllerImpl controllerImpl = - new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider); + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider); TestEnvironment testEnvironment = new TestEnvironment( mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. controllerImpl.initialize(testEnvironment, mTestCallback); - mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestCallback.assertNoSuggestionMade(); - Duration expectedTimeout = expectedProviderInitializationTimeout(); - mTestThreadingDomain.assertSingleDelayedQueueItem(expectedTimeout); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - // Simulate time passing with no event being received. + // Simulate time passing with no provider event being received from the primary. mTestThreadingDomain.executeNext(); - mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED); + // The primary should have reported uncertainty, which should trigger the controller to + // start the uncertainty timeout. + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); + + // Finally, the uncertainty timeout should cause the controller to make an uncertain + // suggestion. + mTestThreadingDomain.executeNext(); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestCallback.assertUncertainSuggestionMadeAndCommit(); - mTestThreadingDomain.assertQueueEmpty(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); } @Test - public void enabled_uncertaintySuggestionCancelledIfEventReceived() { - ControllerImpl controllerImpl = - new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider); + public void enabled_eventReceivedBeforeInitializationTimeout() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider); TestEnvironment testEnvironment = new TestEnvironment( mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. controllerImpl.initialize(testEnvironment, mTestCallback); - mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED); - Duration expectedTimeout = expectedProviderInitializationTimeout(); - mTestThreadingDomain.assertSingleDelayedQueueItem(expectedTimeout); + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - // Simulate a location event being received by the provider. This should cause a suggestion - // to be made, and the timeout to be cleared. - mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent( + // Simulate a location event being received from the primary provider. This should cause a + // suggestion to be made. + mTestPrimaryLocationTimeZoneProvider.simulateLocationTimeZoneEvent( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); - mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestThreadingDomain.assertQueueEmpty(); + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestCallback.assertSuggestionMadeAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); } @Test - public void enabled_repeatedCertainty() { - ControllerImpl controllerImpl = - new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider); + public void enabled_eventReceivedFromPrimaryAfterInitializationTimeout() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider); TestEnvironment testEnvironment = new TestEnvironment( mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. controllerImpl.initialize(testEnvironment, mTestCallback); - mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED); - Duration expectedTimeout = expectedProviderInitializationTimeout(); - mTestThreadingDomain.assertSingleDelayedQueueItem(expectedTimeout); + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - // Simulate a location event being received by the provider. This should cause a suggestion - // to be made, and the timeout to be cleared. - mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent( + // Simulate time passing with no provider event being received from the primary. + mTestThreadingDomain.executeNext(); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); + + // Simulate a location event being received from the primary provider. This should cause a + // suggestion to be made. + mTestPrimaryLocationTimeZoneProvider.simulateLocationTimeZoneEvent( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); - mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestThreadingDomain.assertQueueEmpty(); + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestCallback.assertSuggestionMadeAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void enabled_repeatedPrimaryCertainty() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate a location event being received from the primary provider. This should cause a + // suggestion to be made. + mTestPrimaryLocationTimeZoneProvider.simulateLocationTimeZoneEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertSuggestionMadeAndCommit( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); // A second, identical event should not cause another suggestion. - mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent( + mTestPrimaryLocationTimeZoneProvider.simulateLocationTimeZoneEvent( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); - mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestThreadingDomain.assertQueueEmpty(); + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); // And a third, different event should cause another suggestion. - mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent( + mTestPrimaryLocationTimeZoneProvider.simulateLocationTimeZoneEvent( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); - mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestThreadingDomain.assertQueueEmpty(); + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestCallback.assertSuggestionMadeAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2.getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void enabled_uncertaintyTriggersASuggestionAfterUncertaintyTimeout() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate a location event being received from the primary provider. This should cause a + // suggestion to be made and ensure the primary is considered initialized. + mTestPrimaryLocationTimeZoneProvider.simulateLocationTimeZoneEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertSuggestionMadeAndCommit( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate an uncertain event being received from the primary provider. This should not + // cause a suggestion to be made straight away, but the uncertainty timeout should be + // started. + mTestPrimaryLocationTimeZoneProvider.simulateLocationTimeZoneEvent( + USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); + + // Simulate time passing. This means the uncertainty timeout should fire and the uncertain + // suggestion should be made. + mTestThreadingDomain.executeNext(); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertUncertainSuggestionMadeAndCommit(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); } @Test public void enabled_briefUncertaintyTriggersNoSuggestion() { - ControllerImpl controllerImpl = - new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider); + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider); TestEnvironment testEnvironment = new TestEnvironment( mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. controllerImpl.initialize(testEnvironment, mTestCallback); - mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED); - Duration expectedTimeout = expectedProviderInitializationTimeout(); - mTestThreadingDomain.assertSingleDelayedQueueItem(expectedTimeout); + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - // Simulate a location event being received by the provider. This should cause a suggestion - // to be made, and the timeout to be cleared. - mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent( + // Simulate a location event being received from the primary provider. This should cause a + // suggestion to be made. + mTestPrimaryLocationTimeZoneProvider.simulateLocationTimeZoneEvent( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); - mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestThreadingDomain.assertQueueEmpty(); + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestCallback.assertSuggestionMadeAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - // Uncertainty should cause a suggestion to (only) be queued. - mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent( + // Uncertainty should not cause a suggestion to be made straight away, but the uncertainty + // timeout should be started. + mTestPrimaryLocationTimeZoneProvider.simulateLocationTimeZoneEvent( USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); - mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestThreadingDomain.assertSingleDelayedQueueItem(testEnvironment.getUncertaintyDelay()); + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestCallback.assertNoSuggestionMade(); + assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); - // And a third event should cause yet another suggestion and for the queued item to be - // removed. - mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent( + // And a success event from the primary provider should cause the controller to make another + // suggestion, the uncertainty timeout should be cancelled. + mTestPrimaryLocationTimeZoneProvider.simulateLocationTimeZoneEvent( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); - mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestThreadingDomain.assertQueueEmpty(); + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestCallback.assertSuggestionMadeAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2.getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); } @Test - public void configChanges_enableAndDisable() { - ControllerImpl controllerImpl = - new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider); + public void configChanges_enableAndDisableWithNoPreviousSuggestion() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider); TestEnvironment testEnvironment = new TestEnvironment( mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_DISABLED); + + // Initialize and check initial state. controllerImpl.initialize(testEnvironment, mTestCallback); - mTestLocationTimeZoneProvider.assertIsDisabled(); - mTestThreadingDomain.assertQueueEmpty(); + mTestPrimaryLocationTimeZoneProvider.assertIsDisabledAndCommit(); mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); // Now signal a config change so that geo detection is enabled. testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED); - Duration expectedTimeout = expectedProviderInitializationTimeout(); - mTestThreadingDomain.assertSingleDelayedQueueItem(expectedTimeout); + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); // Now signal a config change so that geo detection is disabled. testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_DISABLED); - mTestLocationTimeZoneProvider.assertIsDisabled(); - mTestThreadingDomain.assertQueueEmpty(); + mTestPrimaryLocationTimeZoneProvider.assertIsDisabledAndCommit(); mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); } @Test - public void configChanges_disableWithPreviousSuggestion() { - ControllerImpl controllerImpl = - new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider); + public void configChanges_enableAndDisableWithPreviousSuggestion() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider); TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_DISABLED); + + // Initialize and check initial state. controllerImpl.initialize(testEnvironment, mTestCallback); - mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED); - Duration expectedTimeout = expectedProviderInitializationTimeout(); - mTestThreadingDomain.assertSingleDelayedQueueItem(expectedTimeout); + mTestPrimaryLocationTimeZoneProvider.assertIsDisabledAndCommit(); mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - // Simulate a location event being received by the provider. This should cause a suggestion - // to be made, and the timeout to be cleared. - mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent( + // Now signal a config change so that geo detection is enabled. + testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_ENABLED); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate a success event being received from the primary provider. + mTestPrimaryLocationTimeZoneProvider.simulateLocationTimeZoneEvent( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); - mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestThreadingDomain.assertQueueEmpty(); + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestCallback.assertSuggestionMadeAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - // Simulate the user disabling the provider. - testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_DISABLED); - + // Now signal a config change so that geo detection is disabled. // Because there had been a previous suggestion, the controller should withdraw it // immediately to let the downstream components know that the provider can no longer be sure // of the time zone. - mTestLocationTimeZoneProvider.assertIsDisabled(); - mTestThreadingDomain.assertQueueEmpty(); - mTestCallback.assertSuggestionMadeAndCommit(null); + testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_DISABLED); + + mTestPrimaryLocationTimeZoneProvider.assertIsDisabledAndCommit(); + mTestCallback.assertUncertainSuggestionMadeAndCommit(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); } @Test public void configChanges_userSwitch_enabledToEnabled() { - ControllerImpl controllerImpl = - new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider); + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider); TestEnvironment testEnvironment = new TestEnvironment( mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. controllerImpl.initialize(testEnvironment, mTestCallback); - // There should be a runnable scheduled to suggest uncertainty if no event is received. - mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED); - Duration expectedTimeout = expectedProviderInitializationTimeout(); - mTestThreadingDomain.assertSingleDelayedQueueItem(expectedTimeout); + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - // Have the provider suggest a time zone. - mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent( + // Simulate the primary provider suggesting a time zone. + mTestPrimaryLocationTimeZoneProvider.simulateLocationTimeZoneEvent( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); // Receiving a "success" provider event should cause a suggestion to be made synchronously, // and also clear the scheduled uncertainty suggestion. - mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestThreadingDomain.assertQueueEmpty(); + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestCallback.assertSuggestionMadeAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); // Simulate the user change (but geo detection still enabled). testEnvironment.simulateConfigChange(USER2_CONFIG_GEO_DETECTION_ENABLED); // We expect the provider to end up in PROVIDER_STATE_ENABLED, but it should have been // disabled when the user changed. - // The controller should schedule a runnable to make a suggestion if the provider doesn't - // send a success event. - int[] expectedStateTransitions = { PROVIDER_STATE_DISABLED, PROVIDER_STATE_ENABLED }; - mTestLocationTimeZoneProvider.assertStateChangesAndCommit(expectedStateTransitions); - mTestLocationTimeZoneProvider.assertConfig(USER2_CONFIG_GEO_DETECTION_ENABLED); - expectedTimeout = expectedProviderInitializationTimeout(); - mTestThreadingDomain.assertSingleDelayedQueueItem(expectedTimeout); + int[] expectedStateTransitions = + { PROVIDER_STATE_DISABLED, PROVIDER_STATE_ENABLED_INITIALIZING }; + mTestPrimaryLocationTimeZoneProvider.assertStateChangesAndCommit(expectedStateTransitions); + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfig( + PROVIDER_STATE_ENABLED_INITIALIZING, USER2_CONFIG_GEO_DETECTION_ENABLED); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void primaryPermFailure_disableAndEnable() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_ENABLED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - // Simulate no event being received, and time passing. - mTestThreadingDomain.executeNext(); + // Simulate a failure location event being received from the primary provider. This should + // cause an uncertain suggestion to be made. + mTestPrimaryLocationTimeZoneProvider.simulateLocationTimeZoneEvent( + USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT); - mTestLocationTimeZoneProvider.assertIsEnabled(USER2_CONFIG_GEO_DETECTION_ENABLED); - mTestThreadingDomain.assertQueueEmpty(); + mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); mTestCallback.assertUncertainSuggestionMadeAndCommit(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Now signal a config change so that geo detection is disabled. + testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_DISABLED); + + mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Now signal a config change so that geo detection is enabled. + testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_ENABLED); + + mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestCallback.assertUncertainSuggestionMadeAndCommit(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + private static void assertUncertaintyTimeoutSet( + LocationTimeZoneProviderController.Environment environment, + LocationTimeZoneProviderController controller) { + assertTrue(controller.isUncertaintyTimeoutSet()); + assertEquals(environment.getUncertaintyDelay().toMillis(), + controller.getUncertaintyTimeoutDelayMillis()); } private static LocationTimeZoneEvent createLocationTimeZoneEvent(@UserIdInt int userId, @@ -362,16 +540,15 @@ public class ControllerImplTest { } - private Duration expectedProviderInitializationTimeout() { - return TestEnvironment.PROVIDER_INITIALIZATION_TIMEOUT - .plus(TestEnvironment.PROVIDER_INITIALIZATION_TIMEOUT_FUZZ); - } - private static class TestEnvironment extends LocationTimeZoneProviderController.Environment { + // These timeouts are set deliberately so that: + // (initialization timeout * 2) < uncertainty delay + // + // That makes the order of initialization timeout Vs uncertainty delay deterministic. static final Duration PROVIDER_INITIALIZATION_TIMEOUT = Duration.ofMinutes(5); static final Duration PROVIDER_INITIALIZATION_TIMEOUT_FUZZ = Duration.ofMinutes(1); - private static final Duration UNCERTAINTY_DELAY = Duration.ofMinutes(3); + private static final Duration UNCERTAINTY_DELAY = Duration.ofMinutes(15); private final LocationTimeZoneProviderController mController; private ConfigurationInternal mConfigurationInternal; @@ -491,38 +668,57 @@ public class ControllerImplTest { assertTrue(mInitialized); } - void assertIsDisabled() { - // Disabled providers don't hold config. - assertConfig(null); - assertIsEnabledAndCommit(false); - } - - /** - * Asserts the provider's config matches the expected, and the current state is set - * accordingly. Commits the latest changes to the state. - */ - void assertIsEnabled(@NonNull ConfigurationInternal expectedConfig) { - assertConfig(expectedConfig); - - boolean expectIsEnabled = expectedConfig.getAutoDetectionEnabledBehavior(); - assertIsEnabledAndCommit(expectIsEnabled); - } - - private void assertIsEnabledAndCommit(boolean enabled) { - ProviderState currentState = mCurrentState.get(); - if (enabled) { - assertEquals(PROVIDER_STATE_ENABLED, currentState.stateEnum); - } else { - assertEquals(PROVIDER_STATE_DISABLED, currentState.stateEnum); - } + public void assertIsPermFailedAndCommit() { + // A failed provider doesn't hold config. + assertStateEnumAndConfig(PROVIDER_STATE_PERM_FAILED, null /* config */); mTestProviderState.commitLatest(); } - void assertConfig(@NonNull ConfigurationInternal expectedConfig) { + void assertIsDisabledAndCommit() { + // A disabled provider doesn't hold config. + assertStateEnumAndConfig(PROVIDER_STATE_DISABLED, null /* config */); + mTestProviderState.commitLatest(); + } + + /** + * Asserts the provider's state enum and config matches the expected. + * Commits the latest changes to the state. + */ + void assertStateEnumAndConfigAndCommit( + @ProviderStateEnum int expectedStateEnum, + @Nullable ConfigurationInternal expectedConfig) { + assertStateEnumAndConfig(expectedStateEnum, expectedConfig); + mTestProviderState.commitLatest(); + } + + /** + * Asserts the provider's state enum and config matches the expected. + * Does not commit any state changes. + */ + void assertStateEnumAndConfig( + @ProviderStateEnum int expectedStateEnum, + @Nullable ConfigurationInternal expectedConfig) { + ProviderState currentState = mCurrentState.get(); + assertEquals(expectedStateEnum, currentState.stateEnum); + + // If and only if the controller is initializing, the initialization timeout must be + // set. + assertEquals(expectedStateEnum == PROVIDER_STATE_ENABLED_INITIALIZING, + isInitializationTimeoutSet()); + + assertConfig(expectedConfig); + } + + private void assertConfig(@Nullable ConfigurationInternal expectedConfig) { ProviderState currentState = mCurrentState.get(); assertEquals(expectedConfig, currentState.currentUserConfiguration); } + void assertInitializationTimeoutSet(Duration expectedTimeout) { + assertTrue(isInitializationTimeoutSet()); + assertEquals(expectedTimeout, getInitializationTimeoutDelay()); + } + void simulateLocationTimeZoneEvent(@NonNull LocationTimeZoneEvent event) { handleLocationTimeZoneEvent(event); } 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 cbaf0f391375a..4d6775dcaecf5 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 @@ -115,28 +115,7 @@ public class HandlerThreadingDomainTest { } @Test - public void singleRunnableHandler_runSynchronously() throws Exception { - ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler); - SingleRunnableQueue singleRunnableQueue = domain.createSingleRunnableQueue(); - - AtomicBoolean testPassed = new AtomicBoolean(false); - // Calls to SingleRunnableQueue must be made on the handler thread it is associated with, - // so this uses runWithScissors() to block until the lambda has completed. - mTestHandler.runWithScissors(() -> { - Thread testThread = Thread.currentThread(); - CountDownLatch latch = new CountDownLatch(1); - singleRunnableQueue.runSynchronously(() -> { - assertSame(Thread.currentThread(), testThread); - latch.countDown(); - }); - assertTrue(awaitWithRuntimeException(latch, 60, TimeUnit.SECONDS)); - testPassed.set(true); - }, TimeUnit.SECONDS.toMillis(60)); - assertTrue(testPassed.get()); - } - - @Test - public void singleRunnableHandler_runDelayed() throws Exception { + public void singleRunnableQueue_runDelayed() throws Exception { ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler); SingleRunnableQueue singleRunnableQueue = domain.createSingleRunnableQueue(); 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 index 5542db0b5ae3d..5403a654f7e43 100644 --- a/services/tests/servicestests/src/com/android/server/location/timezone/NullLocationTimeZoneProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/location/timezone/NullLocationTimeZoneProviderTest.java @@ -16,7 +16,7 @@ package com.android.server.location.timezone; import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DISABLED; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED; +import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED_INITIALIZING; import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED; import static com.android.server.location.timezone.TestSupport.USER1_CONFIG_GEO_DETECTION_ENABLED; @@ -76,15 +76,16 @@ public class NullLocationTimeZoneProviderTest { ConfigurationInternal config = USER1_CONFIG_GEO_DETECTION_ENABLED; Duration arbitraryInitializationTimeout = Duration.ofMinutes(5); - provider.enable(config, arbitraryInitializationTimeout); + Duration arbitraryInitializationTimeoutFuzz = Duration.ofMinutes(2); + provider.enable(config, arbitraryInitializationTimeout, arbitraryInitializationTimeoutFuzz); - // The StubbedProvider should enters enabled state, but immediately schedule a runnable to - // switch to perm failure. + // 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_ENABLED, currentState.stateEnum); + assertEquals(PROVIDER_STATE_ENABLED_INITIALIZING, currentState.stateEnum); assertEquals(config, currentState.currentUserConfiguration); - mTestThreadingDomain.assertSingleImmediateQueueItem(); + mTestThreadingDomain.assertNextQueueItemIsImmediate(); // Entering enabled() does not trigger an onProviderStateChanged() as it is requested by the // controller. mTestController.assertProviderChangeNotTriggered(); @@ -116,6 +117,18 @@ public class NullLocationTimeZoneProviderTest { // 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); } 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 def919e79731a..7359abd11c3a3 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 @@ -16,13 +16,15 @@ package com.android.server.location.timezone; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.annotation.NonNull; import android.annotation.Nullable; import java.time.Duration; -import java.util.LinkedList; +import java.util.ArrayList; +import java.util.Comparator; import java.util.Objects; /** @@ -33,6 +35,10 @@ import java.util.Objects; class TestThreadingDomain extends ThreadingDomain { static class QueuedRunnable { + + static final Comparator COMPARATOR = + (o1, o2) -> (int) (o1.executionTimeMillis - o2.executionTimeMillis); + @NonNull public final Runnable runnable; @Nullable public final Object token; public final long executionTimeMillis; @@ -55,7 +61,7 @@ class TestThreadingDomain extends ThreadingDomain { } private long mCurrentTimeMillis; - private LinkedList mQueue = new LinkedList<>(); + private ArrayList mQueue = new ArrayList<>(); TestThreadingDomain() { // Pick an arbitrary time. @@ -69,22 +75,23 @@ class TestThreadingDomain extends ThreadingDomain { @Override void post(Runnable r) { - mQueue.add(new QueuedRunnable(r, null, mCurrentTimeMillis)); + postDelayed(r, null, 0); } @Override void postDelayed(Runnable r, long delayMillis) { - mQueue.add(new QueuedRunnable(r, null, mCurrentTimeMillis + delayMillis)); + postDelayed(r, null, delayMillis); } @Override void postDelayed(Runnable r, Object token, long delayMillis) { mQueue.add(new QueuedRunnable(r, token, mCurrentTimeMillis + delayMillis)); + mQueue.sort(QueuedRunnable.COMPARATOR); } @Override void removeQueuedRunnables(Object token) { - mQueue.removeIf(runnable -> runnable.token != null && runnable.token.equals(token)); + mQueue.removeIf(runnable -> runnable.token != null && runnable.token == token); } void assertSingleDelayedQueueItem(Duration expectedDelay) { @@ -114,14 +121,14 @@ class TestThreadingDomain extends ThreadingDomain { } long getNextQueueItemDelayMillis() { - assertQueueLength(1); - return mQueue.getFirst().executionTimeMillis - mCurrentTimeMillis; + assertFalse(mQueue.isEmpty()); + return mQueue.get(0).executionTimeMillis - mCurrentTimeMillis; } void executeNext() { - assertQueueLength(1); + assertFalse(mQueue.isEmpty()); + QueuedRunnable queued = mQueue.remove(0); - QueuedRunnable queued = mQueue.removeFirst(); mCurrentTimeMillis = queued.executionTimeMillis; queued.runnable.run(); }