blob: ed7ea00ec8f57a613ce7e303f3267eea8f1aa863 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.timezonedetector.location;
import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_PERMANENT_FAILURE;
import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_SUGGESTION;
import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_UNCERTAIN;
import static com.android.server.timezonedetector.location.LocationTimeZoneManagerService.debugLog;
import static com.android.server.timezonedetector.location.LocationTimeZoneManagerService.warnLog;
import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState;
import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED;
import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_CERTAIN;
import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING;
import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_UNCERTAIN;
import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STOPPED;
import android.annotation.DurationMillisLong;
import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringDef;
import android.app.time.DetectorStatusTypes;
import android.app.time.DetectorStatusTypes.DetectionAlgorithmStatus;
import android.app.time.LocationTimeZoneAlgorithmStatus;
import android.app.time.LocationTimeZoneAlgorithmStatus.ProviderStatus;
import android.service.timezone.TimeZoneProviderEvent;
import android.service.timezone.TimeZoneProviderSuggestion;
import android.util.IndentingPrintWriter;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.timezonedetector.ConfigurationInternal;
import com.android.server.timezonedetector.Dumpable;
import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion;
import com.android.server.timezonedetector.LocationAlgorithmEvent;
import com.android.server.timezonedetector.ReferenceWithHistory;
import com.android.server.timezonedetector.location.ThreadingDomain.SingleRunnableQueue;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Objects;
/**
* The component responsible handling events from {@link LocationTimeZoneProvider}s and synthesizing
* time zone ID suggestions for sending to the time zone detector.
*
* <p>This class primarily exists to extract unit-testable logic from the surrounding service class,
* i.e. with a minimal number of threading considerations or direct dependencies on Android
* infrastructure.
*
* <p>This class supports a primary and a secondary {@link LocationTimeZoneProvider}. The primary is
* used until it fails or becomes uncertain. The secondary will then be started. The controller will
* immediately make suggestions based on "certain" {@link TimeZoneProviderEvent}s, i.e. events that
* demonstrate the provider is certain what the time zone is. The controller will not make immediate
* suggestions based on "uncertain" events, giving providers time to change their mind. This also
* gives the secondary provider time to initialize when the primary becomes uncertain.
*
* <p>The controller interacts with the following components:
* <ul>
* <li>The surrounding service, which calls {@link #initialize(Environment, Callback)}.
* <li>The {@link Environment} through which it obtains information it needs.</li>
* <li>The {@link Callback} through which it makes time zone suggestions.</li>
* <li>Any {@link LocationTimeZoneProvider} instances it owns, which communicate via the
* {@link LocationTimeZoneProvider.ProviderListener#onProviderStateChange(ProviderState)}
* method.</li>
* </ul>
*
* <p>All incoming calls except for {@link
* LocationTimeZoneProviderController#dump(android.util.IndentingPrintWriter, String[])} must be
* made on the {@link android.os.Handler} thread of the {@link ThreadingDomain} passed to {@link
* #LocationTimeZoneProviderController}.
*
* <p>Provider / controller integration notes:
*
* <p>Providers distinguish between "unknown unknowns" ("uncertain") and "known unknowns"
* ("certain"), i.e. a provider can be uncertain and not know what the time zone is, which is
* different from the certainty that there are no time zone IDs for the current location. A provider
* can be certain about there being no time zone IDs for a location for good reason, e.g. for
* disputed areas and oceans. Distinguishing uncertainty allows the controller to try other
* providers (or give up), whereas certainty means it should not.
*
* <p>A provider can fail permanently. A permanent failure will stop the provider until next
* boot.
*/
class LocationTimeZoneProviderController implements Dumpable {
// String is used for easier logging / interpretation in bug reports Vs int.
@StringDef(prefix = "STATE_",
value = { STATE_UNKNOWN, STATE_PROVIDERS_INITIALIZING, STATE_STOPPED,
STATE_INITIALIZING, STATE_UNCERTAIN, STATE_CERTAIN, STATE_FAILED,
STATE_DESTROYED })
@Retention(RetentionPolicy.SOURCE)
@Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER })
@interface State {}
/** The state used for an uninitialized controller. */
static final @State String STATE_UNKNOWN = "UNKNOWN";
/**
* A state used while the location time zone providers are initializing. Enables detection
* / avoidance of unwanted fail-over behavior before both providers are initialized.
*/
static final @State String STATE_PROVIDERS_INITIALIZING = "PROVIDERS_INITIALIZING";
/** An inactive state: Detection is disabled. */
static final @State String STATE_STOPPED = "STOPPED";
/** An active state: No suggestion has yet been made. */
static final @State String STATE_INITIALIZING = "INITIALIZING";
/** An active state: The last suggestion was "uncertain". */
static final @State String STATE_UNCERTAIN = "UNCERTAIN";
/** An active state: The last suggestion was "certain". */
static final @State String STATE_CERTAIN = "CERTAIN";
/** An inactive state: The location time zone providers have failed. */
static final @State String STATE_FAILED = "FAILED";
/** An inactive state: The controller is destroyed. */
static final @State String STATE_DESTROYED = "DESTROYED";
@NonNull private final ThreadingDomain mThreadingDomain;
@NonNull private final Object mSharedLock;
/**
* Used for scheduling uncertainty timeouts, i.e. after a provider has reported uncertainty.
* This timeout is not provider-specific: it is started when the controller becomes uncertain
* due to events it has received from one or other provider.
*/
@NonNull private final SingleRunnableQueue mUncertaintyTimeoutQueue;
@NonNull private final MetricsLogger mMetricsLogger;
@NonNull private final LocationTimeZoneProvider mPrimaryProvider;
@NonNull private final LocationTimeZoneProvider mSecondaryProvider;
@GuardedBy("mSharedLock")
// Non-null after initialize()
private ConfigurationInternal mCurrentUserConfiguration;
@GuardedBy("mSharedLock")
// Non-null after initialize()
private Environment mEnvironment;
@GuardedBy("mSharedLock")
// Non-null after initialize()
private Callback mCallback;
/** Usually {@code false} but can be set to {@code true} to record state changes for testing. */
private final boolean mRecordStateChanges;
@GuardedBy("mSharedLock")
@NonNull
private final ArrayList<@State String> mRecordedStates = new ArrayList<>(0);
/**
* The current state. This is primarily for metrics / reporting of how long the controller
* spends active / inactive during a period. There is overlap with the provider states, but
* providers operate independently of each other, so this can help to understand how long the
* geo detection system overall was certain or uncertain when multiple providers might have been
* enabled concurrently.
*/
@GuardedBy("mSharedLock")
private final ReferenceWithHistory<@State String> mState = new ReferenceWithHistory<>(10);
/** Contains the last event reported, if there is one. */
@GuardedBy("mSharedLock")
@Nullable
private LocationAlgorithmEvent mLastEvent;
LocationTimeZoneProviderController(@NonNull ThreadingDomain threadingDomain,
@NonNull MetricsLogger metricsLogger,
@NonNull LocationTimeZoneProvider primaryProvider,
@NonNull LocationTimeZoneProvider secondaryProvider,
boolean recordStateChanges) {
mThreadingDomain = Objects.requireNonNull(threadingDomain);
mSharedLock = threadingDomain.getLockObject();
mUncertaintyTimeoutQueue = threadingDomain.createSingleRunnableQueue();
mMetricsLogger = Objects.requireNonNull(metricsLogger);
mPrimaryProvider = Objects.requireNonNull(primaryProvider);
mSecondaryProvider = Objects.requireNonNull(secondaryProvider);
mRecordStateChanges = recordStateChanges;
synchronized (mSharedLock) {
mState.set(STATE_UNKNOWN);
}
}
/**
* Called to initialize the controller during boot. Called once only.
* {@link LocationTimeZoneProvider#initialize} must be called by this method.
*/
void initialize(@NonNull Environment environment, @NonNull Callback callback) {
mThreadingDomain.assertCurrentThread();
synchronized (mSharedLock) {
debugLog("initialize()");
mEnvironment = Objects.requireNonNull(environment);
mCallback = Objects.requireNonNull(callback);
mCurrentUserConfiguration = environment.getCurrentUserConfigurationInternal();
LocationTimeZoneProvider.ProviderListener providerListener =
LocationTimeZoneProviderController.this::onProviderStateChange;
setState(STATE_PROVIDERS_INITIALIZING);
mPrimaryProvider.initialize(providerListener);
mSecondaryProvider.initialize(providerListener);
setStateAndReportStatusOnlyEvent(STATE_STOPPED, "initialize()");
alterProvidersStartedStateIfRequired(
null /* oldConfiguration */, mCurrentUserConfiguration);
}
}
/**
* Called when the content of the {@link ConfigurationInternal} may have changed. The receiver
* should call {@link Environment#getCurrentUserConfigurationInternal()} to get the current
* user's config. This call must be made on the {@link ThreadingDomain} handler thread.
*/
void onConfigurationInternalChanged() {
mThreadingDomain.assertCurrentThread();
synchronized (mSharedLock) {
debugLog("onConfigChanged()");
ConfigurationInternal oldConfig = mCurrentUserConfiguration;
ConfigurationInternal newConfig = mEnvironment.getCurrentUserConfigurationInternal();
mCurrentUserConfiguration = newConfig;
if (!newConfig.equals(oldConfig)) {
if (newConfig.getUserId() != oldConfig.getUserId()) {
// If the user changed, stop the providers if needed. They may be re-started
// for the new user immediately afterwards if their settings allow.
String reason = "User changed. old=" + oldConfig.getUserId()
+ ", new=" + newConfig.getUserId();
debugLog("Stopping providers: " + reason);
stopProviders(reason);
alterProvidersStartedStateIfRequired(null /* oldConfiguration */, newConfig);
} else {
alterProvidersStartedStateIfRequired(oldConfig, newConfig);
}
}
}
}
@VisibleForTesting
boolean isUncertaintyTimeoutSet() {
return mUncertaintyTimeoutQueue.hasQueued();
}
@VisibleForTesting
@DurationMillisLong
long getUncertaintyTimeoutDelayMillis() {
return mUncertaintyTimeoutQueue.getQueuedDelayMillis();
}
/** Called if the geolocation time zone detection is being reconfigured. */
void destroy() {
mThreadingDomain.assertCurrentThread();
synchronized (mSharedLock) {
stopProviders("destroy()");
// Enter destroyed state.
mPrimaryProvider.destroy();
mSecondaryProvider.destroy();
setStateAndReportStatusOnlyEvent(STATE_DESTROYED, "destroy()");
}
}
/**
* Sets the state and reports an event containing the algorithm status and a {@code null}
* suggestion.
*/
@GuardedBy("mSharedLock")
private void setStateAndReportStatusOnlyEvent(@State String state, @NonNull String reason) {
setState(state);
final GeolocationTimeZoneSuggestion suggestion = null;
LocationAlgorithmEvent event =
new LocationAlgorithmEvent(generateCurrentAlgorithmStatus(), suggestion);
event.addDebugInfo(reason);
reportEvent(event);
}
/**
* Reports an event containing the algorithm status and the supplied suggestion.
*/
@GuardedBy("mSharedLock")
private void reportSuggestionEvent(
@NonNull GeolocationTimeZoneSuggestion suggestion, @NonNull String reason) {
LocationTimeZoneAlgorithmStatus algorithmStatus = generateCurrentAlgorithmStatus();
LocationAlgorithmEvent event = new LocationAlgorithmEvent(
algorithmStatus, suggestion);
event.addDebugInfo(reason);
reportEvent(event);
}
/**
* Sends an event immediately. This method updates {@link #mLastEvent}.
*/
@GuardedBy("mSharedLock")
private void reportEvent(@NonNull LocationAlgorithmEvent event) {
debugLog("makeSuggestion: suggestion=" + event);
mCallback.sendEvent(event);
mLastEvent = event;
}
/**
* Updates the state if needed. This includes setting {@link #mState} and performing all the
* record-keeping / callbacks associated with state changes.
*/
@GuardedBy("mSharedLock")
private void setState(@State String state) {
if (!Objects.equals(mState.get(), state)) {
mState.set(state);
if (mRecordStateChanges) {
mRecordedStates.add(state);
}
mMetricsLogger.onStateChange(state);
}
}
@GuardedBy("mSharedLock")
private void stopProviders(@NonNull String reason) {
stopProviderIfStarted(mPrimaryProvider);
stopProviderIfStarted(mSecondaryProvider);
// By definition, if both providers are stopped, the controller is uncertain.
cancelUncertaintyTimeout();
setStateAndReportStatusOnlyEvent(STATE_STOPPED, "Providers stopped: " + reason);
}
@GuardedBy("mSharedLock")
private void stopProviderIfStarted(@NonNull LocationTimeZoneProvider provider) {
if (provider.getCurrentState().isStarted()) {
stopProvider(provider);
}
}
@GuardedBy("mSharedLock")
private void stopProvider(@NonNull LocationTimeZoneProvider provider) {
ProviderState providerState = provider.getCurrentState();
switch (providerState.stateEnum) {
case PROVIDER_STATE_STOPPED: {
debugLog("No need to stop " + provider + ": already stopped");
break;
}
case PROVIDER_STATE_STARTED_INITIALIZING:
case PROVIDER_STATE_STARTED_CERTAIN:
case PROVIDER_STATE_STARTED_UNCERTAIN: {
debugLog("Stopping " + provider);
provider.stopUpdates();
break;
}
case PROVIDER_STATE_PERM_FAILED:
case PROVIDER_STATE_DESTROYED: {
debugLog("Unable to stop " + provider + ": it is terminated.");
break;
}
default: {
warnLog("Unknown provider state: " + provider);
break;
}
}
}
/**
* Sets the providers into the correct started/stopped 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 started / stopped state may need to be set or changed, e.g. during initialization
* or when a new configuration has been received.
*/
@GuardedBy("mSharedLock")
private void alterProvidersStartedStateIfRequired(
@Nullable ConfigurationInternal oldConfiguration,
@NonNull ConfigurationInternal newConfiguration) {
// Provider started / stopped states only need to be changed if geoDetectionEnabled has
// changed.
boolean oldIsGeoDetectionExecutionEnabled = oldConfiguration != null
&& oldConfiguration.isGeoDetectionExecutionEnabled();
boolean newIsGeoDetectionExecutionEnabled =
newConfiguration.isGeoDetectionExecutionEnabled();
if (oldIsGeoDetectionExecutionEnabled == newIsGeoDetectionExecutionEnabled) {
return;
}
// The check above ensures that the logic below only executes if providers are going from
// {started *} -> {stopped}, or {stopped} -> {started initializing}. If this changes in
// future and there could be {started *} -> {started *} cases, or cases where the provider
// can't be assumed to go straight to the {started initializing} state, then the logic below
// would need to cover extra conditions, for example:
// 1) If the primary is in {started uncertain}, the secondary should be started.
// 2) If (1), and the secondary instantly enters the {perm failed} state, the uncertainty
// timeout started when the primary entered {started uncertain} should be cancelled.
if (newIsGeoDetectionExecutionEnabled) {
setStateAndReportStatusOnlyEvent(STATE_INITIALIZING, "initializing()");
// Try to start the primary provider.
tryStartProvider(mPrimaryProvider, newConfiguration);
// The secondary should only ever be started if the primary now isn't started (i.e. it
// couldn't become {started initializing} because it is {perm failed}).
ProviderState newPrimaryState = mPrimaryProvider.getCurrentState();
if (!newPrimaryState.isStarted()) {
// If the primary provider is {perm failed} then the controller must try to start
// the secondary.
tryStartProvider(mSecondaryProvider, newConfiguration);
ProviderState newSecondaryState = mSecondaryProvider.getCurrentState();
if (!newSecondaryState.isStarted()) {
// If both providers are {perm failed} then the controller immediately
// reports the failure.
String reason = "Providers are failed:"
+ " primary=" + mPrimaryProvider.getCurrentState()
+ " secondary=" + mPrimaryProvider.getCurrentState();
setStateAndReportStatusOnlyEvent(STATE_FAILED, reason);
}
}
} else {
stopProviders("Geo detection behavior disabled");
}
}
@GuardedBy("mSharedLock")
private void tryStartProvider(@NonNull LocationTimeZoneProvider provider,
@NonNull ConfigurationInternal configuration) {
ProviderState providerState = provider.getCurrentState();
switch (providerState.stateEnum) {
case PROVIDER_STATE_STOPPED: {
debugLog("Enabling " + provider);
provider.startUpdates(configuration,
mEnvironment.getProviderInitializationTimeout(),
mEnvironment.getProviderInitializationTimeoutFuzz(),
mEnvironment.getProviderEventFilteringAgeThreshold());
break;
}
case PROVIDER_STATE_STARTED_INITIALIZING:
case PROVIDER_STATE_STARTED_CERTAIN:
case PROVIDER_STATE_STARTED_UNCERTAIN: {
debugLog("No need to start " + provider + ": already started");
break;
}
case PROVIDER_STATE_PERM_FAILED:
case PROVIDER_STATE_DESTROYED: {
debugLog("Unable to start " + provider + ": it is terminated");
break;
}
default: {
throw new IllegalStateException("Unknown provider state:"
+ " provider=" + provider);
}
}
}
void onProviderStateChange(@NonNull ProviderState providerState) {
mThreadingDomain.assertCurrentThread();
LocationTimeZoneProvider provider = providerState.provider;
assertProviderKnown(provider);
synchronized (mSharedLock) {
// Ignore provider state changes during initialization. e.g. if the primary provider
// moves to PROVIDER_STATE_PERM_FAILED during initialization, the secondary will not
// be ready to take over yet.
if (Objects.equals(mState.get(), STATE_PROVIDERS_INITIALIZING)) {
warnLog("onProviderStateChange: Ignoring provider state change because both"
+ " providers have not yet completed initialization."
+ " providerState=" + providerState);
return;
}
switch (providerState.stateEnum) {
case PROVIDER_STATE_STARTED_INITIALIZING:
case PROVIDER_STATE_STOPPED:
case PROVIDER_STATE_DESTROYED: {
// This should never happen: entering initializing, stopped or destroyed are
// triggered by the controller so and should not trigger a state change
// callback.
warnLog("onProviderStateChange: Unexpected state change for provider,"
+ " provider=" + provider);
break;
}
case PROVIDER_STATE_STARTED_CERTAIN:
case PROVIDER_STATE_STARTED_UNCERTAIN: {
// These are valid and only happen if an event is received while the provider is
// started.
debugLog("onProviderStateChange: Received notification of a state change while"
+ " started, provider=" + provider);
handleProviderStartedStateChange(providerState);
break;
}
case PROVIDER_STATE_PERM_FAILED: {
debugLog("Received notification of permanent failure for"
+ " provider=" + provider);
handleProviderFailedStateChange(providerState);
break;
}
default: {
warnLog("onProviderStateChange: Unexpected provider=" + provider);
}
}
}
}
private void assertProviderKnown(@NonNull LocationTimeZoneProvider provider) {
if (provider != mPrimaryProvider && provider != mSecondaryProvider) {
throw new IllegalArgumentException("Unknown provider: " + provider);
}
}
/**
* Called when a provider has reported that it has failed permanently.
*/
@GuardedBy("mSharedLock")
private void handleProviderFailedStateChange(@NonNull ProviderState providerState) {
LocationTimeZoneProvider failedProvider = providerState.provider;
ProviderState primaryCurrentState = mPrimaryProvider.getCurrentState();
ProviderState secondaryCurrentState = mSecondaryProvider.getCurrentState();
// If a provider has failed, the other may need to be started.
if (failedProvider == mPrimaryProvider) {
if (!secondaryCurrentState.isTerminated()) {
// Try to start the secondary. This does nothing if the provider is already
// started, and will leave the provider in {started initializing} if the provider is
// stopped.
tryStartProvider(mSecondaryProvider, mCurrentUserConfiguration);
}
} else if (failedProvider == mSecondaryProvider) {
// No-op: The secondary will only be active if the primary is uncertain or is
// terminated. So, there the primary should not need to be started when the secondary
// fails.
if (primaryCurrentState.stateEnum != PROVIDER_STATE_STARTED_UNCERTAIN
&& !primaryCurrentState.isTerminated()) {
warnLog("Secondary provider unexpected reported a failure:"
+ " failed provider=" + failedProvider.getName()
+ ", primary provider=" + mPrimaryProvider
+ ", secondary provider=" + mSecondaryProvider);
}
}
// If both providers are now terminated, the controller needs to tell the next component in
// the time zone detection process.
if (primaryCurrentState.isTerminated() && secondaryCurrentState.isTerminated()) {
// If both providers are newly terminated then the controller is uncertain by definition
// and it will never recover so it can send a suggestion immediately.
cancelUncertaintyTimeout();
// If both providers are now terminated, then a suggestion must be sent informing the
// time zone detector that there are no further updates coming in the future.
String reason = "Both providers are terminated:"
+ " primary=" + primaryCurrentState.provider
+ ", secondary=" + secondaryCurrentState.provider;
setStateAndReportStatusOnlyEvent(STATE_FAILED, reason);
}
}
/**
* Called when a provider has changed state but just moved from one started state to another
* started state, usually as a result of a new {@link TimeZoneProviderEvent} being received.
* However, there are rare cases where the event can also be null.
*/
@GuardedBy("mSharedLock")
private void handleProviderStartedStateChange(@NonNull ProviderState providerState) {
LocationTimeZoneProvider provider = providerState.provider;
TimeZoneProviderEvent event = providerState.event;
if (event == null) {
// Implicit uncertainty, i.e. where the provider is started, 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, 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.
long uncertaintyStartedElapsedMillis = mEnvironment.elapsedRealtimeMillis();
handleProviderUncertainty(provider, uncertaintyStartedElapsedMillis,
"provider=" + provider + ", implicit uncertainty, event=null");
return;
}
if (!mCurrentUserConfiguration.isGeoDetectionExecutionEnabled()) {
// This should not happen: the provider should not be in a started state if
// geodetection is not enabled.
warnLog("Provider=" + provider + " is started, but"
+ " currentUserConfiguration=" + mCurrentUserConfiguration
+ " suggests it shouldn't be.");
}
switch (event.getType()) {
case EVENT_TYPE_PERMANENT_FAILURE: {
// This shouldn't happen. A provider cannot be started and have this event type.
warnLog("Provider=" + provider + " is started, but event suggests it shouldn't be");
break;
}
case EVENT_TYPE_UNCERTAIN: {
long uncertaintyStartedElapsedMillis = event.getCreationElapsedMillis();
handleProviderUncertainty(provider, uncertaintyStartedElapsedMillis,
"provider=" + provider + ", explicit uncertainty. event=" + event);
break;
}
case EVENT_TYPE_SUGGESTION: {
handleProviderSuggestion(provider, event);
break;
}
default: {
warnLog("Unknown eventType=" + event.getType());
break;
}
}
}
/**
* Called when a provider has become "certain" about the time zone(s).
*/
@GuardedBy("mSharedLock")
private void handleProviderSuggestion(
@NonNull LocationTimeZoneProvider provider,
@NonNull TimeZoneProviderEvent providerEvent) {
// By definition, the controller is now certain.
cancelUncertaintyTimeout();
if (provider == mPrimaryProvider) {
stopProviderIfStarted(mSecondaryProvider);
}
TimeZoneProviderSuggestion providerSuggestion = providerEvent.getSuggestion();
// Set the current state so it is correct when the suggestion event is created.
setState(STATE_CERTAIN);
// For the suggestion's effectiveFromElapsedMillis, use the time embedded in the provider's
// suggestion (which indicates the time when the provider detected the location used to
// establish the time zone).
//
// An alternative would be to use the current time or the providerEvent creation time, but
// this would hinder the ability for the time_zone_detector to judge which suggestions are
// based on newer information when comparing suggestions between different sources.
long effectiveFromElapsedMillis = providerSuggestion.getElapsedRealtimeMillis();
GeolocationTimeZoneSuggestion suggestion =
GeolocationTimeZoneSuggestion.createCertainSuggestion(
effectiveFromElapsedMillis, providerSuggestion.getTimeZoneIds());
String debugInfo = "Provider event received: provider=" + provider
+ ", providerEvent=" + providerEvent
+ ", suggestionCreationTime=" + mEnvironment.elapsedRealtimeMillis();
reportSuggestionEvent(suggestion, debugInfo);
}
@Override
public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) {
synchronized (mSharedLock) {
ipw.println("LocationTimeZoneProviderController:");
ipw.increaseIndent(); // level 1
ipw.println("mCurrentUserConfiguration=" + mCurrentUserConfiguration);
ipw.println("providerInitializationTimeout="
+ mEnvironment.getProviderInitializationTimeout());
ipw.println("providerInitializationTimeoutFuzz="
+ mEnvironment.getProviderInitializationTimeoutFuzz());
ipw.println("uncertaintyDelay=" + mEnvironment.getUncertaintyDelay());
ipw.println("mState=" + mState.get());
ipw.println("mLastEvent=" + mLastEvent);
ipw.println("State history:");
ipw.increaseIndent(); // level 2
mState.dump(ipw);
ipw.decreaseIndent(); // level 2
ipw.println("Primary Provider:");
ipw.increaseIndent(); // level 2
mPrimaryProvider.dump(ipw, args);
ipw.decreaseIndent(); // level 2
ipw.println("Secondary Provider:");
ipw.increaseIndent(); // level 2
mSecondaryProvider.dump(ipw, args);
ipw.decreaseIndent(); // level 2
ipw.decreaseIndent(); // level 1
}
}
/** Clears the uncertainty timeout. */
@GuardedBy("mSharedLock")
private void cancelUncertaintyTimeout() {
mUncertaintyTimeoutQueue.cancel();
}
/**
* Called when a provider has reported it is "uncertain" about the time zone.
*
* <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 start other providers when there are
* multiple ones available. The controller is therefore responsible for deciding when to pass
* the "uncertain" suggestion to the downstream time zone detector.
*
* <p>This method schedules an "uncertainty" timeout (if one isn't already scheduled) to be
* triggered later if nothing else preempts it. It can be preempted if the provider becomes
* certain within {@link Environment#getUncertaintyDelay()}. Preemption causes the scheduled
* "uncertainty" timeout to be cancelled. If the provider repeatedly sends uncertainty events
* within the uncertainty delay period, those events are effectively ignored (i.e. the timeout
* is not reset each time).
*/
@GuardedBy("mSharedLock")
void handleProviderUncertainty(
@NonNull LocationTimeZoneProvider provider,
@ElapsedRealtimeLong long uncertaintyStartedElapsedMillis,
@NonNull String reason) {
Objects.requireNonNull(provider);
// Start the uncertainty timeout if needed to ensure the controller will eventually make an
// uncertain suggestion if no success event arrives in time to counteract it.
if (!mUncertaintyTimeoutQueue.hasQueued()) {
debugLog("Starting uncertainty timeout: reason=" + reason);
Duration uncertaintyDelay = mEnvironment.getUncertaintyDelay();
mUncertaintyTimeoutQueue.runDelayed(
() -> onProviderUncertaintyTimeout(
provider, uncertaintyStartedElapsedMillis, uncertaintyDelay),
uncertaintyDelay.toMillis());
}
if (provider == mPrimaryProvider) {
// (Try to) start the secondary. It could already be started, or enabling might not
// succeed if the provider has previously reported it is perm failed. The uncertainty
// timeout (set above) is used to ensure that an uncertain suggestion will be made if
// the secondary cannot generate a success event in time.
tryStartProvider(mSecondaryProvider, mCurrentUserConfiguration);
}
}
private void onProviderUncertaintyTimeout(
@NonNull LocationTimeZoneProvider provider,
@ElapsedRealtimeLong long uncertaintyStartedElapsedMillis,
@NonNull Duration uncertaintyDelay) {
mThreadingDomain.assertCurrentThread();
synchronized (mSharedLock) {
long afterUncertaintyTimeoutElapsedMillis = mEnvironment.elapsedRealtimeMillis();
setState(STATE_UNCERTAIN);
// For the effectiveFromElapsedMillis suggestion property, use the
// uncertaintyStartedElapsedMillis. This is the time when the provider first reported
// uncertainty, i.e. before the uncertainty timeout.
//
// afterUncertaintyTimeoutElapsedMillis could be used instead, which is the time when
// the location_time_zone_manager finally confirms that the time zone was uncertain,
// but the suggestion property allows the information to be back-dated, which should
// help when comparing suggestions from different sources.
GeolocationTimeZoneSuggestion suggestion =
GeolocationTimeZoneSuggestion.createUncertainSuggestion(
uncertaintyStartedElapsedMillis);
String debugInfo = "Uncertainty timeout triggered for " + provider.getName() + ":"
+ " primary=" + mPrimaryProvider
+ ", secondary=" + mSecondaryProvider
+ ", uncertaintyStarted="
+ Duration.ofMillis(uncertaintyStartedElapsedMillis)
+ ", afterUncertaintyTimeout="
+ Duration.ofMillis(afterUncertaintyTimeoutElapsedMillis)
+ ", uncertaintyDelay=" + uncertaintyDelay;
reportSuggestionEvent(suggestion, debugInfo);
}
}
@GuardedBy("mSharedLock")
@NonNull
private LocationTimeZoneAlgorithmStatus generateCurrentAlgorithmStatus() {
@State String controllerState = mState.get();
ProviderState primaryProviderState = mPrimaryProvider.getCurrentState();
ProviderState secondaryProviderState = mSecondaryProvider.getCurrentState();
return createAlgorithmStatus(controllerState, primaryProviderState, secondaryProviderState);
}
@NonNull
private static LocationTimeZoneAlgorithmStatus createAlgorithmStatus(
@NonNull @State String controllerState,
@NonNull ProviderState primaryProviderState,
@NonNull ProviderState secondaryProviderState) {
@DetectionAlgorithmStatus int algorithmStatus =
mapControllerStateToDetectionAlgorithmStatus(controllerState);
@ProviderStatus int primaryProviderStatus = primaryProviderState.getProviderStatus();
@ProviderStatus int secondaryProviderStatus = secondaryProviderState.getProviderStatus();
// Neither provider is running. The algorithm is not running.
return new LocationTimeZoneAlgorithmStatus(algorithmStatus,
primaryProviderStatus, primaryProviderState.getReportedStatus(),
secondaryProviderStatus, secondaryProviderState.getReportedStatus());
}
/**
* Maps the internal state enum value to one of the status values exposed to the layers above.
*/
private static @DetectionAlgorithmStatus int mapControllerStateToDetectionAlgorithmStatus(
@NonNull @State String controllerState) {
switch (controllerState) {
case STATE_INITIALIZING:
case STATE_PROVIDERS_INITIALIZING:
case STATE_CERTAIN:
case STATE_UNCERTAIN:
return DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING;
case STATE_STOPPED:
case STATE_DESTROYED:
case STATE_FAILED:
case STATE_UNKNOWN:
default:
return DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_RUNNING;
}
}
/**
* Clears recorded controller and provider state changes (for use during tests).
*/
void clearRecordedStates() {
mThreadingDomain.assertCurrentThread();
synchronized (mSharedLock) {
mRecordedStates.clear();
mPrimaryProvider.clearRecordedStates();
mSecondaryProvider.clearRecordedStates();
}
}
/**
* Returns a snapshot of the current controller state for tests.
*/
@NonNull
LocationTimeZoneManagerServiceState getStateForTests() {
mThreadingDomain.assertCurrentThread();
synchronized (mSharedLock) {
LocationTimeZoneManagerServiceState.Builder builder =
new LocationTimeZoneManagerServiceState.Builder();
if (mLastEvent != null) {
builder.setLastEvent(mLastEvent);
}
builder.setControllerState(mState.get())
.setStateChanges(mRecordedStates)
.setPrimaryProviderStateChanges(mPrimaryProvider.getRecordedStates())
.setSecondaryProviderStateChanges(mSecondaryProvider.getRecordedStates());
return builder.build();
}
}
/**
* Used by {@link LocationTimeZoneProviderController} to obtain information from the surrounding
* service. It can easily be faked for tests.
*/
abstract static class Environment {
@NonNull protected final ThreadingDomain mThreadingDomain;
@NonNull protected final Object mSharedLock;
Environment(@NonNull ThreadingDomain threadingDomain) {
mThreadingDomain = Objects.requireNonNull(threadingDomain);
mSharedLock = threadingDomain.getLockObject();
}
/** Destroys the environment, i.e. deregisters listeners, etc. */
abstract void destroy();
/** Returns the {@link ConfigurationInternal} for the current user of the device. */
abstract ConfigurationInternal getCurrentUserConfigurationInternal();
/**
* Returns the value passed to LocationTimeZoneProviders informing them of how long they
* have to return their first time zone suggestion.
*/
abstract Duration getProviderInitializationTimeout();
/**
* Returns the extra time granted on top of {@link #getProviderInitializationTimeout()} to
* allow for slop like communication delays.
*/
abstract Duration getProviderInitializationTimeoutFuzz();
/**
* Returns the value passed to LocationTimeZoneProviders to control rate limiting of
* equivalent events.
*/
abstract Duration getProviderEventFilteringAgeThreshold();
/**
* Returns the delay allowed after receiving uncertainty from a provider before it should be
* passed on.
*/
abstract Duration getUncertaintyDelay();
/**
* Returns the elapsed realtime as millis, the same as {@link
* android.os.SystemClock#elapsedRealtime()}.
*/
abstract @ElapsedRealtimeLong long elapsedRealtimeMillis();
}
/**
* Used by {@link LocationTimeZoneProviderController} to interact with the surrounding service.
* It can easily be faked for tests.
*/
abstract static class Callback {
@NonNull protected final ThreadingDomain mThreadingDomain;
Callback(@NonNull ThreadingDomain threadingDomain) {
mThreadingDomain = Objects.requireNonNull(threadingDomain);
}
/**
* Suggests the latest time zone state for the device.
*/
abstract void sendEvent(@NonNull LocationAlgorithmEvent event);
}
/**
* Used by {@link LocationTimeZoneProviderController} to record events for metrics / telemetry.
*/
interface MetricsLogger {
/** Called when the controller's state changes. */
void onStateChange(@State String stateEnum);
}
}