From d3dadfbd017b4e1e8dcd4b40f44cd2c9d3dd05f1 Mon Sep 17 00:00:00 2001 From: Neil Fuller Date: Mon, 22 Nov 2021 17:46:44 +0000 Subject: [PATCH] Remove base class / rename Controller classes Refactoring change before an upcoming behavior change commit. The LocationTimeZoneProviderController class isn't pulling its weight, so the ControllerImpl has been merged into it. The support classes ControllerCallbackImpl and ControllerEnvironmentImpl classes have been renamed to be prefixed by LocationTimeZone for consistency. Test: build / treehugger Bug: 200279201 Change-Id: I9757edfe878d24546b2e82e5e761aea47e6a0679 --- .../location/ControllerImpl.java | 670 ------------------ .../LocationTimeZoneManagerService.java | 52 +- .../LocationTimeZoneProviderController.java | 657 ++++++++++++++++- ...meZoneProviderControllerCallbackImpl.java} | 7 +- ...oneProviderControllerEnvironmentImpl.java} | 7 +- ...cationTimeZoneProviderControllerTest.java} | 369 +++++----- 6 files changed, 867 insertions(+), 895 deletions(-) delete mode 100644 services/core/java/com/android/server/timezonedetector/location/ControllerImpl.java rename services/core/java/com/android/server/timezonedetector/location/{ControllerCallbackImpl.java => LocationTimeZoneProviderControllerCallbackImpl.java} (82%) rename services/core/java/com/android/server/timezonedetector/location/{ControllerEnvironmentImpl.java => LocationTimeZoneProviderControllerEnvironmentImpl.java} (90%) rename services/tests/servicestests/src/com/android/server/timezonedetector/location/{ControllerImplTest.java => LocationTimeZoneProviderControllerTest.java} (82%) diff --git a/services/core/java/com/android/server/timezonedetector/location/ControllerImpl.java b/services/core/java/com/android/server/timezonedetector/location/ControllerImpl.java deleted file mode 100644 index b9da2ebd36638..0000000000000 --- a/services/core/java/com/android/server/timezonedetector/location/ControllerImpl.java +++ /dev/null @@ -1,670 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.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.service.timezone.TimeZoneProviderEvent; -import android.service.timezone.TimeZoneProviderSuggestion; -import android.util.IndentingPrintWriter; - -import com.android.internal.annotations.GuardedBy; -import com.android.server.timezonedetector.ConfigurationInternal; -import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion; -import com.android.server.timezonedetector.location.ThreadingDomain.SingleRunnableQueue; - -import java.time.Duration; -import java.util.Objects; - -/** - * A real implementation of {@link LocationTimeZoneProviderController} that 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. - */ -class ControllerImpl extends LocationTimeZoneProviderController { - - @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; - - /** Indicates both providers have completed initialization. */ - @GuardedBy("mSharedLock") - private boolean mProvidersInitialized; - - /** - * 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; - - /** Contains the last suggestion actually made, if there is one. */ - @GuardedBy("mSharedLock") - @Nullable - private GeolocationTimeZoneSuggestion mLastSuggestion; - - ControllerImpl(@NonNull ThreadingDomain threadingDomain, - @NonNull LocationTimeZoneProvider primaryProvider, - @NonNull LocationTimeZoneProvider secondaryProvider) { - super(threadingDomain); - mUncertaintyTimeoutQueue = threadingDomain.createSingleRunnableQueue(); - mPrimaryProvider = Objects.requireNonNull(primaryProvider); - mSecondaryProvider = Objects.requireNonNull(secondaryProvider); - } - - @Override - 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 = - ControllerImpl.this::onProviderStateChange; - mPrimaryProvider.initialize(providerListener); - mSecondaryProvider.initialize(providerListener); - mProvidersInitialized = true; - - alterProvidersStartedStateIfRequired( - null /* oldConfiguration */, mCurrentUserConfiguration); - } - } - - @Override - 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. - debugLog("User changed. old=" + oldConfig.getUserId() - + ", new=" + newConfig.getUserId() + ": Stopping providers"); - stopProviders(); - - alterProvidersStartedStateIfRequired(null /* oldConfiguration */, newConfig); - } else { - alterProvidersStartedStateIfRequired(oldConfig, newConfig); - } - } - } - } - - @Override - boolean isUncertaintyTimeoutSet() { - return mUncertaintyTimeoutQueue.hasQueued(); - } - - @Override - @DurationMillisLong - long getUncertaintyTimeoutDelayMillis() { - return mUncertaintyTimeoutQueue.getQueuedDelayMillis(); - } - - @Override - void destroy() { - mThreadingDomain.assertCurrentThread(); - - synchronized (mSharedLock) { - stopProviders(); - mPrimaryProvider.destroy(); - mSecondaryProvider.destroy(); - } - } - - @GuardedBy("mSharedLock") - private void stopProviders() { - stopProviderIfStarted(mPrimaryProvider); - stopProviderIfStarted(mSecondaryProvider); - - // By definition, if both providers are stopped, the controller is uncertain. - 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 providers are - // re-started). - if (mLastSuggestion != null && mLastSuggestion.getZoneIds() != null) { - GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion( - mEnvironment.elapsedRealtimeMillis(), "Providers are stopping"); - makeSuggestion(suggestion); - } - } - - @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. - * - *

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 oldGeoDetectionEnabled = oldConfiguration != null - && oldConfiguration.getGeoDetectionEnabledBehavior(); - boolean newGeoDetectionEnabled = newConfiguration.getGeoDetectionEnabledBehavior(); - if (oldGeoDetectionEnabled == newGeoDetectionEnabled) { - 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 (newGeoDetectionEnabled) { - // 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 - // becomes uncertain. - GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion( - mEnvironment.elapsedRealtimeMillis(), - "Providers are failed:" - + " primary=" + mPrimaryProvider.getCurrentState() - + " secondary=" + mPrimaryProvider.getCurrentState()); - makeSuggestion(suggestion); - } - } - } else { - stopProviders(); - } - } - - @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 (!mProvidersInitialized) { - 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 future. - GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion( - mEnvironment.elapsedRealtimeMillis(), - "Both providers are terminated:" - + " primary=" + primaryCurrentState.provider - + ", secondary=" + secondaryCurrentState.provider); - makeSuggestion(suggestion); - } - } - - /** - * 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.getGeoDetectionEnabledBehavior()) { - // This should not happen: the provider should not be in an started state if the user - // does not have geodetection 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(); - - // 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 geoSuggestion = - GeolocationTimeZoneSuggestion.createCertainSuggestion( - effectiveFromElapsedMillis, providerSuggestion.getTimeZoneIds()); - - String debugInfo = "Event received provider=" + provider - + ", providerEvent=" + providerEvent - + ", suggestionCreationTime=" + mEnvironment.elapsedRealtimeMillis(); - geoSuggestion.addDebugInfo(debugInfo); - makeSuggestion(geoSuggestion); - } - - @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("mLastSuggestion=" + mLastSuggestion); - - 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 - } - } - - /** Sends an immediate suggestion, updating mLastSuggestion. */ - @GuardedBy("mSharedLock") - private void makeSuggestion(@NonNull GeolocationTimeZoneSuggestion suggestion) { - debugLog("makeSuggestion: suggestion=" + suggestion); - mCallback.suggest(suggestion); - mLastSuggestion = suggestion; - } - - /** Clears the uncertainty timeout. */ - @GuardedBy("mSharedLock") - private void cancelUncertaintyTimeout() { - mUncertaintyTimeoutQueue.cancel(); - } - - /** - * Called when a provider has become "uncertain" about the time zone. - * - *

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 make a - * "uncertain" suggestion to the downstream time zone detector. - * - *

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 (or does anything else that calls {@link - * #makeSuggestion(GeolocationTimeZoneSuggestion)}) 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(); - - // 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 = createUncertainSuggestion( - uncertaintyStartedElapsedMillis, - "Uncertainty timeout triggered for " + provider.getName() + ":" - + " primary=" + mPrimaryProvider - + ", secondary=" + mSecondaryProvider - + ", uncertaintyStarted=" - + Duration.ofMillis(uncertaintyStartedElapsedMillis) - + ", afterUncertaintyTimeout=" - + Duration.ofMillis(afterUncertaintyTimeoutElapsedMillis) - + ", uncertaintyDelay=" + uncertaintyDelay - ); - makeSuggestion(suggestion); - } - } - - @NonNull - private static GeolocationTimeZoneSuggestion createUncertainSuggestion( - @ElapsedRealtimeLong long effectiveFromElapsedMillis, - @NonNull String reason) { - GeolocationTimeZoneSuggestion suggestion = - GeolocationTimeZoneSuggestion.createUncertainSuggestion( - effectiveFromElapsedMillis); - suggestion.addDebugInfo(reason); - return suggestion; - } - - /** - * Clears recorded provider state changes (for use during tests). - */ - void clearRecordedProviderStates() { - mThreadingDomain.assertCurrentThread(); - - synchronized (mSharedLock) { - 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 (mLastSuggestion != null) { - builder.setLastSuggestion(mLastSuggestion); - } - builder.setPrimaryProviderStateChanges(mPrimaryProvider.getRecordedStates()) - .setSecondaryProviderStateChanges(mSecondaryProvider.getRecordedStates()); - return builder.build(); - } - } -} diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java index ddbeac4e458aa..af8cf6e05a840 100644 --- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java +++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java @@ -147,16 +147,16 @@ public class LocationTimeZoneManagerService extends Binder { /** The shared lock from {@link #mThreadingDomain}. */ @NonNull private final Object mSharedLock; - @NonNull - private final ServiceConfigAccessor mServiceConfigAccessor; + @NonNull private final ServiceConfigAccessor mServiceConfigAccessor; // Lazily initialized. Can be null if the service has been stopped. @GuardedBy("mSharedLock") - private ControllerImpl mLocationTimeZoneDetectorController; + private LocationTimeZoneProviderController mLocationTimeZoneProviderController; // Lazily initialized. Can be null if the service has been stopped. @GuardedBy("mSharedLock") - private ControllerEnvironmentImpl mEnvironment; + private LocationTimeZoneProviderControllerEnvironmentImpl + mLocationTimeZoneProviderControllerEnvironment; LocationTimeZoneManagerService(@NonNull Context context, @NonNull ServiceConfigAccessor serviceConfigAccessor) { @@ -190,7 +190,7 @@ public class LocationTimeZoneManagerService extends Binder { // Avoid starting the service if it is currently stopped. This is required because // server flags are used by tests to set behavior with the service stopped, and we don't // want the service being restarted after each flag is set. - if (mLocationTimeZoneDetectorController != null) { + if (mLocationTimeZoneProviderController != null) { // Stop and start the service, waiting until completion. stopOnDomainThread(); startOnDomainThread(); @@ -278,19 +278,22 @@ public class LocationTimeZoneManagerService extends Binder { return; } - if (mLocationTimeZoneDetectorController == null) { + if (mLocationTimeZoneProviderController == null) { LocationTimeZoneProvider primary = mPrimaryProviderConfig.createProvider(); LocationTimeZoneProvider secondary = mSecondaryProviderConfig.createProvider(); - ControllerImpl controller = - new ControllerImpl(mThreadingDomain, primary, secondary); - ControllerEnvironmentImpl environment = new ControllerEnvironmentImpl( - mThreadingDomain, mServiceConfigAccessor, controller); - ControllerCallbackImpl callback = new ControllerCallbackImpl(mThreadingDomain); + LocationTimeZoneProviderController controller = + new LocationTimeZoneProviderController( + mThreadingDomain, primary, secondary); + LocationTimeZoneProviderControllerEnvironmentImpl environment = + new LocationTimeZoneProviderControllerEnvironmentImpl( + mThreadingDomain, mServiceConfigAccessor, controller); + LocationTimeZoneProviderControllerCallbackImpl callback = + new LocationTimeZoneProviderControllerCallbackImpl(mThreadingDomain); controller.initialize(environment, callback); - mEnvironment = environment; - mLocationTimeZoneDetectorController = controller; + mLocationTimeZoneProviderControllerEnvironment = environment; + mLocationTimeZoneProviderController = controller; } } } @@ -312,11 +315,11 @@ public class LocationTimeZoneManagerService extends Binder { mThreadingDomain.assertCurrentThread(); synchronized (mSharedLock) { - if (mLocationTimeZoneDetectorController != null) { - mLocationTimeZoneDetectorController.destroy(); - mLocationTimeZoneDetectorController = null; - mEnvironment.destroy(); - mEnvironment = null; + if (mLocationTimeZoneProviderController != null) { + mLocationTimeZoneProviderController.destroy(); + mLocationTimeZoneProviderController = null; + mLocationTimeZoneProviderControllerEnvironment.destroy(); + mLocationTimeZoneProviderControllerEnvironment = null; // Clear test state so it won't be used the next time the service is started. mServiceConfigAccessor.resetVolatileTestConfig(); @@ -338,8 +341,8 @@ public class LocationTimeZoneManagerService extends Binder { mThreadingDomain.postAndWait(() -> { synchronized (mSharedLock) { - if (mLocationTimeZoneDetectorController != null) { - mLocationTimeZoneDetectorController.clearRecordedProviderStates(); + if (mLocationTimeZoneProviderController != null) { + mLocationTimeZoneProviderController.clearRecordedProviderStates(); } } }, BLOCKING_OP_WAIT_DURATION_MILLIS); @@ -357,10 +360,10 @@ public class LocationTimeZoneManagerService extends Binder { return mThreadingDomain.postAndWait( () -> { synchronized (mSharedLock) { - if (mLocationTimeZoneDetectorController == null) { + if (mLocationTimeZoneProviderController == null) { return null; } - return mLocationTimeZoneDetectorController.getStateForTests(); + return mLocationTimeZoneProviderController.getStateForTests(); } }, BLOCKING_OP_WAIT_DURATION_MILLIS); @@ -390,10 +393,10 @@ public class LocationTimeZoneManagerService extends Binder { mSecondaryProviderConfig.dump(ipw, args); ipw.decreaseIndent(); - if (mLocationTimeZoneDetectorController == null) { + if (mLocationTimeZoneProviderController == null) { ipw.println("{Stopped}"); } else { - mLocationTimeZoneDetectorController.dump(ipw, args); + mLocationTimeZoneProviderController.dump(ipw, args); } ipw.decreaseIndent(); } @@ -450,7 +453,6 @@ public class LocationTimeZoneManagerService extends Binder { mServiceConfigAccessor.getRecordProviderStateChanges()); } - @GuardedBy("mSharedLock") @Override public void dump(IndentingPrintWriter ipw, String[] args) { ipw.printf("getMode()=%s\n", getMode()); diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java index 4dff02e8ab6ff..dbd3877debfeb 100644 --- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java +++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java @@ -16,31 +16,57 @@ 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.os.Handler; +import android.annotation.Nullable; +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.location.LocationTimeZoneProvider.ProviderState; +import com.android.server.timezonedetector.location.ThreadingDomain.SingleRunnableQueue; import java.time.Duration; import java.util.Objects; /** - * An base class for the component responsible handling events from {@link - * LocationTimeZoneProvider}s and synthesizing time zone ID suggestions for sending to the time zone - * detector. This interface primarily exists to extract testable detection logic, i.e. with - * a minimal number of threading considerations or dependencies on Android infrastructure. + * The component responsible handling events from {@link LocationTimeZoneProvider}s and synthesizing + * time zone ID suggestions for sending to the time zone detector. + * + *

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. + * + *

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

The controller interacts with the following components: *