Merge "Refactoring to make adding a secondary easier"

This commit is contained in:
Neil Fuller
2020-09-30 16:04:23 +00:00
committed by Android (Google) Code Review
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();
}