Refactoring to make adding a secondary easier

This commit contains refactorings intended to make the addition of a
secondary LocationTimeZoneProvider easier and clearer.

This change contains various refactorings to code and comments:
1) Start referring to the provider as the primary in some places.

2) Move the initialization timeout tracking to the
LocationTimeZoneProvider so each provider can be responsible for
tracking its own initialization timeout, giving the controller less to
do.

3) The PROVIDER_STATE_ENABLED has been split into _INITIALIZING,
_CERTAIN and _UNCERTAIN variants which are determined by the most recent
event the provider has sent. The controller therefore doesn't need
to track this either.

4) Introduction of a "single runnable" for the uncertainty timeout
managed by the ControllerImpl.

5) Many ControllerImplTest changes resulting from all the above and to
maximize tests readability.

Bug: 152744911
Bug: 149014708
Test: atest services/tests/servicestests/src/com/android/server/location/timezone/
Change-Id: I64b9cc33c5f97f2edb1063a8133434bfd8e9dda1
This commit is contained in:
Neil Fuller
2020-09-23 11:35:37 +01:00
parent 8e612b1d1d
commit 4373cecd8b
10 changed files with 826 additions and 404 deletions

View File

@@ -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;

View File

@@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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<String> 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.
*
* <p>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.
*
* <p>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);
}
}

View File

@@ -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);

View File

@@ -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<ProviderState> 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());
}
}
}

View File

@@ -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.

View File

@@ -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;
}
}
}

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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<? super QueuedRunnable> 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<QueuedRunnable> mQueue = new LinkedList<>();
private ArrayList<QueuedRunnable> 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();
}