From 040dc8e2cdb7516a6487b08ad12b0b4f803e464a Mon Sep 17 00:00:00 2001 From: Neil Fuller Date: Tue, 26 Jan 2021 20:18:01 +0000 Subject: Move code from c.a.server.location.timezone Move code from com.android.server.location.timezone to com.android.server.timezonedetector.location. It has more in common with time zone detection than location code. Bug: 175119330 Test: Treehugger Change-Id: I80e8c88ba1e3903f3b662cb195c5b2877ba661b0 --- .../timezone/BinderLocationTimeZoneProvider.java | 212 ---- .../location/timezone/ControllerCallbackImpl.java | 43 - .../timezone/ControllerEnvironmentImpl.java | 83 -- .../server/location/timezone/ControllerImpl.java | 673 ---------- .../location/timezone/HandlerThreadingDomain.java | 110 -- .../timezone/LocationTimeZoneManagerService.java | 491 -------- .../LocationTimeZoneManagerServiceState.java | 97 -- .../LocationTimeZoneManagerShellCommand.java | 330 ----- .../timezone/LocationTimeZoneProvider.java | 740 ----------- .../LocationTimeZoneProviderController.java | 158 --- .../timezone/LocationTimeZoneProviderProxy.java | 150 --- .../NullLocationTimeZoneProviderProxy.java | 86 -- .../RealLocationTimeZoneProviderProxy.java | 244 ---- .../SimulatedLocationTimeZoneProviderProxy.java | 211 ---- .../server/location/timezone/TestCommand.java | 194 --- .../server/location/timezone/ThreadingDomain.java | 190 --- .../location/timezone/TimeZoneProviderEvent.java | 143 --- .../location/timezone/TimeZoneProviderRequest.java | 105 -- .../location/BinderLocationTimeZoneProvider.java | 212 ++++ .../location/ControllerCallbackImpl.java | 43 + .../location/ControllerEnvironmentImpl.java | 83 ++ .../timezonedetector/location/ControllerImpl.java | 673 ++++++++++ .../location/HandlerThreadingDomain.java | 110 ++ .../location/LocationTimeZoneManagerService.java | 491 ++++++++ .../LocationTimeZoneManagerServiceState.java | 97 ++ .../LocationTimeZoneManagerShellCommand.java | 330 +++++ .../location/LocationTimeZoneProvider.java | 740 +++++++++++ .../LocationTimeZoneProviderController.java | 158 +++ .../location/LocationTimeZoneProviderProxy.java | 150 +++ .../NullLocationTimeZoneProviderProxy.java | 86 ++ .../RealLocationTimeZoneProviderProxy.java | 244 ++++ .../SimulatedLocationTimeZoneProviderProxy.java | 211 ++++ .../timezonedetector/location/TestCommand.java | 194 +++ .../timezonedetector/location/ThreadingDomain.java | 190 +++ .../location/TimeZoneProviderEvent.java | 143 +++ .../location/TimeZoneProviderRequest.java | 105 ++ services/java/com/android/server/SystemServer.java | 2 +- .../location/timezone/ControllerImplTest.java | 1316 -------------------- .../timezone/HandlerThreadingDomainTest.java | 260 ---- .../timezone/LocationTimeZoneProviderTest.java | 311 ----- .../server/location/timezone/TestSupport.java | 54 - .../location/timezone/TestThreadingDomain.java | 145 --- .../location/ControllerImplTest.java | 1314 +++++++++++++++++++ .../location/HandlerThreadingDomainTest.java | 260 ++++ .../location/LocationTimeZoneProviderTest.java | 311 +++++ .../timezonedetector/location/TestSupport.java | 54 + .../location/TestThreadingDomain.java | 145 +++ 47 files changed, 6345 insertions(+), 6347 deletions(-) delete mode 100644 services/core/java/com/android/server/location/timezone/BinderLocationTimeZoneProvider.java delete mode 100644 services/core/java/com/android/server/location/timezone/ControllerCallbackImpl.java delete mode 100644 services/core/java/com/android/server/location/timezone/ControllerEnvironmentImpl.java delete mode 100644 services/core/java/com/android/server/location/timezone/ControllerImpl.java delete mode 100644 services/core/java/com/android/server/location/timezone/HandlerThreadingDomain.java delete mode 100644 services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerService.java delete mode 100644 services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerServiceState.java delete mode 100644 services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerShellCommand.java delete mode 100644 services/core/java/com/android/server/location/timezone/LocationTimeZoneProvider.java delete mode 100644 services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderController.java delete mode 100644 services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderProxy.java delete mode 100644 services/core/java/com/android/server/location/timezone/NullLocationTimeZoneProviderProxy.java delete mode 100644 services/core/java/com/android/server/location/timezone/RealLocationTimeZoneProviderProxy.java delete mode 100644 services/core/java/com/android/server/location/timezone/SimulatedLocationTimeZoneProviderProxy.java delete mode 100644 services/core/java/com/android/server/location/timezone/TestCommand.java delete mode 100644 services/core/java/com/android/server/location/timezone/ThreadingDomain.java delete mode 100644 services/core/java/com/android/server/location/timezone/TimeZoneProviderEvent.java delete mode 100644 services/core/java/com/android/server/location/timezone/TimeZoneProviderRequest.java create mode 100644 services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java create mode 100644 services/core/java/com/android/server/timezonedetector/location/ControllerCallbackImpl.java create mode 100644 services/core/java/com/android/server/timezonedetector/location/ControllerEnvironmentImpl.java create mode 100644 services/core/java/com/android/server/timezonedetector/location/ControllerImpl.java create mode 100644 services/core/java/com/android/server/timezonedetector/location/HandlerThreadingDomain.java create mode 100644 services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java create mode 100644 services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerServiceState.java create mode 100644 services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerShellCommand.java create mode 100644 services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java create mode 100644 services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java create mode 100644 services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderProxy.java create mode 100644 services/core/java/com/android/server/timezonedetector/location/NullLocationTimeZoneProviderProxy.java create mode 100644 services/core/java/com/android/server/timezonedetector/location/RealLocationTimeZoneProviderProxy.java create mode 100644 services/core/java/com/android/server/timezonedetector/location/SimulatedLocationTimeZoneProviderProxy.java create mode 100644 services/core/java/com/android/server/timezonedetector/location/TestCommand.java create mode 100644 services/core/java/com/android/server/timezonedetector/location/ThreadingDomain.java create mode 100644 services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderEvent.java create mode 100644 services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderRequest.java delete mode 100644 services/tests/servicestests/src/com/android/server/location/timezone/ControllerImplTest.java delete mode 100644 services/tests/servicestests/src/com/android/server/location/timezone/HandlerThreadingDomainTest.java delete mode 100644 services/tests/servicestests/src/com/android/server/location/timezone/LocationTimeZoneProviderTest.java delete mode 100644 services/tests/servicestests/src/com/android/server/location/timezone/TestSupport.java delete mode 100644 services/tests/servicestests/src/com/android/server/location/timezone/TestThreadingDomain.java create mode 100644 services/tests/servicestests/src/com/android/server/timezonedetector/location/ControllerImplTest.java create mode 100644 services/tests/servicestests/src/com/android/server/timezonedetector/location/HandlerThreadingDomainTest.java create mode 100644 services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java create mode 100644 services/tests/servicestests/src/com/android/server/timezonedetector/location/TestSupport.java create mode 100644 services/tests/servicestests/src/com/android/server/timezonedetector/location/TestThreadingDomain.java diff --git a/services/core/java/com/android/server/location/timezone/BinderLocationTimeZoneProvider.java b/services/core/java/com/android/server/location/timezone/BinderLocationTimeZoneProvider.java deleted file mode 100644 index 210fb5c0a1ab..000000000000 --- a/services/core/java/com/android/server/location/timezone/BinderLocationTimeZoneProvider.java +++ /dev/null @@ -1,212 +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.location.timezone; - -import static com.android.server.location.timezone.LocationTimeZoneManagerService.debugLog; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_CERTAIN; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_UNCERTAIN; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STOPPED; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.os.RemoteCallback; -import android.util.IndentingPrintWriter; - -import java.time.Duration; -import java.util.Objects; - -/** - * The real, system-server side implementation of a binder call backed {@link - * LocationTimeZoneProvider}. It handles keeping track of current state, timeouts and ensuring - * events are passed to the {@link LocationTimeZoneProviderController} on the required thread. - */ -class BinderLocationTimeZoneProvider extends LocationTimeZoneProvider { - - private static final String TAG = LocationTimeZoneManagerService.TAG; - - @NonNull private final LocationTimeZoneProviderProxy mProxy; - - BinderLocationTimeZoneProvider( - @NonNull ThreadingDomain threadingDomain, - @NonNull String providerName, - @NonNull LocationTimeZoneProviderProxy proxy) { - super(threadingDomain, providerName); - mProxy = Objects.requireNonNull(proxy); - } - - @Override - void onInitialize() { - mProxy.initialize(new LocationTimeZoneProviderProxy.Listener() { - @Override - public void onReportTimeZoneProviderEvent( - @NonNull TimeZoneProviderEvent timeZoneProviderEvent) { - handleTimeZoneProviderEvent(timeZoneProviderEvent); - } - - @Override - public void onProviderBound() { - handleOnProviderBound(); - } - - @Override - public void onProviderUnbound() { - handleProviderLost("onProviderUnbound()"); - } - }); - } - - @Override - void onDestroy() { - mProxy.destroy(); - } - - private void handleProviderLost(String reason) { - mThreadingDomain.assertCurrentThread(); - - synchronized (mSharedLock) { - ProviderState currentState = mCurrentState.get(); - switch (currentState.stateEnum) { - case PROVIDER_STATE_STARTED_INITIALIZING: - case PROVIDER_STATE_STARTED_UNCERTAIN: - case PROVIDER_STATE_STARTED_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_STARTED_UNCERTAIN state because - // event == null - ProviderState newState = currentState.newState( - PROVIDER_STATE_STARTED_UNCERTAIN, null, - currentState.currentUserConfiguration, msg); - setCurrentState(newState, true); - break; - } - case PROVIDER_STATE_STOPPED: { - debugLog("handleProviderLost reason=" + reason - + ", mProviderName=" + mProviderName - + ", currentState=" + currentState - + ": No state change required, provider is stopped."); - break; - } - case PROVIDER_STATE_PERM_FAILED: - case PROVIDER_STATE_DESTROYED: { - debugLog("handleProviderLost reason=" + reason - + ", mProviderName=" + mProviderName - + ", currentState=" + currentState - + ": No state change required, provider is terminated."); - break; - } - default: { - throw new IllegalStateException("Unknown currentState=" + currentState); - } - } - } - } - - private void handleOnProviderBound() { - mThreadingDomain.assertCurrentThread(); - - synchronized (mSharedLock) { - ProviderState currentState = mCurrentState.get(); - switch (currentState.stateEnum) { - case PROVIDER_STATE_STARTED_INITIALIZING: - case PROVIDER_STATE_STARTED_CERTAIN: - case PROVIDER_STATE_STARTED_UNCERTAIN: { - debugLog("handleOnProviderBound mProviderName=" + mProviderName - + ", currentState=" + currentState + ": Provider is started."); - break; - } - case PROVIDER_STATE_STOPPED: { - debugLog("handleOnProviderBound mProviderName=" + mProviderName - + ", currentState=" + currentState + ": Provider is stopped."); - break; - } - case PROVIDER_STATE_PERM_FAILED: - case PROVIDER_STATE_DESTROYED: { - debugLog("handleOnProviderBound" - + ", mProviderName=" + mProviderName - + ", currentState=" + currentState - + ": No state change required, provider is terminated."); - break; - } - default: { - throw new IllegalStateException("Unknown currentState=" + currentState); - } - } - } - } - - @Override - void onStartUpdates(@NonNull Duration initializationTimeout) { - // Set a request on the proxy - it will be sent immediately if the service is bound, - // or will be sent as soon as the service becomes bound. - TimeZoneProviderRequest request = - TimeZoneProviderRequest.createStartUpdatesRequest(initializationTimeout); - mProxy.setRequest(request); - } - - @Override - void onStopUpdates() { - TimeZoneProviderRequest request = TimeZoneProviderRequest.createStopUpdatesRequest(); - mProxy.setRequest(request); - } - - /** - * Passes the supplied test command to the current proxy. - */ - @Override - void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) { - mThreadingDomain.assertCurrentThread(); - - mProxy.handleTestCommand(testCommand, callback); - } - - @Override - public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) { - synchronized (mSharedLock) { - ipw.println("{BinderLocationTimeZoneProvider}"); - ipw.println("mProviderName=" + mProviderName); - ipw.println("mCurrentState=" + mCurrentState); - ipw.println("mProxy=" + mProxy); - - ipw.println("State history:"); - ipw.increaseIndent(); - mCurrentState.dump(ipw); - ipw.decreaseIndent(); - - ipw.println("Proxy details:"); - ipw.increaseIndent(); - mProxy.dump(ipw, args); - ipw.decreaseIndent(); - } - } - - @Override - public String toString() { - synchronized (mSharedLock) { - return "BinderLocationTimeZoneProvider{" - + "mProviderName=" + mProviderName - + ", mCurrentState=" + mCurrentState - + ", mProxy=" + mProxy - + '}'; - } - } -} diff --git a/services/core/java/com/android/server/location/timezone/ControllerCallbackImpl.java b/services/core/java/com/android/server/location/timezone/ControllerCallbackImpl.java deleted file mode 100644 index cd9aa2fd6a5b..000000000000 --- a/services/core/java/com/android/server/location/timezone/ControllerCallbackImpl.java +++ /dev/null @@ -1,43 +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.location.timezone; - -import android.annotation.NonNull; - -import com.android.server.LocalServices; -import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion; -import com.android.server.timezonedetector.TimeZoneDetectorInternal; - -/** - * The real implementation of {@link LocationTimeZoneProviderController.Callback} used by - * {@link ControllerImpl} to interact with other server components. - */ -class ControllerCallbackImpl extends LocationTimeZoneProviderController.Callback { - - ControllerCallbackImpl(@NonNull ThreadingDomain threadingDomain) { - super(threadingDomain); - } - - @Override - void suggest(@NonNull GeolocationTimeZoneSuggestion suggestion) { - mThreadingDomain.assertCurrentThread(); - - TimeZoneDetectorInternal timeZoneDetector = - LocalServices.getService(TimeZoneDetectorInternal.class); - timeZoneDetector.suggestGeolocationTimeZone(suggestion); - } -} diff --git a/services/core/java/com/android/server/location/timezone/ControllerEnvironmentImpl.java b/services/core/java/com/android/server/location/timezone/ControllerEnvironmentImpl.java deleted file mode 100644 index d896f6e441d8..000000000000 --- a/services/core/java/com/android/server/location/timezone/ControllerEnvironmentImpl.java +++ /dev/null @@ -1,83 +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.location.timezone; - -import android.annotation.NonNull; - -import com.android.server.LocalServices; -import com.android.server.timezonedetector.ConfigurationChangeListener; -import com.android.server.timezonedetector.ConfigurationInternal; -import com.android.server.timezonedetector.TimeZoneDetectorInternal; - -import java.time.Duration; -import java.util.Objects; - -/** - * The real implementation of {@link LocationTimeZoneProviderController.Environment} used by - * {@link ControllerImpl} to interact with other server components. - */ -class ControllerEnvironmentImpl extends LocationTimeZoneProviderController.Environment { - - private static final Duration PROVIDER_INITIALIZATION_TIMEOUT = Duration.ofMinutes(5); - private static final Duration PROVIDER_INITIALIZATION_TIMEOUT_FUZZ = Duration.ofMinutes(1); - private static final Duration PROVIDER_UNCERTAINTY_DELAY = Duration.ofMinutes(5); - - @NonNull private final TimeZoneDetectorInternal mTimeZoneDetectorInternal; - @NonNull private final LocationTimeZoneProviderController mController; - @NonNull private final ConfigurationChangeListener mConfigurationChangeListener; - - ControllerEnvironmentImpl(@NonNull ThreadingDomain threadingDomain, - @NonNull LocationTimeZoneProviderController controller) { - super(threadingDomain); - mController = Objects.requireNonNull(controller); - mTimeZoneDetectorInternal = LocalServices.getService(TimeZoneDetectorInternal.class); - - // Listen for configuration changes. - mConfigurationChangeListener = () -> mThreadingDomain.post(mController::onConfigChanged); - mTimeZoneDetectorInternal.addConfigurationListener(mConfigurationChangeListener); - } - - - @Override - void destroy() { - mTimeZoneDetectorInternal.removeConfigurationListener(mConfigurationChangeListener); - } - - @Override - @NonNull - ConfigurationInternal getCurrentUserConfigurationInternal() { - return mTimeZoneDetectorInternal.getCurrentUserConfigurationInternal(); - } - - @Override - @NonNull - Duration getProviderInitializationTimeout() { - return PROVIDER_INITIALIZATION_TIMEOUT; - } - - @Override - @NonNull - Duration getProviderInitializationTimeoutFuzz() { - return PROVIDER_INITIALIZATION_TIMEOUT_FUZZ; - } - - @Override - @NonNull - Duration getUncertaintyDelay() { - return PROVIDER_UNCERTAINTY_DELAY; - } -} diff --git a/services/core/java/com/android/server/location/timezone/ControllerImpl.java b/services/core/java/com/android/server/location/timezone/ControllerImpl.java deleted file mode 100644 index 0d284fc2de4f..000000000000 --- a/services/core/java/com/android/server/location/timezone/ControllerImpl.java +++ /dev/null @@ -1,673 +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.location.timezone; - -import static com.android.server.location.timezone.LocationTimeZoneManagerService.debugLog; -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_DESTROYED; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_CERTAIN; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_UNCERTAIN; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STOPPED; -import static com.android.server.location.timezone.TimeZoneProviderEvent.EVENT_TYPE_PERMANENT_FAILURE; -import static com.android.server.location.timezone.TimeZoneProviderEvent.EVENT_TYPE_SUGGESTION; -import static com.android.server.location.timezone.TimeZoneProviderEvent.EVENT_TYPE_UNCERTAIN; - -import android.annotation.DurationMillisLong; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.os.RemoteCallback; -import android.util.IndentingPrintWriter; - -import com.android.internal.annotations.GuardedBy; -import com.android.server.location.timezone.ThreadingDomain.SingleRunnableQueue; -import com.android.server.timezonedetector.ConfigurationInternal; -import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion; - -import java.time.Duration; -import java.util.List; -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 onConfigChanged() { - 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(); - - // If the controller has made a "certain" suggestion, it should make an uncertain - // suggestion to cancel it. - if (mLastSuggestion != null && mLastSuggestion.getZoneIds() != null) { - makeSuggestion(createUncertainSuggestion("Controller is destroyed")); - } - } - } - - @GuardedBy("mSharedLock") - private void stopProviders() { - stopProviderIfStarted(mPrimaryProvider); - stopProviderIfStarted(mSecondaryProvider); - - // By definition, if both providers are stopped, the controller is uncertain. - cancelUncertaintyTimeout(); - } - - @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( - "Providers are failed:" - + " primary=" + mPrimaryProvider.getCurrentState() - + " secondary=" + mPrimaryProvider.getCurrentState()); - makeSuggestion(suggestion); - } - } - } else { - stopProviders(); - - // 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-started). - if (mLastSuggestion != null && mLastSuggestion.getZoneIds() != null) { - GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion( - "Provider is stopped:" - + " primary=" + mPrimaryProvider.getCurrentState()); - makeSuggestion(suggestion); - } - } - } - - 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()); - 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( - "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. - handleProviderUncertainty(provider, "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: { - handleProviderUncertainty(provider, "provider=" + provider - + ", explicit uncertainty. event=" + event); - break; - } - case EVENT_TYPE_SUGGESTION: { - handleProviderSuggestion(provider, event.getSuggestion().getTimeZoneIds(), - "Event received provider=" + provider + ", event=" + 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, - @Nullable List timeZoneIds, - @NonNull String reason) { - // By definition, the controller is now certain. - cancelUncertaintyTimeout(); - - if (provider == mPrimaryProvider) { - stopProviderIfStarted(mSecondaryProvider); - } - - GeolocationTimeZoneSuggestion suggestion = new GeolocationTimeZoneSuggestion(timeZoneIds); - suggestion.addDebugInfo(reason); - // Rely on the receiver to dedupe suggestions. It is better to over-communicate. - makeSuggestion(suggestion); - } - - @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, @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 delay = mEnvironment.getUncertaintyDelay(); - mUncertaintyTimeoutQueue.runDelayed(() -> onProviderUncertaintyTimeout(provider), - delay.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) { - mThreadingDomain.assertCurrentThread(); - - synchronized (mSharedLock) { - GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion( - "Uncertainty timeout triggered for " + provider.getName() + ":" - + " primary=" + mPrimaryProvider - + ", secondary=" + mSecondaryProvider); - makeSuggestion(suggestion); - } - } - - @NonNull - private static GeolocationTimeZoneSuggestion createUncertainSuggestion(@NonNull String reason) { - GeolocationTimeZoneSuggestion suggestion = new GeolocationTimeZoneSuggestion(null); - suggestion.addDebugInfo(reason); - return suggestion; - } - - /** - * Passes a test command to the specified provider. If the provider name does not match a - * known provider, then the command is logged and discarded. - */ - void handleProviderTestCommand( - @NonNull String providerName, @NonNull TestCommand testCommand, - @Nullable RemoteCallback callback) { - mThreadingDomain.assertCurrentThread(); - - LocationTimeZoneProvider targetProvider = getLocationTimeZoneProvider(providerName); - if (targetProvider == null) { - warnLog("Unable to process test command:" - + " providerName=" + providerName + ", testCommand=" + testCommand); - return; - } - - synchronized (mSharedLock) { - try { - targetProvider.handleTestCommand(testCommand, callback); - } catch (Exception e) { - warnLog("Unable to process test command:" - + " providerName=" + providerName + ", testCommand=" + testCommand, e); - } - } - } - - /** - * Sets whether the controller should record provider state changes for later dumping via - * {@link #getStateForTests()}. - */ - void setProviderStateRecordingEnabled(boolean enabled) { - mThreadingDomain.assertCurrentThread(); - - synchronized (mSharedLock) { - mPrimaryProvider.setStateChangeRecordingEnabled(enabled); - mSecondaryProvider.setStateChangeRecordingEnabled(enabled); - } - } - - /** - * 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(); - } - } - - @Nullable - private LocationTimeZoneProvider getLocationTimeZoneProvider(@NonNull String providerName) { - LocationTimeZoneProvider targetProvider; - if (Objects.equals(mPrimaryProvider.getName(), providerName)) { - targetProvider = mPrimaryProvider; - } else if (Objects.equals(mSecondaryProvider.getName(), providerName)) { - targetProvider = mSecondaryProvider; - } else { - warnLog("Bad providerName=" + providerName); - targetProvider = null; - } - return targetProvider; - } -} diff --git a/services/core/java/com/android/server/location/timezone/HandlerThreadingDomain.java b/services/core/java/com/android/server/location/timezone/HandlerThreadingDomain.java deleted file mode 100644 index 3055ff8a2b59..000000000000 --- a/services/core/java/com/android/server/location/timezone/HandlerThreadingDomain.java +++ /dev/null @@ -1,110 +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.location.timezone; - -import android.annotation.DurationMillisLong; -import android.annotation.NonNull; -import android.os.Handler; - -import java.util.Objects; -import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -/** - * The real implementation of {@link ThreadingDomain} that uses a {@link Handler}. - */ -final class HandlerThreadingDomain extends ThreadingDomain { - - @NonNull private final Handler mHandler; - - HandlerThreadingDomain(Handler handler) { - mHandler = Objects.requireNonNull(handler); - } - - /** - * Returns the {@link Handler} associated with this threading domain. The same {@link Handler} - * may be associated with multiple threading domains, e.g. multiple threading domains could - * choose to use the {@link com.android.server.FgThread} handler. - * - *

If you find yourself making this public because you need a {@link Handler}, then it may - * cause problems with testability. Try to avoid using this method and use methods like {@link - * #post(Runnable)} instead. - */ - @NonNull - Handler getHandler() { - return mHandler; - } - - @NonNull - Thread getThread() { - return getHandler().getLooper().getThread(); - } - - @Override - void post(@NonNull Runnable r) { - getHandler().post(r); - } - - @Override - V postAndWait(@NonNull Callable callable, @DurationMillisLong long durationMillis) - throws Exception { - // Calling this on this domain's thread would lead to deadlock. - assertNotCurrentThread(); - - AtomicReference resultReference = new AtomicReference<>(); - AtomicReference exceptionReference = new AtomicReference<>(); - CountDownLatch latch = new CountDownLatch(1); - post(() -> { - try { - resultReference.set(callable.call()); - } catch (Exception e) { - exceptionReference.set(e); - } finally { - latch.countDown(); - } - }); - - try { - if (!latch.await(durationMillis, TimeUnit.MILLISECONDS)) { - throw new RuntimeException("Timed out"); - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - if (exceptionReference.get() != null) { - throw exceptionReference.get(); - } - return resultReference.get(); - } - - @Override - void postDelayed(@NonNull Runnable r, @DurationMillisLong long delayMillis) { - getHandler().postDelayed(r, delayMillis); - } - - @Override - void postDelayed(Runnable r, Object token, @DurationMillisLong long delayMillis) { - getHandler().postDelayed(r, token, delayMillis); - } - - @Override - void removeQueuedRunnables(Object token) { - getHandler().removeCallbacksAndMessages(token); - } -} diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerService.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerService.java deleted file mode 100644 index 54535eb50130..000000000000 --- a/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerService.java +++ /dev/null @@ -1,491 +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.location.timezone; - -import static android.app.time.LocationTimeZoneManager.PRIMARY_PROVIDER_NAME; -import static android.app.time.LocationTimeZoneManager.PROVIDER_MODE_OVERRIDE_DISABLED; -import static android.app.time.LocationTimeZoneManager.PROVIDER_MODE_OVERRIDE_NONE; -import static android.app.time.LocationTimeZoneManager.PROVIDER_MODE_OVERRIDE_SIMULATED; -import static android.app.time.LocationTimeZoneManager.SECONDARY_PROVIDER_NAME; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; -import android.content.res.Resources; -import android.os.Binder; -import android.os.Bundle; -import android.os.Handler; -import android.os.RemoteCallback; -import android.os.ResultReceiver; -import android.os.ShellCallback; -import android.service.timezone.TimeZoneProviderService; -import android.util.IndentingPrintWriter; -import android.util.Log; -import android.util.Slog; - -import com.android.internal.R; -import com.android.internal.annotations.GuardedBy; -import com.android.internal.util.DumpUtils; -import com.android.internal.util.Preconditions; -import com.android.server.FgThread; -import com.android.server.SystemService; -import com.android.server.timezonedetector.TimeZoneDetectorInternal; -import com.android.server.timezonedetector.TimeZoneDetectorService; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.time.Duration; -import java.util.Objects; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -/** - * A service class that acts as a container for the {@link LocationTimeZoneProviderController}, - * which determines what {@link com.android.server.timezonedetector.GeolocationTimeZoneSuggestion} - * are made to the {@link TimeZoneDetectorInternal}, and the {@link LocationTimeZoneProvider}s that - * (indirectly) generate {@link TimeZoneProviderEvent}s. - * - *

For details of the time zone suggestion behavior, see {@link - * LocationTimeZoneProviderController}. - * - *

Implementation details: - * - *

For simplicity, with the exception of a few outliers like {@link #dump}, all processing in - * this service (and package-private helper objects) takes place on a single thread / handler, the - * one indicated by {@link ThreadingDomain}. Because methods like {@link #dump} can be invoked on - * another thread, the service and its related objects must still be thread-safe. - * - *

For testing / reproduction of bugs, it is possible to put providers into "simulation - * mode" where the real binder clients are replaced by {@link - * SimulatedLocationTimeZoneProviderProxy}. This means that the real client providers are never - * bound (ensuring no real location events will be received) and simulated events / behaviors - * can be injected via the command line. To enter simulation mode for a provider, use - * "{@code adb shell setprop persist.sys.location_tz_simulation_mode. 1}" and reboot. - * e.g. "{@code adb shell setprop persist.sys.location_tz_simulation_mode.primary 1}}" - * Then use "{@code adb shell cmd location_time_zone_manager help}" for injection. Set the system - * properties to "0" and reboot to return to exit simulation mode. - */ -public class LocationTimeZoneManagerService extends Binder { - - /** - * Controls lifecycle of the {@link LocationTimeZoneManagerService}. - */ - public static class Lifecycle extends SystemService { - - private LocationTimeZoneManagerService mService; - - public Lifecycle(@NonNull Context context) { - super(Objects.requireNonNull(context)); - } - - @Override - public void onStart() { - Context context = getContext(); - if (TimeZoneDetectorService.isGeoLocationTimeZoneDetectionEnabled(context)) { - mService = new LocationTimeZoneManagerService(context); - - // The service currently exposes no LocalService or Binder API, but it extends - // Binder and is registered as a binder service so it can receive shell commands. - publishBinderService("location_time_zone_manager", mService); - } else { - Slog.i(TAG, getClass() + " is disabled"); - } - } - - @Override - public void onBootPhase(int phase) { - Context context = getContext(); - if (TimeZoneDetectorService.isGeoLocationTimeZoneDetectionEnabled(context)) { - if (phase == PHASE_SYSTEM_SERVICES_READY) { - // The location service must be functioning after this boot phase. - mService.onSystemReady(); - } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { - // Some providers rely on non-platform code (e.g. gcore), so we wait to - // initialize providers until third party code is allowed to run. - mService.onSystemThirdPartyAppsCanStart(); - } - } - } - } - - static final String TAG = "LocationTZDetector"; - - private static final long BLOCKING_OP_WAIT_DURATION_MILLIS = Duration.ofSeconds(20).toMillis(); - - private static final String ATTRIBUTION_TAG = "LocationTimeZoneService"; - - private static final String PRIMARY_LOCATION_TIME_ZONE_SERVICE_ACTION = - TimeZoneProviderService.PRIMARY_LOCATION_TIME_ZONE_PROVIDER_SERVICE_INTERFACE; - private static final String SECONDARY_LOCATION_TIME_ZONE_SERVICE_ACTION = - TimeZoneProviderService.SECONDARY_LOCATION_TIME_ZONE_PROVIDER_SERVICE_INTERFACE; - - - @NonNull private final Context mContext; - - /** - * The {@link ThreadingDomain} used to supply the shared lock object used by the controller and - * related components. - * - *

Most operations are executed on the associated handler thread but not all, hence - * the requirement for additional synchronization using a shared lock. - */ - @NonNull private final ThreadingDomain mThreadingDomain; - - /** A handler associated with the {@link #mThreadingDomain}. */ - @NonNull private final Handler mHandler; - - /** The shared lock from {@link #mThreadingDomain}. */ - @NonNull private final Object mSharedLock; - - // Lazily initialized. Can be null if the service has been stopped. - @GuardedBy("mSharedLock") - private ControllerImpl mLocationTimeZoneDetectorController; - - // Lazily initialized. Can be null if the service has been stopped. - @GuardedBy("mSharedLock") - private ControllerEnvironmentImpl mEnvironment; - - @GuardedBy("mSharedLock") - @NonNull - private String mPrimaryProviderModeOverride = PROVIDER_MODE_OVERRIDE_NONE; - - @GuardedBy("mSharedLock") - @NonNull - private String mSecondaryProviderModeOverride = PROVIDER_MODE_OVERRIDE_NONE; - - LocationTimeZoneManagerService(Context context) { - mContext = context.createAttributionContext(ATTRIBUTION_TAG); - mHandler = FgThread.getHandler(); - mThreadingDomain = new HandlerThreadingDomain(mHandler); - mSharedLock = mThreadingDomain.getLockObject(); - } - - void onSystemReady() { - // Called on an arbitrary thread during initialization. - synchronized (mSharedLock) { - // TODO(b/152744911): LocationManagerService watches for packages disappearing. Need to - // do anything here? - - // TODO(b/152744911): LocationManagerService watches for foreground app changes. Need to - // do anything here? - // TODO(b/152744911): LocationManagerService watches screen state. Need to do anything - // here? - } - } - - void onSystemThirdPartyAppsCanStart() { - // Called on an arbitrary thread during initialization. We do not want to wait for - // completion as it would delay boot. - final boolean waitForCompletion = false; - startInternal(waitForCompletion); - } - - /** - * Starts the service during server initialization or during tests after a call to - * {@link #stop()}. - */ - void start() { - enforceManageTimeZoneDetectorPermission(); - - final boolean waitForCompletion = true; - startInternal(waitForCompletion); - } - - /** - * Starts the service during server initialization or during tests after a call to - * {@link #stop()}. - * - *

To avoid tests needing to sleep, when {@code waitForCompletion} is {@code true}, this - * method will not return until all the system server components have started. - */ - private void startInternal(boolean waitForCompletion) { - Runnable runnable = () -> { - synchronized (mSharedLock) { - if (mLocationTimeZoneDetectorController == null) { - LocationTimeZoneProvider primary = createPrimaryProvider(); - LocationTimeZoneProvider secondary = createSecondaryProvider(); - mLocationTimeZoneDetectorController = - new ControllerImpl(mThreadingDomain, primary, secondary); - ControllerCallbackImpl callback = new ControllerCallbackImpl( - mThreadingDomain); - mEnvironment = new ControllerEnvironmentImpl( - mThreadingDomain, mLocationTimeZoneDetectorController); - mLocationTimeZoneDetectorController.initialize(mEnvironment, callback); - } - } - }; - if (waitForCompletion) { - mThreadingDomain.postAndWait(runnable, BLOCKING_OP_WAIT_DURATION_MILLIS); - } else { - mThreadingDomain.post(runnable); - } - } - - private LocationTimeZoneProvider createPrimaryProvider() { - LocationTimeZoneProviderProxy proxy; - if (isProviderInSimulationMode(PRIMARY_PROVIDER_NAME)) { - proxy = new SimulatedLocationTimeZoneProviderProxy(mContext, mThreadingDomain); - } else if (isProviderDisabled(PRIMARY_PROVIDER_NAME)) { - proxy = new NullLocationTimeZoneProviderProxy(mContext, mThreadingDomain); - } else { - proxy = new RealLocationTimeZoneProviderProxy( - mContext, - mHandler, - mThreadingDomain, - PRIMARY_LOCATION_TIME_ZONE_SERVICE_ACTION, - R.bool.config_enablePrimaryLocationTimeZoneOverlay, - R.string.config_primaryLocationTimeZoneProviderPackageName - ); - } - return new BinderLocationTimeZoneProvider(mThreadingDomain, PRIMARY_PROVIDER_NAME, proxy); - } - - private LocationTimeZoneProvider createSecondaryProvider() { - LocationTimeZoneProviderProxy proxy; - if (isProviderInSimulationMode(SECONDARY_PROVIDER_NAME)) { - proxy = new SimulatedLocationTimeZoneProviderProxy(mContext, mThreadingDomain); - } else if (isProviderDisabled(SECONDARY_PROVIDER_NAME)) { - proxy = new NullLocationTimeZoneProviderProxy(mContext, mThreadingDomain); - } else { - proxy = new RealLocationTimeZoneProviderProxy( - mContext, - mHandler, - mThreadingDomain, - SECONDARY_LOCATION_TIME_ZONE_SERVICE_ACTION, - R.bool.config_enableSecondaryLocationTimeZoneOverlay, - R.string.config_secondaryLocationTimeZoneProviderPackageName - ); - } - return new BinderLocationTimeZoneProvider(mThreadingDomain, SECONDARY_PROVIDER_NAME, proxy); - } - - /** Used for bug triage and in tests to simulate provider events. */ - private boolean isProviderInSimulationMode(String providerName) { - return isProviderModeOverrideSet(providerName, PROVIDER_MODE_OVERRIDE_SIMULATED); - } - - /** Used for bug triage, tests and experiments to remove a provider. */ - private boolean isProviderDisabled(String providerName) { - return !isProviderEnabledInConfig(providerName) - || isProviderModeOverrideSet(providerName, PROVIDER_MODE_OVERRIDE_DISABLED); - } - - private boolean isProviderEnabledInConfig(String providerName) { - int providerEnabledConfigId; - switch (providerName) { - case PRIMARY_PROVIDER_NAME: { - providerEnabledConfigId = R.bool.config_enablePrimaryLocationTimeZoneProvider; - break; - } - case SECONDARY_PROVIDER_NAME: { - providerEnabledConfigId = R.bool.config_enableSecondaryLocationTimeZoneProvider; - break; - } - default: { - throw new IllegalArgumentException(providerName); - } - } - Resources resources = mContext.getResources(); - return resources.getBoolean(providerEnabledConfigId); - } - - private boolean isProviderModeOverrideSet(@NonNull String providerName, @NonNull String mode) { - switch (providerName) { - case PRIMARY_PROVIDER_NAME: { - return Objects.equals(mPrimaryProviderModeOverride, mode); - } - case SECONDARY_PROVIDER_NAME: { - return Objects.equals(mSecondaryProviderModeOverride, mode); - } - default: { - throw new IllegalArgumentException(providerName); - } - } - } - - /** - * Stops the service for tests. To avoid tests needing to sleep, this method will not return - * until all the system server components have stopped. - */ - void stop() { - enforceManageTimeZoneDetectorPermission(); - - mThreadingDomain.postAndWait(() -> { - synchronized (mSharedLock) { - if (mLocationTimeZoneDetectorController != null) { - mLocationTimeZoneDetectorController.destroy(); - mLocationTimeZoneDetectorController = null; - mEnvironment.destroy(); - mEnvironment = null; - } - } - }, BLOCKING_OP_WAIT_DURATION_MILLIS); - } - - @Override - public void onShellCommand(FileDescriptor in, FileDescriptor out, - FileDescriptor err, String[] args, ShellCallback callback, - ResultReceiver resultReceiver) { - (new LocationTimeZoneManagerShellCommand(this)).exec( - this, in, out, err, args, callback, resultReceiver); - } - - /** Sets this service into provider state recording mode for tests. */ - void setProviderModeOverride(@NonNull String providerName, @NonNull String mode) { - enforceManageTimeZoneDetectorPermission(); - - Preconditions.checkArgument( - PRIMARY_PROVIDER_NAME.equals(providerName) - || SECONDARY_PROVIDER_NAME.equals(providerName)); - Preconditions.checkArgument(PROVIDER_MODE_OVERRIDE_DISABLED.equals(mode) - || PROVIDER_MODE_OVERRIDE_SIMULATED.equals(mode) - || PROVIDER_MODE_OVERRIDE_NONE.equals(mode)); - - mThreadingDomain.postAndWait(() -> { - synchronized (mSharedLock) { - switch (providerName) { - case PRIMARY_PROVIDER_NAME: { - mPrimaryProviderModeOverride = mode; - break; - } - case SECONDARY_PROVIDER_NAME: { - mSecondaryProviderModeOverride = mode; - break; - } - } - } - }, BLOCKING_OP_WAIT_DURATION_MILLIS); - } - - /** Sets this service into provider state recording mode for tests. */ - void setProviderStateRecordingEnabled(boolean enabled) { - enforceManageTimeZoneDetectorPermission(); - - mThreadingDomain.postAndWait(() -> { - synchronized (mSharedLock) { - if (mLocationTimeZoneDetectorController != null) { - mLocationTimeZoneDetectorController.setProviderStateRecordingEnabled(enabled); - } - } - }, BLOCKING_OP_WAIT_DURATION_MILLIS); - } - - /** Returns a snapshot of the current controller state for tests. */ - @NonNull - LocationTimeZoneManagerServiceState getStateForTests() { - enforceManageTimeZoneDetectorPermission(); - - try { - return mThreadingDomain.postAndWait( - () -> { - synchronized (mSharedLock) { - if (mLocationTimeZoneDetectorController == null) { - return null; - } - return mLocationTimeZoneDetectorController.getStateForTests(); - } - }, - BLOCKING_OP_WAIT_DURATION_MILLIS); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** - * Passes a {@link TestCommand} to the specified provider and waits for the response. - */ - @NonNull - Bundle handleProviderTestCommand( - @NonNull String providerName, @NonNull TestCommand testCommand) { - enforceManageTimeZoneDetectorPermission(); - - // Because this method blocks and posts work to the threading domain thread, it would cause - // a deadlock if it were called by the threading domain thread. - mThreadingDomain.assertNotCurrentThread(); - - AtomicReference resultReference = new AtomicReference<>(); - CountDownLatch latch = new CountDownLatch(1); - RemoteCallback remoteCallback = new RemoteCallback(x -> { - resultReference.set(x); - latch.countDown(); - }); - - mThreadingDomain.post(() -> { - synchronized (mSharedLock) { - if (mLocationTimeZoneDetectorController == null) { - remoteCallback.sendResult(null); - return; - } - mLocationTimeZoneDetectorController.handleProviderTestCommand( - providerName, testCommand, remoteCallback); - } - }); - - try { - // Wait, but not indefinitely. - if (!latch.await(BLOCKING_OP_WAIT_DURATION_MILLIS, TimeUnit.MILLISECONDS)) { - throw new RuntimeException("Command did not complete in time"); - } - } catch (InterruptedException e) { - throw new AssertionError(e); - } - - return resultReference.get(); - } - - @Override - protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, - @Nullable String[] args) { - if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; - - IndentingPrintWriter ipw = new IndentingPrintWriter(pw); - // Called on an arbitrary thread at any time. - synchronized (mSharedLock) { - ipw.println("LocationTimeZoneManagerService:"); - ipw.increaseIndent(); - if (mLocationTimeZoneDetectorController == null) { - ipw.println("{Stopped}"); - } else { - mLocationTimeZoneDetectorController.dump(ipw, args); - } - ipw.decreaseIndent(); - } - } - - static void debugLog(String msg) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Slog.d(TAG, msg); - } - } - - static void warnLog(String msg) { - warnLog(msg, null); - } - - static void warnLog(String msg, @Nullable Throwable t) { - if (Log.isLoggable(TAG, Log.WARN)) { - Slog.w(TAG, msg, t); - } - } - - private void enforceManageTimeZoneDetectorPermission() { - mContext.enforceCallingPermission( - android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION, - "manage time and time zone detection"); - } -} diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerServiceState.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerServiceState.java deleted file mode 100644 index b1dd55f3d4fd..000000000000 --- a/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerServiceState.java +++ /dev/null @@ -1,97 +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.location.timezone; - -import android.annotation.NonNull; -import android.annotation.Nullable; - -import com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState; -import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -/** A snapshot of the location time zone manager service's state for tests. */ -final class LocationTimeZoneManagerServiceState { - - @Nullable private final GeolocationTimeZoneSuggestion mLastSuggestion; - @NonNull private final List mPrimaryProviderStates; - @NonNull private final List mSecondaryProviderStates; - - LocationTimeZoneManagerServiceState(@NonNull Builder builder) { - mLastSuggestion = builder.mLastSuggestion; - mPrimaryProviderStates = Objects.requireNonNull(builder.mPrimaryProviderStates); - mSecondaryProviderStates = Objects.requireNonNull(builder.mSecondaryProviderStates); - } - - @Nullable - public GeolocationTimeZoneSuggestion getLastSuggestion() { - return mLastSuggestion; - } - - @NonNull - public List getPrimaryProviderStates() { - return Collections.unmodifiableList(mPrimaryProviderStates); - } - - @NonNull - public List getSecondaryProviderStates() { - return Collections.unmodifiableList(mSecondaryProviderStates); - } - - @Override - public String toString() { - return "LocationTimeZoneManagerServiceState{" - + "mLastSuggestion=" + mLastSuggestion - + ", mPrimaryProviderStates=" + mPrimaryProviderStates - + ", mSecondaryProviderStates=" + mSecondaryProviderStates - + '}'; - } - - static final class Builder { - - private GeolocationTimeZoneSuggestion mLastSuggestion; - private List mPrimaryProviderStates; - private List mSecondaryProviderStates; - - @NonNull - Builder setLastSuggestion(@NonNull GeolocationTimeZoneSuggestion lastSuggestion) { - mLastSuggestion = Objects.requireNonNull(lastSuggestion); - return this; - } - - @NonNull - Builder setPrimaryProviderStateChanges(@NonNull List primaryProviderStates) { - mPrimaryProviderStates = new ArrayList<>(primaryProviderStates); - return this; - } - - @NonNull - Builder setSecondaryProviderStateChanges( - @NonNull List secondaryProviderStates) { - mSecondaryProviderStates = new ArrayList<>(secondaryProviderStates); - return this; - } - - @NonNull - LocationTimeZoneManagerServiceState build() { - return new LocationTimeZoneManagerServiceState(this); - } - } -} diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerShellCommand.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerShellCommand.java deleted file mode 100644 index 6f9863c9bd09..000000000000 --- a/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerShellCommand.java +++ /dev/null @@ -1,330 +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.location.timezone; - -import static android.app.time.LocationTimeZoneManager.DUMP_STATE_OPTION_PROTO; -import static android.app.time.LocationTimeZoneManager.PRIMARY_PROVIDER_NAME; -import static android.app.time.LocationTimeZoneManager.PROVIDER_MODE_OVERRIDE_DISABLED; -import static android.app.time.LocationTimeZoneManager.PROVIDER_MODE_OVERRIDE_NONE; -import static android.app.time.LocationTimeZoneManager.PROVIDER_MODE_OVERRIDE_SIMULATED; -import static android.app.time.LocationTimeZoneManager.SECONDARY_PROVIDER_NAME; -import static android.app.time.LocationTimeZoneManager.SHELL_COMMAND_DUMP_STATE; -import static android.app.time.LocationTimeZoneManager.SHELL_COMMAND_RECORD_PROVIDER_STATES; -import static android.app.time.LocationTimeZoneManager.SHELL_COMMAND_SEND_PROVIDER_TEST_COMMAND; -import static android.app.time.LocationTimeZoneManager.SHELL_COMMAND_SET_PROVIDER_MODE_OVERRIDE; -import static android.app.time.LocationTimeZoneManager.SHELL_COMMAND_START; -import static android.app.time.LocationTimeZoneManager.SHELL_COMMAND_STOP; - -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_CERTAIN; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_UNCERTAIN; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STOPPED; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_UNKNOWN; - -import android.annotation.NonNull; -import android.app.time.GeolocationTimeZoneSuggestionProto; -import android.app.time.LocationTimeZoneManagerProto; -import android.app.time.LocationTimeZoneManagerServiceStateProto; -import android.app.time.TimeZoneProviderStateProto; -import android.os.Bundle; -import android.os.ShellCommand; -import android.util.IndentingPrintWriter; -import android.util.proto.ProtoOutputStream; - -import com.android.internal.util.dump.DualDumpOutputStream; -import com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.ProviderStateEnum; -import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; - -/** Implements the shell command interface for {@link LocationTimeZoneManagerService}. */ -class LocationTimeZoneManagerShellCommand extends ShellCommand { - - private static final List VALID_PROVIDER_NAMES = - Arrays.asList(PRIMARY_PROVIDER_NAME, SECONDARY_PROVIDER_NAME); - - private final LocationTimeZoneManagerService mService; - - LocationTimeZoneManagerShellCommand(LocationTimeZoneManagerService service) { - mService = service; - } - - @Override - public int onCommand(String cmd) { - if (cmd == null) { - return handleDefaultCommands(cmd); - } - - switch (cmd) { - case SHELL_COMMAND_START: { - return runStart(); - } - case SHELL_COMMAND_STOP: { - return runStop(); - } - case SHELL_COMMAND_SET_PROVIDER_MODE_OVERRIDE: { - return runSetProviderModeOverride(); - } - case SHELL_COMMAND_SEND_PROVIDER_TEST_COMMAND: { - return runSendProviderTestCommand(); - } - case SHELL_COMMAND_RECORD_PROVIDER_STATES: { - return runRecordProviderStates(); - } - case SHELL_COMMAND_DUMP_STATE: { - return runDumpControllerState(); - } - default: { - return handleDefaultCommands(cmd); - } - } - } - - @Override - public void onHelp() { - final PrintWriter pw = getOutPrintWriter(); - pw.println("Location Time Zone Manager (location_time_zone_manager) commands for tests:"); - pw.println(" help"); - pw.println(" Print this help text."); - pw.printf(" %s\n", SHELL_COMMAND_START); - pw.println(" Starts the location_time_zone_manager, creating time zone providers."); - pw.printf(" %s\n", SHELL_COMMAND_STOP); - pw.println(" Stops the location_time_zone_manager, destroying time zone providers."); - pw.printf(" %s \n", SHELL_COMMAND_SET_PROVIDER_MODE_OVERRIDE); - pw.println(" Sets a provider into a test mode next time the service started."); - pw.printf(" Values: %s|%s|%s\n", PROVIDER_MODE_OVERRIDE_NONE, - PROVIDER_MODE_OVERRIDE_DISABLED, PROVIDER_MODE_OVERRIDE_SIMULATED); - pw.printf(" %s (true|false)\n", SHELL_COMMAND_RECORD_PROVIDER_STATES); - pw.printf(" Enables / disables provider state recording mode. See also %s. The default" - + " state is always \"false\".\n", SHELL_COMMAND_DUMP_STATE); - pw.println(" Note: When enabled, this mode consumes memory and it is only intended for" - + " testing."); - pw.println(" It should be disabled after use, or the device can be rebooted to" - + " reset the mode to disabled."); - pw.println(" Disabling (or enabling repeatedly) clears any existing stored states."); - pw.printf(" %s [%s]\n", SHELL_COMMAND_DUMP_STATE, DUMP_STATE_OPTION_PROTO); - pw.println(" Dumps Location Time Zone Manager state for tests as text or binary proto" - + " form."); - pw.println(" See the LocationTimeZoneManagerServiceStateProto definition for details."); - pw.printf(" %s \n", - SHELL_COMMAND_SEND_PROVIDER_TEST_COMMAND); - pw.println(" Passes a test command to the named provider."); - pw.println(); - pw.printf(" = One of %s\n", VALID_PROVIDER_NAMES); - pw.println(); - pw.printf("%s details:\n", SHELL_COMMAND_SEND_PROVIDER_TEST_COMMAND); - pw.println(); - pw.println("Provider encoding:"); - pw.println(); - TestCommand.printShellCommandEncodingHelp(pw); - pw.println(); - pw.println("Simulated provider mode can be used to test the system server behavior or to" - + " reproduce bugs without the complexity of using real providers."); - pw.println(); - pw.println("The test commands for simulated providers are:"); - SimulatedLocationTimeZoneProviderProxy.printTestCommandShellHelp(pw); - pw.println(); - pw.println("Test commands cannot currently be passed to real provider implementations."); - pw.println(); - } - - private int runStart() { - try { - mService.start(); - } catch (RuntimeException e) { - reportError(e); - return 1; - } - PrintWriter outPrintWriter = getOutPrintWriter(); - outPrintWriter.println("Service started"); - return 0; - } - - private int runStop() { - try { - mService.stop(); - } catch (RuntimeException e) { - reportError(e); - return 1; - } - PrintWriter outPrintWriter = getOutPrintWriter(); - outPrintWriter.println("Service stopped"); - return 0; - } - - private int runSetProviderModeOverride() { - PrintWriter outPrintWriter = getOutPrintWriter(); - try { - String providerName = getNextArgRequired(); - String modeOverride = getNextArgRequired(); - outPrintWriter.println("Setting provider mode override for " + providerName - + " to " + modeOverride); - mService.setProviderModeOverride(providerName, modeOverride); - } catch (RuntimeException e) { - reportError(e); - return 1; - } - return 0; - } - - private int runRecordProviderStates() { - PrintWriter outPrintWriter = getOutPrintWriter(); - boolean enabled; - try { - String nextArg = getNextArgRequired(); - enabled = Boolean.parseBoolean(nextArg); - } catch (RuntimeException e) { - reportError(e); - return 1; - } - - outPrintWriter.println("Setting provider state recording to " + enabled); - try { - mService.setProviderStateRecordingEnabled(enabled); - } catch (IllegalStateException e) { - reportError(e); - return 2; - } - return 0; - } - - private int runDumpControllerState() { - LocationTimeZoneManagerServiceState state; - try { - state = mService.getStateForTests(); - } catch (RuntimeException e) { - reportError(e); - return 1; - } - - DualDumpOutputStream outputStream; - boolean useProto = Objects.equals(DUMP_STATE_OPTION_PROTO, getNextOption()); - if (useProto) { - FileDescriptor outFd = getOutFileDescriptor(); - outputStream = new DualDumpOutputStream(new ProtoOutputStream(outFd)); - } else { - outputStream = new DualDumpOutputStream( - new IndentingPrintWriter(getOutPrintWriter(), " ")); - } - if (state.getLastSuggestion() != null) { - GeolocationTimeZoneSuggestion lastSuggestion = state.getLastSuggestion(); - long lastSuggestionToken = outputStream.start( - "last_suggestion", LocationTimeZoneManagerServiceStateProto.LAST_SUGGESTION); - for (String zoneId : lastSuggestion.getZoneIds()) { - outputStream.write( - "zone_ids" , GeolocationTimeZoneSuggestionProto.ZONE_IDS, zoneId); - } - for (String debugInfo : lastSuggestion.getDebugInfo()) { - outputStream.write( - "debug_info", GeolocationTimeZoneSuggestionProto.DEBUG_INFO, debugInfo); - } - outputStream.end(lastSuggestionToken); - } - - writeProviderStates(outputStream, state.getPrimaryProviderStates(), - "primary_provider_states", - LocationTimeZoneManagerServiceStateProto.PRIMARY_PROVIDER_STATES); - writeProviderStates(outputStream, state.getSecondaryProviderStates(), - "secondary_provider_states", - LocationTimeZoneManagerServiceStateProto.SECONDARY_PROVIDER_STATES); - outputStream.flush(); - - return 0; - } - - private static void writeProviderStates(DualDumpOutputStream outputStream, - List providerStates, String fieldName, - long fieldId) { - for (LocationTimeZoneProvider.ProviderState providerState : providerStates) { - long providerStateToken = outputStream.start(fieldName, fieldId); - outputStream.write("state", TimeZoneProviderStateProto.STATE, - convertProviderStateEnumToProtoEnum(providerState.stateEnum)); - outputStream.end(providerStateToken); - } - } - - private static int convertProviderStateEnumToProtoEnum(@ProviderStateEnum int stateEnum) { - switch (stateEnum) { - case PROVIDER_STATE_UNKNOWN: - return LocationTimeZoneManagerProto.TIME_ZONE_PROVIDER_STATE_UNKNOWN; - case PROVIDER_STATE_STARTED_INITIALIZING: - return LocationTimeZoneManagerProto.TIME_ZONE_PROVIDER_STATE_INITIALIZING; - case PROVIDER_STATE_STARTED_CERTAIN: - return LocationTimeZoneManagerProto.TIME_ZONE_PROVIDER_STATE_CERTAIN; - case PROVIDER_STATE_STARTED_UNCERTAIN: - return LocationTimeZoneManagerProto.TIME_ZONE_PROVIDER_STATE_UNCERTAIN; - case PROVIDER_STATE_STOPPED: - return LocationTimeZoneManagerProto.TIME_ZONE_PROVIDER_STATE_DISABLED; - case PROVIDER_STATE_PERM_FAILED: - return LocationTimeZoneManagerProto.TIME_ZONE_PROVIDER_STATE_PERM_FAILED; - case PROVIDER_STATE_DESTROYED: - return LocationTimeZoneManagerProto.TIME_ZONE_PROVIDER_STATE_DESTROYED; - default: { - throw new IllegalArgumentException("Unknown stateEnum=" + stateEnum); - } - } - } - - private int runSendProviderTestCommand() { - PrintWriter outPrintWriter = getOutPrintWriter(); - - String providerName; - TestCommand testCommand; - try { - providerName = validateProviderName(getNextArgRequired()); - testCommand = createTestCommandFromNextShellArg(); - } catch (RuntimeException e) { - reportError(e); - return 1; - } - - outPrintWriter.println("Injecting testCommand=" + testCommand - + " to providerName=" + providerName); - try { - Bundle result = mService.handleProviderTestCommand(providerName, testCommand); - outPrintWriter.println(result); - } catch (RuntimeException e) { - reportError(e); - return 2; - } - return 0; - } - - @NonNull - private TestCommand createTestCommandFromNextShellArg() { - return TestCommand.createFromShellCommandArgs(this); - } - - private void reportError(Throwable e) { - PrintWriter errPrintWriter = getErrPrintWriter(); - errPrintWriter.println("Error: "); - e.printStackTrace(errPrintWriter); - } - - @NonNull - static String validateProviderName(@NonNull String value) { - if (!VALID_PROVIDER_NAMES.contains(value)) { - throw new IllegalArgumentException("Unknown provider name=" + value); - } - return value; - } -} diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProvider.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProvider.java deleted file mode 100644 index 9a7b7750659c..000000000000 --- a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProvider.java +++ /dev/null @@ -1,740 +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.location.timezone; - -import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_ERROR_KEY; -import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_SUCCESS_KEY; - -import static com.android.server.location.timezone.LocationTimeZoneManagerService.debugLog; -import static com.android.server.location.timezone.LocationTimeZoneManagerService.warnLog; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_CERTAIN; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_UNCERTAIN; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STOPPED; -import static com.android.server.location.timezone.TimeZoneProviderEvent.EVENT_TYPE_PERMANENT_FAILURE; -import static com.android.server.location.timezone.TimeZoneProviderEvent.EVENT_TYPE_SUGGESTION; -import static com.android.server.location.timezone.TimeZoneProviderEvent.EVENT_TYPE_UNCERTAIN; - -import android.annotation.ElapsedRealtimeLong; -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.os.Bundle; -import android.os.Handler; -import android.os.RemoteCallback; -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; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** - * A facade used by the {@link LocationTimeZoneProviderController} to interact with a location time - * zone provider. The provider implementation will typically have logic running in another process. - * - *

The provider is supplied with a {@link ProviderListener} via {@link - * #initialize(ProviderListener)}. This starts communication of asynchronous detection / error - * events back to the {@link LocationTimeZoneProviderController} via the {@link - * ProviderListener#onProviderStateChange} method. This call must be made on the - * {@link Handler} thread from the {@link ThreadingDomain} passed to the constructor. - * - *

This class is also responsible for monitoring the initialization timeout for a provider. i.e. - * if the provider fails to send its first suggestion within a certain time, this is the component - * responsible for generating the necessary "uncertain" event. - * - *

All incoming calls from the controller except for {@link - * LocationTimeZoneProvider#dump(android.util.IndentingPrintWriter, String[])} will be made on the - * {@link Handler} thread of the {@link ThreadingDomain} passed to the constructor. - */ -abstract class LocationTimeZoneProvider implements Dumpable { - - /** - * Listener interface used by the {@link LocationTimeZoneProviderController} to register an - * interest in provider events. - */ - interface ProviderListener { - /** - * Indicated that a provider changed states. The {@code providerState} indicates which one - */ - void onProviderStateChange(@NonNull ProviderState providerState); - } - - /** - * Information about the provider's current state. - */ - static class ProviderState { - - @IntDef(prefix = "PROVIDER_STATE_", - value = { PROVIDER_STATE_UNKNOWN, PROVIDER_STATE_STARTED_INITIALIZING, - PROVIDER_STATE_STARTED_CERTAIN, PROVIDER_STATE_STARTED_UNCERTAIN, - PROVIDER_STATE_STOPPED, PROVIDER_STATE_PERM_FAILED, PROVIDER_STATE_DESTROYED }) - @interface ProviderStateEnum {} - - /** - * Uninitialized value. Must not be used afte {@link LocationTimeZoneProvider#initialize}. - */ - static final int PROVIDER_STATE_UNKNOWN = 0; - - /** - * The provider is started and has not reported its first event. - */ - static final int PROVIDER_STATE_STARTED_INITIALIZING = 1; - - /** - * The provider is started and most recently reported a "suggestion" event. - */ - static final int PROVIDER_STATE_STARTED_CERTAIN = 2; - - /** - * The provider is started and most recently reported an "uncertain" event. - */ - static final int PROVIDER_STATE_STARTED_UNCERTAIN = 3; - - /** - * The provider is stopped. - * - * This is the state after {@link #initialize} is called. - */ - static final int PROVIDER_STATE_STOPPED = 4; - - /** - * The provider has failed and cannot be restarted. This is a terminated state triggered by - * the provider itself. - * - * Providers may enter this state any time after a provider is started. - */ - static final int PROVIDER_STATE_PERM_FAILED = 5; - - /** - * The provider has been destroyed by the controller and cannot be restarted. Similar to - * {@link #PROVIDER_STATE_PERM_FAILED} except that a provider is set into this state. - */ - static final int PROVIDER_STATE_DESTROYED = 6; - - /** The {@link LocationTimeZoneProvider} the state is for. */ - public final @NonNull LocationTimeZoneProvider provider; - - /** The state enum value of the current state. */ - public final @ProviderStateEnum int stateEnum; - - /** - * The last {@link TimeZoneProviderEvent} received. Only populated when {@link #stateEnum} - * is either {@link #PROVIDER_STATE_STARTED_CERTAIN} or {@link - * #PROVIDER_STATE_STARTED_UNCERTAIN}, but it can be {@code null} then too if no event has - * yet been received. - */ - @Nullable public final TimeZoneProviderEvent event; - - /** - * The user configuration associated with the current state. Only and always present when - * {@link #stateEnum} is one of the started states. - */ - @Nullable public final ConfigurationInternal currentUserConfiguration; - - /** - * The time according to the elapsed realtime clock when the provider entered the current - * state. Included for debugging, not used for equality. - */ - @ElapsedRealtimeLong - private final long mStateEntryTimeMillis; - - /** - * Debug information providing context for the transition to this state. Included for - * debugging, not used for equality. - */ - @Nullable private final String mDebugInfo; - - - private ProviderState(@NonNull LocationTimeZoneProvider provider, - @ProviderStateEnum int stateEnum, - @Nullable TimeZoneProviderEvent event, - @Nullable ConfigurationInternal currentUserConfiguration, - @Nullable String debugInfo) { - this.provider = Objects.requireNonNull(provider); - this.stateEnum = stateEnum; - this.event = event; - this.currentUserConfiguration = currentUserConfiguration; - this.mStateEntryTimeMillis = SystemClock.elapsedRealtime(); - this.mDebugInfo = debugInfo; - } - - /** Creates the bootstrap state, uses {@link #PROVIDER_STATE_UNKNOWN}. */ - static ProviderState createStartingState( - @NonNull LocationTimeZoneProvider provider) { - return new ProviderState( - provider, PROVIDER_STATE_UNKNOWN, null, null, "Initial state"); - } - - /** - * Create a new state from this state. Validates that the state transition is valid - * and that the required parameters for the new state are present / absent. - */ - ProviderState newState(@ProviderStateEnum int newStateEnum, - @Nullable TimeZoneProviderEvent event, - @Nullable ConfigurationInternal currentUserConfig, - @Nullable String debugInfo) { - - // Check valid "from" transitions. - switch (this.stateEnum) { - case PROVIDER_STATE_UNKNOWN: { - if (newStateEnum != PROVIDER_STATE_STOPPED) { - throw new IllegalArgumentException( - "Must transition from " + prettyPrintStateEnum( - PROVIDER_STATE_UNKNOWN) - + " to " + prettyPrintStateEnum(PROVIDER_STATE_STOPPED)); - } - break; - } - case PROVIDER_STATE_STOPPED: - case PROVIDER_STATE_STARTED_INITIALIZING: - case PROVIDER_STATE_STARTED_CERTAIN: - case PROVIDER_STATE_STARTED_UNCERTAIN: { - // These can go to each other or either of PROVIDER_STATE_PERM_FAILED and - // PROVIDER_STATE_DESTROYED. - break; - } - case PROVIDER_STATE_PERM_FAILED: - case PROVIDER_STATE_DESTROYED: { - throw new IllegalArgumentException("Illegal transition out of " - + prettyPrintStateEnum(this.stateEnum)); - } - default: { - throw new IllegalArgumentException("Invalid this.stateEnum=" + this.stateEnum); - } - } - - // Validate "to" transitions / arguments. - switch (newStateEnum) { - case PROVIDER_STATE_UNKNOWN: { - throw new IllegalArgumentException("Cannot transition to " - + prettyPrintStateEnum(PROVIDER_STATE_UNKNOWN)); - } - case PROVIDER_STATE_STOPPED: { - if (event != null || currentUserConfig != null) { - throw new IllegalArgumentException( - "Stopped state: event and currentUserConfig must be null" - + ", event=" + event - + ", currentUserConfig=" + currentUserConfig); - } - break; - } - case PROVIDER_STATE_STARTED_INITIALIZING: - case PROVIDER_STATE_STARTED_CERTAIN: - case PROVIDER_STATE_STARTED_UNCERTAIN: { - if (currentUserConfig == null) { - throw new IllegalArgumentException( - "Started state: currentUserConfig must not be null"); - } - break; - } - case PROVIDER_STATE_PERM_FAILED: - case PROVIDER_STATE_DESTROYED: { - if (event != null || currentUserConfig != null) { - throw new IllegalArgumentException( - "Terminal state: event and currentUserConfig must be null" - + ", newStateEnum=" + newStateEnum - + ", event=" + event - + ", currentUserConfig=" + currentUserConfig); - } - break; - } - default: { - throw new IllegalArgumentException("Unknown newStateEnum=" + newStateEnum); - } - } - return new ProviderState(provider, newStateEnum, event, currentUserConfig, debugInfo); - } - - /** Returns {@code true} if {@link #stateEnum} is one of the started states. */ - boolean isStarted() { - return stateEnum == PROVIDER_STATE_STARTED_INITIALIZING - || stateEnum == PROVIDER_STATE_STARTED_CERTAIN - || stateEnum == PROVIDER_STATE_STARTED_UNCERTAIN; - } - - /** Returns {@code true} if {@link #stateEnum} is one of the terminated states. */ - boolean isTerminated() { - return stateEnum == PROVIDER_STATE_PERM_FAILED - || stateEnum == PROVIDER_STATE_DESTROYED; - } - - @Override - public String toString() { - // this.provider is omitted deliberately to avoid recursion, since the provider holds - // a reference to its state. - return "ProviderState{" - + "stateEnum=" + prettyPrintStateEnum(stateEnum) - + ", event=" + event - + ", currentUserConfiguration=" + currentUserConfiguration - + ", mStateEntryTimeMillis=" + mStateEntryTimeMillis - + ", mDebugInfo=" + mDebugInfo - + '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ProviderState state = (ProviderState) o; - return stateEnum == state.stateEnum - && Objects.equals(event, state.event) - && Objects.equals(currentUserConfiguration, state.currentUserConfiguration); - } - - @Override - public int hashCode() { - return Objects.hash(stateEnum, event, currentUserConfiguration); - } - - private static String prettyPrintStateEnum(@ProviderStateEnum int state) { - switch (state) { - case PROVIDER_STATE_STOPPED: - return "Stopped (" + PROVIDER_STATE_STOPPED + ")"; - case PROVIDER_STATE_STARTED_INITIALIZING: - return "Started initializing (" + PROVIDER_STATE_STARTED_INITIALIZING + ")"; - case PROVIDER_STATE_STARTED_CERTAIN: - return "Started certain (" + PROVIDER_STATE_STARTED_CERTAIN + ")"; - case PROVIDER_STATE_STARTED_UNCERTAIN: - return "Started uncertain (" + PROVIDER_STATE_STARTED_UNCERTAIN + ")"; - case PROVIDER_STATE_PERM_FAILED: - return "Perm failure (" + PROVIDER_STATE_PERM_FAILED + ")"; - case PROVIDER_STATE_DESTROYED: - return "Destroyed (" + PROVIDER_STATE_DESTROYED + ")"; - case PROVIDER_STATE_UNKNOWN: - default: - return "Unknown (" + state + ")"; - } - } - } - - @NonNull final ThreadingDomain mThreadingDomain; - @NonNull final Object mSharedLock; - @NonNull final String mProviderName; - - /** - * Usually {@code false} but can be set to {@code true} for testing. - */ - @GuardedBy("mSharedLock") - private boolean mStateChangeRecording; - - @GuardedBy("mSharedLock") - @NonNull - private final ArrayList mRecordedStates = new ArrayList<>(0); - - /** - * The current state (with history for debugging). - */ - @GuardedBy("mSharedLock") - final ReferenceWithHistory mCurrentState = new ReferenceWithHistory<>(10); - - /** - * Used for scheduling initialization timeouts, i.e. for providers that have just been started. - */ - @NonNull private final SingleRunnableQueue mInitializationTimeoutQueue; - - // Non-null and effectively final after initialize() is called. - ProviderListener mProviderListener; - - /** Creates the instance. */ - LocationTimeZoneProvider(@NonNull ThreadingDomain threadingDomain, - @NonNull String providerName) { - mThreadingDomain = Objects.requireNonNull(threadingDomain); - mInitializationTimeoutQueue = threadingDomain.createSingleRunnableQueue(); - mSharedLock = threadingDomain.getLockObject(); - mProviderName = Objects.requireNonNull(providerName); - } - - /** - * Initializes the provider. Called before the provider is first used. - */ - final void initialize(@NonNull ProviderListener providerListener) { - mThreadingDomain.assertCurrentThread(); - - synchronized (mSharedLock) { - if (mProviderListener != null) { - throw new IllegalStateException("initialize already called"); - } - mProviderListener = Objects.requireNonNull(providerListener); - ProviderState currentState = ProviderState.createStartingState(this); - currentState = currentState.newState( - PROVIDER_STATE_STOPPED, null, null, - "initialize() called"); - setCurrentState(currentState, false); - - // Guard against uncaught exceptions due to initialization problems. - try { - onInitialize(); - } catch (RuntimeException e) { - warnLog("Unable to initialize the provider", e); - currentState = currentState - .newState(PROVIDER_STATE_PERM_FAILED, null, null, - "Provider failed to initialize"); - setCurrentState(currentState, true); - } - } - } - - /** - * Implemented by subclasses to do work during {@link #initialize}. - */ - @GuardedBy("mSharedLock") - abstract void onInitialize(); - - /** - * Destroys the provider. Called after the provider is stopped. This instance will not be called - * again by the {@link LocationTimeZoneProviderController}. - */ - final void destroy() { - mThreadingDomain.assertCurrentThread(); - - synchronized (mSharedLock) { - ProviderState currentState = mCurrentState.get(); - if (!currentState.isTerminated()) { - ProviderState destroyedState = currentState - .newState(PROVIDER_STATE_DESTROYED, null, null, "destroy() called"); - setCurrentState(destroyedState, false); - onDestroy(); - } - } - } - - /** - * Implemented by subclasses to do work during {@link #destroy()}. - */ - @GuardedBy("mSharedLock") - abstract void onDestroy(); - - /** - * Sets the provider into state recording mode for tests. - */ - final void setStateChangeRecordingEnabled(boolean enabled) { - mThreadingDomain.assertCurrentThread(); - synchronized (mSharedLock) { - mStateChangeRecording = enabled; - mRecordedStates.clear(); - mRecordedStates.trimToSize(); - } - } - - /** - * Returns recorded states. - */ - final List getRecordedStates() { - mThreadingDomain.assertCurrentThread(); - synchronized (mSharedLock) { - return new ArrayList<>(mRecordedStates); - } - } - - /** - * Set the current state, for use by this class and subclasses only. If {@code #notifyChanges} - * is {@code true} and {@code newState} is not equal to the old state, then {@link - * ProviderListener#onProviderStateChange(ProviderState)} must be called on - * {@link #mProviderListener}. - */ - final void setCurrentState(@NonNull ProviderState newState, boolean notifyChanges) { - mThreadingDomain.assertCurrentThread(); - synchronized (mSharedLock) { - ProviderState oldState = mCurrentState.get(); - mCurrentState.set(newState); - onSetCurrentState(newState); - if (!Objects.equals(newState, oldState)) { - if (mStateChangeRecording) { - mRecordedStates.add(newState); - } - if (notifyChanges) { - mProviderListener.onProviderStateChange(newState); - } - } - } - } - - /** - * Overridden by subclasses to do work during {@link #setCurrentState}. - */ - @GuardedBy("mSharedLock") - void onSetCurrentState(ProviderState newState) { - // Default no-op. - } - - /** - * Returns the current state of the provider. This method must be called using the handler - * thread from the {@link ThreadingDomain}. - */ - @NonNull - final ProviderState getCurrentState() { - mThreadingDomain.assertCurrentThread(); - synchronized (mSharedLock) { - return mCurrentState.get(); - } - } - - /** - * Returns the name of the provider. This method must be called using the handler thread from - * the {@link ThreadingDomain}. - */ - final String getName() { - mThreadingDomain.assertCurrentThread(); - return mProviderName; - } - - /** - * Starts the provider. It is an error to call this method except when the {@link - * #getCurrentState()} is at {@link ProviderState#PROVIDER_STATE_STOPPED}. This method must be - * called using the handler thread from the {@link ThreadingDomain}. - */ - final void startUpdates(@NonNull ConfigurationInternal currentUserConfiguration, - @NonNull Duration initializationTimeout, @NonNull Duration initializationTimeoutFuzz) { - mThreadingDomain.assertCurrentThread(); - - synchronized (mSharedLock) { - assertCurrentState(PROVIDER_STATE_STOPPED); - - ProviderState currentState = mCurrentState.get(); - ProviderState newState = currentState.newState( - PROVIDER_STATE_STARTED_INITIALIZING, null /* event */, - currentUserConfiguration, "startUpdates() called"); - setCurrentState(newState, false); - - Duration delay = initializationTimeout.plus(initializationTimeoutFuzz); - mInitializationTimeoutQueue.runDelayed( - this::handleInitializationTimeout, delay.toMillis()); - - onStartUpdates(initializationTimeout); - } - } - - private void handleInitializationTimeout() { - mThreadingDomain.assertCurrentThread(); - - synchronized (mSharedLock) { - ProviderState currentState = mCurrentState.get(); - if (currentState.stateEnum == PROVIDER_STATE_STARTED_INITIALIZING) { - // On initialization timeout the provider becomes uncertain. - ProviderState newState = currentState.newState( - PROVIDER_STATE_STARTED_UNCERTAIN, null /* event */, - currentState.currentUserConfiguration, "initialization timeout"); - setCurrentState(newState, true); - } else { - warnLog("handleInitializationTimeout: Initialization timeout triggered when in" - + " an unexpected state=" + currentState); - } - } - } - - /** - * Implemented by subclasses to do work during {@link #startUpdates}. This is where the logic - * to start the real provider should be implemented. - * - * @param initializationTimeout the initialization timeout to pass to the real provider - */ - abstract void onStartUpdates(@NonNull Duration initializationTimeout); - - /** - * Stops the provider. It is an error to call this method except when the {@link - * #getCurrentState()} is one of the started states. This method must be - * called using the handler thread from the {@link ThreadingDomain}. - */ - final void stopUpdates() { - mThreadingDomain.assertCurrentThread(); - - synchronized (mSharedLock) { - assertIsStarted(); - - ProviderState currentState = mCurrentState.get(); - ProviderState newState = currentState.newState( - PROVIDER_STATE_STOPPED, null, null, "stopUpdates() called"); - setCurrentState(newState, false); - - if (mInitializationTimeoutQueue.hasQueued()) { - mInitializationTimeoutQueue.cancel(); - } - - onStopUpdates(); - } - } - - /** - * Implemented by subclasses to do work during {@link #stopUpdates}. - */ - abstract void onStopUpdates(); - - /** - * Overridden by subclasses to handle the supplied {@link TestCommand}. If {@code callback} is - * non-null, the default implementation sends a result {@link Bundle} with {@link - * android.service.timezone.TimeZoneProviderService#TEST_COMMAND_RESULT_SUCCESS_KEY} set to - * {@code false} and a "Not implemented" error message. - */ - void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) { - Objects.requireNonNull(testCommand); - - if (callback != null) { - Bundle result = new Bundle(); - result.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, false); - result.putString(TEST_COMMAND_RESULT_ERROR_KEY, "Not implemented"); - callback.sendResult(result); - } - } - - /** For subclasses to invoke when a {@link TimeZoneProviderEvent} has been received. */ - final void handleTimeZoneProviderEvent(@NonNull TimeZoneProviderEvent timeZoneProviderEvent) { - mThreadingDomain.assertCurrentThread(); - Objects.requireNonNull(timeZoneProviderEvent); - - synchronized (mSharedLock) { - debugLog("handleTimeZoneProviderEvent: mProviderName=" + mProviderName - + ", timeZoneProviderEvent=" + timeZoneProviderEvent); - - ProviderState currentState = mCurrentState.get(); - int eventType = timeZoneProviderEvent.getType(); - switch (currentState.stateEnum) { - case PROVIDER_STATE_DESTROYED: - case PROVIDER_STATE_PERM_FAILED: { - // After entering a terminated state, there is nothing to do. The remote peer is - // supposed to stop sending events after it has reported perm failure. - warnLog("handleTimeZoneProviderEvent: Event=" + timeZoneProviderEvent - + " received for provider=" + this + " when in terminated state"); - return; - } - case PROVIDER_STATE_STOPPED: { - switch (eventType) { - case EVENT_TYPE_PERMANENT_FAILURE: { - String msg = "handleTimeZoneProviderEvent:" - + " Failure event=" + timeZoneProviderEvent - + " received for stopped provider=" + this - + ", entering permanently failed state"; - warnLog(msg); - ProviderState newState = currentState.newState( - PROVIDER_STATE_PERM_FAILED, null, null, msg); - setCurrentState(newState, true); - if (mInitializationTimeoutQueue.hasQueued()) { - mInitializationTimeoutQueue.cancel(); - } - return; - } - case EVENT_TYPE_SUGGESTION: - case EVENT_TYPE_UNCERTAIN: { - // Any geolocation-related events received for a stopped provider are - // ignored: they should not happen. - warnLog("handleTimeZoneProviderEvent:" - + " event=" + timeZoneProviderEvent - + " received for stopped provider=" + this - + ", ignoring"); - - return; - } - default: { - throw new IllegalStateException( - "Unknown eventType=" + timeZoneProviderEvent); - } - } - } - case PROVIDER_STATE_STARTED_INITIALIZING: - case PROVIDER_STATE_STARTED_CERTAIN: - case PROVIDER_STATE_STARTED_UNCERTAIN: { - switch (eventType) { - case EVENT_TYPE_PERMANENT_FAILURE: { - String msg = "handleTimeZoneProviderEvent:" - + " Failure event=" + timeZoneProviderEvent - + " received for provider=" + this - + ", entering permanently failed state"; - warnLog(msg); - 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_SUGGESTION: { - @ProviderStateEnum int providerStateEnum; - if (eventType == EVENT_TYPE_UNCERTAIN) { - providerStateEnum = PROVIDER_STATE_STARTED_UNCERTAIN; - } else { - providerStateEnum = PROVIDER_STATE_STARTED_CERTAIN; - } - ProviderState newState = currentState.newState(providerStateEnum, - timeZoneProviderEvent, currentState.currentUserConfiguration, - "handleTimeZoneProviderEvent() when started"); - setCurrentState(newState, true); - if (mInitializationTimeoutQueue.hasQueued()) { - mInitializationTimeoutQueue.cancel(); - } - return; - } - default: { - throw new IllegalStateException( - "Unknown eventType=" + timeZoneProviderEvent); - } - } - } - default: { - throw new IllegalStateException("Unknown providerType=" + currentState); - } - } - } - } - - @GuardedBy("mSharedLock") - private void assertIsStarted() { - ProviderState currentState = mCurrentState.get(); - if (!currentState.isStarted()) { - throw new IllegalStateException("Required a started state, but was " + currentState); - } - } - - @GuardedBy("mSharedLock") - private void assertCurrentState(@ProviderStateEnum int requiredState) { - ProviderState currentState = mCurrentState.get(); - if (currentState.stateEnum != requiredState) { - throw new IllegalStateException( - "Required stateEnum=" + requiredState + ", but was " + currentState); - } - } - - @VisibleForTesting - boolean isInitializationTimeoutSet() { - synchronized (mSharedLock) { - return mInitializationTimeoutQueue.hasQueued(); - } - } - - @VisibleForTesting - Duration getInitializationTimeoutDelay() { - synchronized (mSharedLock) { - return Duration.ofMillis(mInitializationTimeoutQueue.getQueuedDelayMillis()); - } - } -} diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderController.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderController.java deleted file mode 100644 index ec2bc13b8a16..000000000000 --- a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderController.java +++ /dev/null @@ -1,158 +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.location.timezone; - -import android.annotation.DurationMillisLong; -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; -import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion; - -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 controller interacts with the following components: - *

- * - *

All incoming calls except for {@link - * LocationTimeZoneProviderController#dump(android.util.IndentingPrintWriter, String[])} must be - * made on the {@link Handler} thread of the {@link ThreadingDomain} passed to {@link - * #LocationTimeZoneProviderController(ThreadingDomain)}. - * - *

Provider / controller integration notes: - * - *

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), where as certainty means it should not. - * - *

A provider can fail permanently. A permanent failure will stop the provider until next - * boot. - */ -abstract class LocationTimeZoneProviderController implements Dumpable { - - @NonNull protected final ThreadingDomain mThreadingDomain; - @NonNull protected final Object mSharedLock; - - LocationTimeZoneProviderController(@NonNull ThreadingDomain threadingDomain) { - mThreadingDomain = Objects.requireNonNull(threadingDomain); - mSharedLock = threadingDomain.getLockObject(); - } - - /** - * Called to initialize the controller during boot. Called once only. - * {@link LocationTimeZoneProvider#initialize} must be called by this method. - */ - abstract void initialize(@NonNull Environment environment, @NonNull Callback callback); - - /** - * Called when any settings or other device state that affect location-based time zone detection - * 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. - */ - abstract void onConfigChanged(); - - @VisibleForTesting - abstract boolean isUncertaintyTimeoutSet(); - - @VisibleForTesting - @DurationMillisLong - abstract long getUncertaintyTimeoutDelayMillis(); - - /** Called if the geolocation time zone detection is being reconfigured. */ - abstract void destroy(); - - /** - * 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 delay allowed after receiving uncertainty from a provider before it should be - * passed on. - */ - abstract Duration getUncertaintyDelay(); - } - - /** - * 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; - @NonNull protected final Object mSharedLock; - - Callback(@NonNull ThreadingDomain threadingDomain) { - mThreadingDomain = Objects.requireNonNull(threadingDomain); - mSharedLock = threadingDomain.getLockObject(); - } - - /** - * Suggests the latest time zone state for the device. - */ - abstract void suggest(@NonNull GeolocationTimeZoneSuggestion suggestion); - } -} diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderProxy.java deleted file mode 100644 index 8368b5ed5d75..000000000000 --- a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderProxy.java +++ /dev/null @@ -1,150 +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.location.timezone; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; -import android.os.Handler; -import android.os.RemoteCallback; -import android.util.IndentingPrintWriter; - -import com.android.internal.annotations.GuardedBy; -import com.android.server.timezonedetector.Dumpable; - -import java.util.Objects; - -/** - * System server-side proxy for ITimeZoneProvider implementations, i.e. this provides the system - * server object used to communicate with a remote TimeZoneProvider over Binder, which could be - * running in a different process. As TimeZoneProviders are bound / unbound this proxy will rebind - * to the "best" available remote process. - * - *

Threading guarantees provided / required by this interface: - *

- * - *

This class exists to enable the introduction of test implementations of {@link - * LocationTimeZoneProviderProxy} that can be used when a device is in a test mode to inject test - * events / behavior that are otherwise difficult to simulate. - */ -abstract class LocationTimeZoneProviderProxy implements Dumpable { - - @NonNull protected final Context mContext; - @NonNull protected final ThreadingDomain mThreadingDomain; - @NonNull protected final Object mSharedLock; - - // Non-null and effectively final after setListener() is called. - @GuardedBy("mSharedLock") - @Nullable - protected Listener mListener; - - LocationTimeZoneProviderProxy( - @NonNull Context context, @NonNull ThreadingDomain threadingDomain) { - mContext = Objects.requireNonNull(context); - mThreadingDomain = Objects.requireNonNull(threadingDomain); - mSharedLock = threadingDomain.getLockObject(); - } - - /** - * Initializes the proxy. The supplied listener can expect to receive all events after this - * point. This method calls {@link #onInitialize()} for subclasses to handle their own - * initialization. - */ - void initialize(@NonNull Listener listener) { - Objects.requireNonNull(listener); - synchronized (mSharedLock) { - if (mListener != null) { - throw new IllegalStateException("listener already set"); - } - this.mListener = listener; - onInitialize(); - } - } - - /** - * Implemented by subclasses to initializes the proxy. This is called after {@link #mListener} - * is set. - */ - @GuardedBy("mSharedLock") - abstract void onInitialize(); - - /** - * Destroys the proxy. This method calls {@link #onDestroy()} for subclasses to handle their own - * destruction. - */ - void destroy() { - synchronized (mSharedLock) { - onDestroy(); - } - } - - /** - * Implemented by subclasses to destroy the proxy. - */ - @GuardedBy("mSharedLock") - abstract void onDestroy(); - - /** - * Sets a new request for the provider. - */ - abstract void setRequest(@NonNull TimeZoneProviderRequest request); - - /** - * Processes the supplied test command. An optional callback can be supplied to listen for a - * response. - */ - abstract void handleTestCommand(@NonNull TestCommand testCommand, - @Nullable RemoteCallback callback); - - /** - * Handles a {@link TimeZoneProviderEvent} from a remote process. - */ - final void handleTimeZoneProviderEvent(@NonNull TimeZoneProviderEvent timeZoneProviderEvent) { - // These calls are invoked on a binder thread. Move to the mThreadingDomain thread as - // required by the guarantees for this class. - mThreadingDomain.post(() -> mListener.onReportTimeZoneProviderEvent(timeZoneProviderEvent)); - } - - /** - * Interface for listening to location time zone providers. See {@link - * LocationTimeZoneProviderProxy} for threading guarantees. - */ - interface Listener { - - /** - * Called when a provider receives a {@link TimeZoneProviderEvent}. - */ - void onReportTimeZoneProviderEvent(@NonNull TimeZoneProviderEvent timeZoneProviderEvent); - - /** - * Called when a provider is (re)bound. - */ - void onProviderBound(); - - /** Called when a provider is unbound. */ - void onProviderUnbound(); - } -} diff --git a/services/core/java/com/android/server/location/timezone/NullLocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/location/timezone/NullLocationTimeZoneProviderProxy.java deleted file mode 100644 index c2abbf9a1b8c..000000000000 --- a/services/core/java/com/android/server/location/timezone/NullLocationTimeZoneProviderProxy.java +++ /dev/null @@ -1,86 +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.location.timezone; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; -import android.os.Bundle; -import android.os.RemoteCallback; -import android.service.timezone.TimeZoneProviderService; -import android.util.IndentingPrintWriter; - -/** - * A {@link LocationTimeZoneProviderProxy} that provides minimal responses needed for the {@link - * BinderLocationTimeZoneProvider} to operate correctly when there is no "real" provider - * configured / enabled. This can be used during development / testing, or in a production build - * when the platform supports more providers than are needed for an Android deployment. - * - *

For example, if the {@link LocationTimeZoneProviderController} supports a primary - * and a secondary {@link LocationTimeZoneProvider}, but only a primary is configured, the secondary - * config will be left null and the {@link LocationTimeZoneProviderProxy} implementation will be - * defaulted to a {@link NullLocationTimeZoneProviderProxy}. The {@link - * NullLocationTimeZoneProviderProxy} sends a "permanent failure" event immediately after being - * started for the first time, which ensures the {@link LocationTimeZoneProviderController} won't - * expect any further {@link TimeZoneProviderEvent}s to come from it, and won't attempt to use it - * again. - */ -class NullLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy { - - /** Creates the instance. */ - NullLocationTimeZoneProviderProxy( - @NonNull Context context, @NonNull ThreadingDomain threadingDomain) { - super(context, threadingDomain); - } - - @Override - void onInitialize() { - // No-op - } - - @Override - void onDestroy() { - // No-op - } - - @Override - void setRequest(@NonNull TimeZoneProviderRequest request) { - if (request.sendUpdates()) { - TimeZoneProviderEvent event = TimeZoneProviderEvent.createPermanentFailureEvent( - "Provider is disabled"); - handleTimeZoneProviderEvent(event); - } - } - - @Override - void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) { - if (callback != null) { - Bundle result = new Bundle(); - result.putBoolean(TimeZoneProviderService.TEST_COMMAND_RESULT_SUCCESS_KEY, false); - result.putString(TimeZoneProviderService.TEST_COMMAND_RESULT_ERROR_KEY, - "Provider is disabled"); - callback.sendResult(result); - } - } - - @Override - public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) { - synchronized (mSharedLock) { - ipw.println("{NullLocationTimeZoneProviderProxy}"); - } - } -} diff --git a/services/core/java/com/android/server/location/timezone/RealLocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/location/timezone/RealLocationTimeZoneProviderProxy.java deleted file mode 100644 index 0904ba419b3d..000000000000 --- a/services/core/java/com/android/server/location/timezone/RealLocationTimeZoneProviderProxy.java +++ /dev/null @@ -1,244 +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.location.timezone; - -import static android.content.pm.PackageManager.PERMISSION_GRANTED; -import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_ERROR_KEY; -import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_SUCCESS_KEY; - -import static com.android.server.location.timezone.LocationTimeZoneManagerService.warnLog; - -import android.Manifest; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.pm.ServiceInfo; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.RemoteCallback; -import android.service.timezone.ITimeZoneProvider; -import android.service.timezone.ITimeZoneProviderManager; -import android.service.timezone.TimeZoneProviderSuggestion; -import android.util.IndentingPrintWriter; - -import com.android.internal.annotations.GuardedBy; -import com.android.server.ServiceWatcher; - -import java.util.Objects; -import java.util.function.Predicate; - -/** - * System server-side proxy for ITimeZoneProvider implementations, i.e. this provides the - * system server object used to communicate with a remote {@link - * android.service.timezone.TimeZoneProviderService} over Binder, which could be running in a - * different process. As "remote" providers are bound / unbound this proxy will rebind to the "best" - * available remote process. - */ -class RealLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy { - - @NonNull private final ServiceWatcher mServiceWatcher; - - @GuardedBy("mSharedLock") - @Nullable private ManagerProxy mManagerProxy; - - @GuardedBy("mSharedLock") - @NonNull private TimeZoneProviderRequest mRequest; - - RealLocationTimeZoneProviderProxy( - @NonNull Context context, @NonNull Handler handler, - @NonNull ThreadingDomain threadingDomain, @NonNull String action, - int enableOverlayResId, int nonOverlayPackageResId) { - super(context, threadingDomain); - mManagerProxy = null; - mRequest = TimeZoneProviderRequest.createStopUpdatesRequest(); - - // A predicate that is used to confirm that an intent service can be used as a - // location-based TimeZoneProvider. The service must: - // 1) Declare android:permission="android.permission.BIND_TIME_ZONE_PROVIDER_SERVICE" - this - // ensures that the provider will only communicate with the system server. - // 2) Be in an application that has been granted the - // android.permission.INSTALL_LOCATION_TIME_ZONE_PROVIDER_SERVICE permission. This - // ensures only trusted time zone providers will be discovered. - final String requiredClientPermission = Manifest.permission.BIND_TIME_ZONE_PROVIDER_SERVICE; - final String requiredPermission = - Manifest.permission.INSTALL_LOCATION_TIME_ZONE_PROVIDER_SERVICE; - Predicate intentServiceCheckPredicate = resolveInfo -> { - ServiceInfo serviceInfo = resolveInfo.serviceInfo; - - boolean hasClientPermissionRequirement = - requiredClientPermission.equals(serviceInfo.permission); - - String packageName = serviceInfo.packageName; - PackageManager packageManager = context.getPackageManager(); - int checkResult = packageManager.checkPermission(requiredPermission, packageName); - boolean hasRequiredPermission = checkResult == PERMISSION_GRANTED; - - boolean result = hasClientPermissionRequirement && hasRequiredPermission; - if (!result) { - warnLog("resolveInfo=" + resolveInfo + " does not meet requirements:" - + " hasClientPermissionRequirement=" + hasClientPermissionRequirement - + ", hasRequiredPermission=" + hasRequiredPermission); - } - return result; - }; - mServiceWatcher = new ServiceWatcher(context, handler, action, this::onBind, this::onUnbind, - enableOverlayResId, nonOverlayPackageResId, intentServiceCheckPredicate); - } - - @Override - void onInitialize() { - if (!register()) { - throw new IllegalStateException("Unable to register binder proxy"); - } - } - - @Override - void onDestroy() { - mServiceWatcher.unregister(); - } - - private boolean register() { - boolean resolves = mServiceWatcher.checkServiceResolves(); - if (resolves) { - mServiceWatcher.register(); - } - return resolves; - } - - private void onBind(IBinder binder, ComponentName componentName) { - mThreadingDomain.assertCurrentThread(); - - synchronized (mSharedLock) { - // When a new remote is first bound we create the ManagerProxy that will be passed to - // it. By creating a new one for each bind the ManagerProxy can check whether it is - // still the current proxy and if not it can ignore incoming calls. - mManagerProxy = new ManagerProxy(); - mListener.onProviderBound(); - - // Send the current request to the remote. - trySendCurrentRequest(); - } - } - - private void onUnbind() { - mThreadingDomain.assertCurrentThread(); - - synchronized (mSharedLock) { - // Clear the ManagerProxy used with the old remote so we will ignore calls from any old - // remotes that somehow hold a reference to it. - mManagerProxy = null; - mListener.onProviderUnbound(); - } - } - - @Override - final void setRequest(@NonNull TimeZoneProviderRequest request) { - mThreadingDomain.assertCurrentThread(); - - Objects.requireNonNull(request); - synchronized (mSharedLock) { - mRequest = request; - - // Two possible outcomes here: Either we are already bound to a remote service, in - // which case trySendCurrentRequest() will communicate the request immediately, or we - // are not bound to the remote service yet, in which case it will be sent during - // onBindOnHandlerThread() instead. - trySendCurrentRequest(); - } - } - - @GuardedBy("mSharedLock") - private void trySendCurrentRequest() { - ManagerProxy managerProxy = mManagerProxy; - TimeZoneProviderRequest request = mRequest; - mServiceWatcher.runOnBinder(binder -> { - ITimeZoneProvider service = ITimeZoneProvider.Stub.asInterface(binder); - if (request.sendUpdates()) { - service.startUpdates(managerProxy, request.getInitializationTimeout().toMillis()); - } else { - service.stopUpdates(); - } - }); - } - - /** - * A stubbed implementation. - */ - @Override - void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) { - mThreadingDomain.assertCurrentThread(); - - if (callback != null) { - Bundle result = new Bundle(); - result.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, false); - result.putString(TEST_COMMAND_RESULT_ERROR_KEY, "Not implemented"); - callback.sendResult(result); - } - } - - @Override - public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) { - synchronized (mSharedLock) { - ipw.println("{RealLocationTimeZoneProviderProxy}"); - ipw.println("mRequest=" + mRequest); - mServiceWatcher.dump(null, ipw, args); - } - } - - /** - * A system Server-side proxy for the ITimeZoneProviderManager, i.e. this is a local binder stub - * Each "remote" TimeZoneProvider is passed a binder instance that it then uses to communicate - * back with the system server, invoking the logic here. - */ - private class ManagerProxy extends ITimeZoneProviderManager.Stub { - - // executed on binder thread - @Override - public void onTimeZoneProviderSuggestion(TimeZoneProviderSuggestion suggestion) { - onTimeZoneProviderEvent(TimeZoneProviderEvent.createSuggestionEvent(suggestion)); - } - - // executed on binder thread - @Override - public void onTimeZoneProviderUncertain() { - onTimeZoneProviderEvent(TimeZoneProviderEvent.createUncertainEvent()); - - } - - // executed on binder thread - @Override - public void onTimeZoneProviderPermanentFailure(String failureReason) { - onTimeZoneProviderEvent( - TimeZoneProviderEvent.createPermanentFailureEvent(failureReason)); - } - - private void onTimeZoneProviderEvent(TimeZoneProviderEvent event) { - synchronized (mSharedLock) { - if (mManagerProxy != this) { - // Ignore incoming calls if this instance is no longer the current - // mManagerProxy. - return; - } - } - handleTimeZoneProviderEvent(event); - } - } -} diff --git a/services/core/java/com/android/server/location/timezone/SimulatedLocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/location/timezone/SimulatedLocationTimeZoneProviderProxy.java deleted file mode 100644 index 66ccaed25e32..000000000000 --- a/services/core/java/com/android/server/location/timezone/SimulatedLocationTimeZoneProviderProxy.java +++ /dev/null @@ -1,211 +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.location.timezone; - -import static android.app.time.LocationTimeZoneManager.SIMULATED_PROVIDER_TEST_COMMAND_ON_BIND; -import static android.app.time.LocationTimeZoneManager.SIMULATED_PROVIDER_TEST_COMMAND_ON_UNBIND; -import static android.app.time.LocationTimeZoneManager.SIMULATED_PROVIDER_TEST_COMMAND_PERM_FAILURE; -import static android.app.time.LocationTimeZoneManager.SIMULATED_PROVIDER_TEST_COMMAND_SUCCESS; -import static android.app.time.LocationTimeZoneManager.SIMULATED_PROVIDER_TEST_COMMAND_SUCCESS_ARG_KEY_TZ; -import static android.app.time.LocationTimeZoneManager.SIMULATED_PROVIDER_TEST_COMMAND_UNCERTAIN; -import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_ERROR_KEY; -import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_SUCCESS_KEY; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; -import android.os.Bundle; -import android.os.RemoteCallback; -import android.os.SystemClock; -import android.service.timezone.TimeZoneProviderSuggestion; -import android.util.IndentingPrintWriter; - -import com.android.internal.annotations.GuardedBy; -import com.android.server.timezonedetector.ReferenceWithHistory; - -import java.io.PrintWriter; -import java.util.Arrays; -import java.util.Objects; - -/** - * A replacement for a real binder proxy for use during integration testing - * that can be used to inject simulated {@link LocationTimeZoneProviderProxy} behavior. - */ -class SimulatedLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy { - - @GuardedBy("mSharedLock") - @NonNull private TimeZoneProviderRequest mRequest; - - @GuardedBy("mSharedLock") - @NonNull private final ReferenceWithHistory mLastEvent = new ReferenceWithHistory<>(50); - - SimulatedLocationTimeZoneProviderProxy( - @NonNull Context context, @NonNull ThreadingDomain threadingDomain) { - super(context, threadingDomain); - mRequest = TimeZoneProviderRequest.createStopUpdatesRequest(); - } - - @Override - void onInitialize() { - // No-op - nothing to do for the simulated provider. - } - - @Override - void onDestroy() { - // No-op - nothing to do for the simulated provider. - } - - void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) { - mThreadingDomain.assertCurrentThread(); - - Objects.requireNonNull(testCommand); - - synchronized (mSharedLock) { - Bundle resultBundle = new Bundle(); - switch (testCommand.getName()) { - case SIMULATED_PROVIDER_TEST_COMMAND_ON_BIND: { - mLastEvent.set("Simulating onProviderBound(), testCommand=" + testCommand); - mThreadingDomain.post(this::onBindOnHandlerThread); - resultBundle.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, true); - break; - } - case SIMULATED_PROVIDER_TEST_COMMAND_ON_UNBIND: { - mLastEvent.set("Simulating onProviderUnbound(), testCommand=" + testCommand); - mThreadingDomain.post(this::onUnbindOnHandlerThread); - resultBundle.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, true); - break; - } - case SIMULATED_PROVIDER_TEST_COMMAND_PERM_FAILURE: - case SIMULATED_PROVIDER_TEST_COMMAND_UNCERTAIN: - case SIMULATED_PROVIDER_TEST_COMMAND_SUCCESS: { - if (!mRequest.sendUpdates()) { - String errorMsg = "testCommand=" + testCommand - + " is testing an invalid case:" - + " updates are off. mRequest=" + mRequest; - mLastEvent.set(errorMsg); - resultBundle.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, false); - resultBundle.putString(TEST_COMMAND_RESULT_ERROR_KEY, errorMsg); - break; - } - mLastEvent.set("Simulating TimeZoneProviderEvent, testCommand=" + testCommand); - TimeZoneProviderEvent timeZoneProviderEvent = - createTimeZoneProviderEventFromTestCommand(testCommand); - handleTimeZoneProviderEvent(timeZoneProviderEvent); - resultBundle.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, true); - break; - } - default: { - String errorMsg = "Unknown test event type. testCommand=" + testCommand; - mLastEvent.set(errorMsg); - resultBundle.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, false); - resultBundle.putString(TEST_COMMAND_RESULT_ERROR_KEY, errorMsg); - break; - } - } - if (callback != null) { - callback.sendResult(resultBundle); - } - } - } - - private void onBindOnHandlerThread() { - mThreadingDomain.assertCurrentThread(); - - synchronized (mSharedLock) { - mListener.onProviderBound(); - } - } - - private void onUnbindOnHandlerThread() { - mThreadingDomain.assertCurrentThread(); - - synchronized (mSharedLock) { - mListener.onProviderUnbound(); - } - } - - @Override - final void setRequest(@NonNull TimeZoneProviderRequest request) { - mThreadingDomain.assertCurrentThread(); - - Objects.requireNonNull(request); - synchronized (mSharedLock) { - mLastEvent.set("Request received: " + request); - mRequest = request; - } - } - - @Override - public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) { - synchronized (mSharedLock) { - ipw.println("{SimulatedLocationTimeZoneProviderProxy}"); - ipw.println("mRequest=" + mRequest); - ipw.println("mLastEvent=" + mLastEvent); - - ipw.increaseIndent(); - ipw.println("Last event history:"); - mLastEvent.dump(ipw); - ipw.decreaseIndent(); - } - } - - /** - * Prints the command line options that to create a {@link TestCommand} that can be passed to - * {@link #createTimeZoneProviderEventFromTestCommand(TestCommand)}. - */ - static void printTestCommandShellHelp(@NonNull PrintWriter pw) { - pw.printf("%s\n", SIMULATED_PROVIDER_TEST_COMMAND_ON_BIND); - pw.printf("%s\n", SIMULATED_PROVIDER_TEST_COMMAND_ON_UNBIND); - pw.printf("%s\n", SIMULATED_PROVIDER_TEST_COMMAND_PERM_FAILURE); - pw.printf("%s\n", SIMULATED_PROVIDER_TEST_COMMAND_UNCERTAIN); - pw.printf("%s %s=string_array:

{@link TestCommand}s can be encoded as arguments in a shell command. See - * {@link #createFromShellCommandArgs(ShellCommand)} and {@link - * #printShellCommandEncodingHelp(PrintWriter)}. - */ -final class TestCommand { - - private static final Pattern SHELL_ARG_PATTERN = Pattern.compile("([^=]+)=([^:]+):(.*)"); - private static final Pattern SHELL_ARG_VALUE_SPLIT_PATTERN = Pattern.compile("&"); - - @NonNull private final String mName; - @NonNull private final Bundle mArgs; - - /** Creates a {@link TestCommand} from components. */ - private TestCommand(@NonNull String type, @NonNull Bundle args) { - mName = Objects.requireNonNull(type); - mArgs = Objects.requireNonNull(args); - } - - @VisibleForTesting - @NonNull - public static TestCommand createForTests(@NonNull String type, @NonNull Bundle args) { - return new TestCommand(type, args); - } - - /** - * Creates a {@link TestCommand} from a {@link ShellCommand}'s remaining arguments. - * - * See {@link #printShellCommandEncodingHelp(PrintWriter)} for encoding details. - */ - @NonNull - public static TestCommand createFromShellCommandArgs(@NonNull ShellCommand shellCommand) { - String name = shellCommand.getNextArgRequired(); - Bundle args = new Bundle(); - String argKeyAndValue; - while ((argKeyAndValue = shellCommand.getNextArg()) != null) { - Matcher matcher = SHELL_ARG_PATTERN.matcher(argKeyAndValue); - if (!matcher.matches()) { - throw new IllegalArgumentException( - argKeyAndValue + " does not match " + SHELL_ARG_PATTERN); - } - String key = matcher.group(1); - String type = matcher.group(2); - String encodedValue = matcher.group(3); - Object value = getTypedValue(type, encodedValue); - args.putObject(key, value); - } - return new TestCommand(name, args); - } - - /** - * Returns the command's name. - */ - @NonNull - public String getName() { - return mName; - } - - /** - * Returns the arg values. Returns an empty bundle if there are no args. - */ - @NonNull - public Bundle getArgs() { - return mArgs.deepCopy(); - } - - @Override - public String toString() { - return "TestCommand{" - + "mName=" + mName - + ", mArgs=" + mArgs - + '}'; - } - - /** - * Prints the text format that {@link #createFromShellCommandArgs(ShellCommand)} understands. - */ - public static void printShellCommandEncodingHelp(@NonNull PrintWriter pw) { - pw.println("Test commands are encoded on the command line as: *"); - pw.println(); - pw.println("The is a string"); - pw.println("The encoding is: \"key=type:value\""); - pw.println(); - pw.println("e.g. \"myKey=string:myValue\" represents an argument with the key \"myKey\"" - + " and a string value of \"myValue\""); - pw.println("Values are one or more URI-encoded strings separated by & characters. Only some" - + " types support multiple values, e.g. string arrays."); - pw.println(); - pw.println("Recognized types are: string, boolean, double, long, string_array."); - pw.println(); - pw.println("When passing test commands via adb shell, the & can be escaped by quoting the" - + " and escaping the & with \\"); - pw.println("For example:"); - pw.println(" $ adb shell ... my-command \"key1=string_array:value1\\&value2\""); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - TestCommand that = (TestCommand) o; - return mName.equals(that.mName) - && mArgs.kindofEquals(that.mArgs); - } - - @Override - public int hashCode() { - return Objects.hash(mName, mArgs); - } - - - private static Object getTypedValue(String type, String encodedValue) { - // The value is stored in a URL encoding. Multiple value types have values separated with - // a & character. - String[] values = SHELL_ARG_VALUE_SPLIT_PATTERN.split(encodedValue); - - // URI decode the values. - for (int i = 0; i < values.length; i++) { - values[i] = Uri.decode(values[i]); - } - - switch (type) { - case "boolean": { - checkSingleValue(values); - return Boolean.parseBoolean(values[0]); - } - case "double": { - checkSingleValue(values); - return Double.parseDouble(values[0]); - } - case "long": { - checkSingleValue(values); - return Long.parseLong(values[0]); - } - case "string": { - checkSingleValue(values); - return values[0]; - } - case "string_array": { - return values; - } - default: { - throw new IllegalArgumentException("Unknown type: " + type); - } - } - - } - - private static void checkSingleValue(String[] values) { - if (values.length != 1) { - throw new IllegalArgumentException("Expected a single value, but there were multiple: " - + Arrays.toString(values)); - } - } -} diff --git a/services/core/java/com/android/server/location/timezone/ThreadingDomain.java b/services/core/java/com/android/server/location/timezone/ThreadingDomain.java deleted file mode 100644 index 4ada6f50b40e..000000000000 --- a/services/core/java/com/android/server/location/timezone/ThreadingDomain.java +++ /dev/null @@ -1,190 +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.location.timezone; - -import android.annotation.DurationMillisLong; -import android.annotation.NonNull; - -import com.android.internal.util.Preconditions; - -import java.util.concurrent.Callable; - -/** - * A class that can be used to enforce / indicate a set of components that need to share threading - * behavior such as a shared lock object and a common thread, with async execution support. - * - *

It is not essential that the object returned by {@link #getLockObject()} is only used - * when executing on the domain's thread, but users should be careful to avoid deadlocks when - * multiple locks / threads are in use. Generally sticking to a single thread / lock is safest. - */ -abstract class ThreadingDomain { - - @NonNull private final Object mLockObject; - - ThreadingDomain() { - mLockObject = new Object(); - } - - /** - * Returns the common lock object for this threading domain that can be used for synchronized () - * blocks. The lock is unique to this threading domain. - */ - @NonNull - Object getLockObject() { - return mLockObject; - } - - /** - * Returns the Thread associated with this threading domain. - */ - @NonNull - abstract Thread getThread(); - - /** - * Asserts the currently executing thread is the one associated with this threading domain. - * Generally useful for documenting expectations in the code. By asserting a single thread is - * being used within a set of components, a lot of races can be avoided. - */ - void assertCurrentThread() { - Preconditions.checkState(Thread.currentThread() == getThread()); - } - - /** - * Asserts the currently executing thread is not the one associated with this threading domain. - * Generally useful for documenting expectations in the code and avoiding deadlocks. - */ - void assertNotCurrentThread() { - Preconditions.checkState(Thread.currentThread() != getThread()); - } - - /** - * Execute the supplied runnable on the threading domain's thread. - */ - abstract void post(@NonNull Runnable runnable); - - /** - * Executes the supplied runnable and waits for up to the duration specified for it to be - * executed. This is only intended for use by test and/or shell command code as it consumes - * multiple threads and could lead to deadlocks. - * - *

An {@link IllegalStateException} will be thrown if calling this method would cause a - * deadlock, e.g. if it is called using the threading domain's own thread. - */ - final void postAndWait(@NonNull Runnable runnable, @DurationMillisLong long durationMillis) { - try { - postAndWait(() -> { - runnable.run(); - return null; - }, durationMillis); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** - * Executes the supplied callable and waits for up to the duration specified for it to be - * executed. This is only intended for use by test and/or shell command code as it consumes - * multiple threads and could lead to deadlocks. - * - *

An {@link IllegalStateException} will be thrown if calling this method would cause a - * deadlock, e.g. if it is called using the threading domain's own thread. - */ - abstract V postAndWait( - @NonNull Callable callable, @DurationMillisLong long durationMillis) - throws Exception; - - /** - * Execute the supplied runnable on the threading domain's thread with a delay. - */ - abstract void postDelayed(@NonNull Runnable runnable, @DurationMillisLong long delayMillis); - - abstract void postDelayed(Runnable r, Object token, @DurationMillisLong long delayMillis); - - abstract void removeQueuedRunnables(Object token); - - /** - * Creates a new {@link SingleRunnableQueue} that can be used to ensure that (at most) a - * single runnable for a given purpose is ever queued. Create new ones for different purposes. - */ - SingleRunnableQueue createSingleRunnableQueue() { - return new SingleRunnableQueue(); - } - - /** - * 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 { - - private boolean mIsQueued; - @DurationMillisLong - private long mDelayMillis; - - /** - * Posts the supplied {@link Runnable} asynchronously and delayed on the threading domain - * handler 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 runDelayed(Runnable r, @DurationMillisLong long delayMillis) { - cancel(); - 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. - */ - @DurationMillisLong - 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(); - if (mIsQueued) { - removeQueuedRunnables(this); - } - mIsQueued = false; - mDelayMillis = -1; - } - } -} diff --git a/services/core/java/com/android/server/location/timezone/TimeZoneProviderEvent.java b/services/core/java/com/android/server/location/timezone/TimeZoneProviderEvent.java deleted file mode 100644 index 2d6f8ad446f0..000000000000 --- a/services/core/java/com/android/server/location/timezone/TimeZoneProviderEvent.java +++ /dev/null @@ -1,143 +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.location.timezone; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.service.timezone.TimeZoneProviderService; -import android.service.timezone.TimeZoneProviderSuggestion; - -import java.util.Objects; - -/** - * An event from a {@link TimeZoneProviderService}. - */ -final class TimeZoneProviderEvent { - - @IntDef(prefix = "EVENT_TYPE_", - value = { EVENT_TYPE_PERMANENT_FAILURE, EVENT_TYPE_SUGGESTION, EVENT_TYPE_UNCERTAIN }) - public @interface EventType {} - - /** - * The provider failed permanently. See {@link - * TimeZoneProviderService#reportPermanentFailure(Throwable)} - */ - public static final int EVENT_TYPE_PERMANENT_FAILURE = 1; - - /** - * The provider made a suggestion. See {@link - * TimeZoneProviderService#reportSuggestion(TimeZoneProviderSuggestion)} - */ - public static final int EVENT_TYPE_SUGGESTION = 2; - - /** - * The provider was uncertain about the time zone. See {@link - * TimeZoneProviderService#reportUncertain()} - */ - public static final int EVENT_TYPE_UNCERTAIN = 3; - - private static final TimeZoneProviderEvent UNCERTAIN_EVENT = - new TimeZoneProviderEvent(EVENT_TYPE_UNCERTAIN, null, null); - - @EventType - private final int mType; - - @Nullable - private final TimeZoneProviderSuggestion mSuggestion; - - @Nullable - private final String mFailureCause; - - private TimeZoneProviderEvent(@EventType int type, - @Nullable TimeZoneProviderSuggestion suggestion, - @Nullable String failureCause) { - mType = type; - mSuggestion = suggestion; - mFailureCause = failureCause; - } - - /** Returns a event of type {@link #EVENT_TYPE_SUGGESTION}. */ - public static TimeZoneProviderEvent createSuggestionEvent( - @NonNull TimeZoneProviderSuggestion suggestion) { - return new TimeZoneProviderEvent(EVENT_TYPE_SUGGESTION, - Objects.requireNonNull(suggestion), null); - } - - /** Returns a event of type {@link #EVENT_TYPE_UNCERTAIN}. */ - public static TimeZoneProviderEvent createUncertainEvent() { - return UNCERTAIN_EVENT; - } - - /** Returns a event of type {@link #EVENT_TYPE_PERMANENT_FAILURE}. */ - public static TimeZoneProviderEvent createPermanentFailureEvent(@NonNull String cause) { - return new TimeZoneProviderEvent(EVENT_TYPE_PERMANENT_FAILURE, null, - Objects.requireNonNull(cause)); - } - - /** - * Returns the event type. - */ - public @EventType int getType() { - return mType; - } - - /** - * Returns the suggestion. Populated when {@link #getType()} is {@link #EVENT_TYPE_SUGGESTION}. - */ - @Nullable - public TimeZoneProviderSuggestion getSuggestion() { - return mSuggestion; - } - - /** - * Returns the failure cauese. Populated when {@link #getType()} is {@link - * #EVENT_TYPE_PERMANENT_FAILURE}. - */ - @Nullable - public String getFailureCause() { - return mFailureCause; - } - - @Override - public String toString() { - return "TimeZoneProviderEvent{" - + "mType=" + mType - + ", mSuggestion=" + mSuggestion - + ", mFailureCause=" + mFailureCause - + '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - TimeZoneProviderEvent that = (TimeZoneProviderEvent) o; - return mType == that.mType - && Objects.equals(mSuggestion, that.mSuggestion) - && Objects.equals(mFailureCause, that.mFailureCause); - } - - @Override - public int hashCode() { - return Objects.hash(mType, mSuggestion, mFailureCause); - } -} diff --git a/services/core/java/com/android/server/location/timezone/TimeZoneProviderRequest.java b/services/core/java/com/android/server/location/timezone/TimeZoneProviderRequest.java deleted file mode 100644 index 649a74bbed49..000000000000 --- a/services/core/java/com/android/server/location/timezone/TimeZoneProviderRequest.java +++ /dev/null @@ -1,105 +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.location.timezone; - -import android.annotation.NonNull; -import android.annotation.Nullable; - -import java.time.Duration; -import java.util.Objects; - -/** - * This class encapsulates a request to a provider. - */ -final class TimeZoneProviderRequest { - - @NonNull - private static final TimeZoneProviderRequest STOP_UPDATES = - new TimeZoneProviderRequest( - false /* sendUpdates */, - null /* initializationTimeout */); - - private final boolean mSendUpdates; - - @Nullable - private final Duration mInitializationTimeout; - - private TimeZoneProviderRequest( - boolean sendUpdates, @Nullable Duration initializationTimeout) { - mSendUpdates = sendUpdates; - mInitializationTimeout = initializationTimeout; - } - - /** Creates a request to start updates with the specified timeout. */ - public static TimeZoneProviderRequest createStartUpdatesRequest( - @NonNull Duration initializationTimeout) { - return new TimeZoneProviderRequest(true, Objects.requireNonNull(initializationTimeout)); - } - - /** Creates a request to stop updates. */ - public static TimeZoneProviderRequest createStopUpdatesRequest() { - return STOP_UPDATES; - } - - /** - * Returns {@code true} if the provider should send updates related to the device's current - * time zone, {@code false} otherwise. - */ - public boolean sendUpdates() { - return mSendUpdates; - } - - // TODO(b/152744911) - once there are a couple of implementations, decide whether this needs to - // be passed to the TimeZoneProviderService and remove if it is not useful. - /** - * Returns the maximum time that the provider is allowed to initialize before it is expected to - * send an event of any sort. Only valid when {@link #sendUpdates()} is {@code true}. Failure to - * send an event in this time (with some fuzz) may be interpreted as if the provider is - * uncertain of the time zone, and/or it could lead to the provider being stopped. - */ - @Nullable - public Duration getInitializationTimeout() { - return mInitializationTimeout; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - TimeZoneProviderRequest - that = (TimeZoneProviderRequest) o; - return mSendUpdates == that.mSendUpdates - && mInitializationTimeout == that.mInitializationTimeout; - } - - @Override - public int hashCode() { - return Objects.hash(mSendUpdates, mInitializationTimeout); - } - - @Override - public String toString() { - return "TimeZoneProviderRequest{" - + "mSendUpdates=" + mSendUpdates - + ", mInitializationTimeout=" + mInitializationTimeout - + "}"; - } -} diff --git a/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java b/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java new file mode 100644 index 000000000000..c0c9e6d58622 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java @@ -0,0 +1,212 @@ +/* + * 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 com.android.server.timezonedetector.location.LocationTimeZoneManagerService.debugLog; +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.NonNull; +import android.annotation.Nullable; +import android.os.RemoteCallback; +import android.util.IndentingPrintWriter; + +import java.time.Duration; +import java.util.Objects; + +/** + * The real, system-server side implementation of a binder call backed {@link + * LocationTimeZoneProvider}. It handles keeping track of current state, timeouts and ensuring + * events are passed to the {@link LocationTimeZoneProviderController} on the required thread. + */ +class BinderLocationTimeZoneProvider extends LocationTimeZoneProvider { + + private static final String TAG = LocationTimeZoneManagerService.TAG; + + @NonNull private final LocationTimeZoneProviderProxy mProxy; + + BinderLocationTimeZoneProvider( + @NonNull ThreadingDomain threadingDomain, + @NonNull String providerName, + @NonNull LocationTimeZoneProviderProxy proxy) { + super(threadingDomain, providerName); + mProxy = Objects.requireNonNull(proxy); + } + + @Override + void onInitialize() { + mProxy.initialize(new LocationTimeZoneProviderProxy.Listener() { + @Override + public void onReportTimeZoneProviderEvent( + @NonNull TimeZoneProviderEvent timeZoneProviderEvent) { + handleTimeZoneProviderEvent(timeZoneProviderEvent); + } + + @Override + public void onProviderBound() { + handleOnProviderBound(); + } + + @Override + public void onProviderUnbound() { + handleProviderLost("onProviderUnbound()"); + } + }); + } + + @Override + void onDestroy() { + mProxy.destroy(); + } + + private void handleProviderLost(String reason) { + mThreadingDomain.assertCurrentThread(); + + synchronized (mSharedLock) { + ProviderState currentState = mCurrentState.get(); + switch (currentState.stateEnum) { + case PROVIDER_STATE_STARTED_INITIALIZING: + case PROVIDER_STATE_STARTED_UNCERTAIN: + case PROVIDER_STATE_STARTED_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_STARTED_UNCERTAIN state because + // event == null + ProviderState newState = currentState.newState( + PROVIDER_STATE_STARTED_UNCERTAIN, null, + currentState.currentUserConfiguration, msg); + setCurrentState(newState, true); + break; + } + case PROVIDER_STATE_STOPPED: { + debugLog("handleProviderLost reason=" + reason + + ", mProviderName=" + mProviderName + + ", currentState=" + currentState + + ": No state change required, provider is stopped."); + break; + } + case PROVIDER_STATE_PERM_FAILED: + case PROVIDER_STATE_DESTROYED: { + debugLog("handleProviderLost reason=" + reason + + ", mProviderName=" + mProviderName + + ", currentState=" + currentState + + ": No state change required, provider is terminated."); + break; + } + default: { + throw new IllegalStateException("Unknown currentState=" + currentState); + } + } + } + } + + private void handleOnProviderBound() { + mThreadingDomain.assertCurrentThread(); + + synchronized (mSharedLock) { + ProviderState currentState = mCurrentState.get(); + switch (currentState.stateEnum) { + case PROVIDER_STATE_STARTED_INITIALIZING: + case PROVIDER_STATE_STARTED_CERTAIN: + case PROVIDER_STATE_STARTED_UNCERTAIN: { + debugLog("handleOnProviderBound mProviderName=" + mProviderName + + ", currentState=" + currentState + ": Provider is started."); + break; + } + case PROVIDER_STATE_STOPPED: { + debugLog("handleOnProviderBound mProviderName=" + mProviderName + + ", currentState=" + currentState + ": Provider is stopped."); + break; + } + case PROVIDER_STATE_PERM_FAILED: + case PROVIDER_STATE_DESTROYED: { + debugLog("handleOnProviderBound" + + ", mProviderName=" + mProviderName + + ", currentState=" + currentState + + ": No state change required, provider is terminated."); + break; + } + default: { + throw new IllegalStateException("Unknown currentState=" + currentState); + } + } + } + } + + @Override + void onStartUpdates(@NonNull Duration initializationTimeout) { + // Set a request on the proxy - it will be sent immediately if the service is bound, + // or will be sent as soon as the service becomes bound. + TimeZoneProviderRequest request = + TimeZoneProviderRequest.createStartUpdatesRequest(initializationTimeout); + mProxy.setRequest(request); + } + + @Override + void onStopUpdates() { + TimeZoneProviderRequest request = TimeZoneProviderRequest.createStopUpdatesRequest(); + mProxy.setRequest(request); + } + + /** + * Passes the supplied test command to the current proxy. + */ + @Override + void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) { + mThreadingDomain.assertCurrentThread(); + + mProxy.handleTestCommand(testCommand, callback); + } + + @Override + public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) { + synchronized (mSharedLock) { + ipw.println("{BinderLocationTimeZoneProvider}"); + ipw.println("mProviderName=" + mProviderName); + ipw.println("mCurrentState=" + mCurrentState); + ipw.println("mProxy=" + mProxy); + + ipw.println("State history:"); + ipw.increaseIndent(); + mCurrentState.dump(ipw); + ipw.decreaseIndent(); + + ipw.println("Proxy details:"); + ipw.increaseIndent(); + mProxy.dump(ipw, args); + ipw.decreaseIndent(); + } + } + + @Override + public String toString() { + synchronized (mSharedLock) { + return "BinderLocationTimeZoneProvider{" + + "mProviderName=" + mProviderName + + ", mCurrentState=" + mCurrentState + + ", mProxy=" + mProxy + + '}'; + } + } +} diff --git a/services/core/java/com/android/server/timezonedetector/location/ControllerCallbackImpl.java b/services/core/java/com/android/server/timezonedetector/location/ControllerCallbackImpl.java new file mode 100644 index 000000000000..46eaad075b54 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/location/ControllerCallbackImpl.java @@ -0,0 +1,43 @@ +/* + * 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 android.annotation.NonNull; + +import com.android.server.LocalServices; +import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion; +import com.android.server.timezonedetector.TimeZoneDetectorInternal; + +/** + * The real implementation of {@link LocationTimeZoneProviderController.Callback} used by + * {@link ControllerImpl} to interact with other server components. + */ +class ControllerCallbackImpl extends LocationTimeZoneProviderController.Callback { + + ControllerCallbackImpl(@NonNull ThreadingDomain threadingDomain) { + super(threadingDomain); + } + + @Override + void suggest(@NonNull GeolocationTimeZoneSuggestion suggestion) { + mThreadingDomain.assertCurrentThread(); + + TimeZoneDetectorInternal timeZoneDetector = + LocalServices.getService(TimeZoneDetectorInternal.class); + timeZoneDetector.suggestGeolocationTimeZone(suggestion); + } +} diff --git a/services/core/java/com/android/server/timezonedetector/location/ControllerEnvironmentImpl.java b/services/core/java/com/android/server/timezonedetector/location/ControllerEnvironmentImpl.java new file mode 100644 index 000000000000..83b33ee75de9 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/location/ControllerEnvironmentImpl.java @@ -0,0 +1,83 @@ +/* + * 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 android.annotation.NonNull; + +import com.android.server.LocalServices; +import com.android.server.timezonedetector.ConfigurationChangeListener; +import com.android.server.timezonedetector.ConfigurationInternal; +import com.android.server.timezonedetector.TimeZoneDetectorInternal; + +import java.time.Duration; +import java.util.Objects; + +/** + * The real implementation of {@link LocationTimeZoneProviderController.Environment} used by + * {@link ControllerImpl} to interact with other server components. + */ +class ControllerEnvironmentImpl extends LocationTimeZoneProviderController.Environment { + + private static final Duration PROVIDER_INITIALIZATION_TIMEOUT = Duration.ofMinutes(5); + private static final Duration PROVIDER_INITIALIZATION_TIMEOUT_FUZZ = Duration.ofMinutes(1); + private static final Duration PROVIDER_UNCERTAINTY_DELAY = Duration.ofMinutes(5); + + @NonNull private final TimeZoneDetectorInternal mTimeZoneDetectorInternal; + @NonNull private final LocationTimeZoneProviderController mController; + @NonNull private final ConfigurationChangeListener mConfigurationChangeListener; + + ControllerEnvironmentImpl(@NonNull ThreadingDomain threadingDomain, + @NonNull LocationTimeZoneProviderController controller) { + super(threadingDomain); + mController = Objects.requireNonNull(controller); + mTimeZoneDetectorInternal = LocalServices.getService(TimeZoneDetectorInternal.class); + + // Listen for configuration changes. + mConfigurationChangeListener = () -> mThreadingDomain.post(mController::onConfigChanged); + mTimeZoneDetectorInternal.addConfigurationListener(mConfigurationChangeListener); + } + + + @Override + void destroy() { + mTimeZoneDetectorInternal.removeConfigurationListener(mConfigurationChangeListener); + } + + @Override + @NonNull + ConfigurationInternal getCurrentUserConfigurationInternal() { + return mTimeZoneDetectorInternal.getCurrentUserConfigurationInternal(); + } + + @Override + @NonNull + Duration getProviderInitializationTimeout() { + return PROVIDER_INITIALIZATION_TIMEOUT; + } + + @Override + @NonNull + Duration getProviderInitializationTimeoutFuzz() { + return PROVIDER_INITIALIZATION_TIMEOUT_FUZZ; + } + + @Override + @NonNull + Duration getUncertaintyDelay() { + return PROVIDER_UNCERTAINTY_DELAY; + } +} diff --git a/services/core/java/com/android/server/timezonedetector/location/ControllerImpl.java b/services/core/java/com/android/server/timezonedetector/location/ControllerImpl.java new file mode 100644 index 000000000000..fb2a18493b7d --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/location/ControllerImpl.java @@ -0,0 +1,673 @@ +/* + * 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 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 static com.android.server.timezonedetector.location.TimeZoneProviderEvent.EVENT_TYPE_PERMANENT_FAILURE; +import static com.android.server.timezonedetector.location.TimeZoneProviderEvent.EVENT_TYPE_SUGGESTION; +import static com.android.server.timezonedetector.location.TimeZoneProviderEvent.EVENT_TYPE_UNCERTAIN; + +import android.annotation.DurationMillisLong; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.RemoteCallback; +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.List; +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 onConfigChanged() { + 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(); + + // If the controller has made a "certain" suggestion, it should make an uncertain + // suggestion to cancel it. + if (mLastSuggestion != null && mLastSuggestion.getZoneIds() != null) { + makeSuggestion(createUncertainSuggestion("Controller is destroyed")); + } + } + } + + @GuardedBy("mSharedLock") + private void stopProviders() { + stopProviderIfStarted(mPrimaryProvider); + stopProviderIfStarted(mSecondaryProvider); + + // By definition, if both providers are stopped, the controller is uncertain. + cancelUncertaintyTimeout(); + } + + @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( + "Providers are failed:" + + " primary=" + mPrimaryProvider.getCurrentState() + + " secondary=" + mPrimaryProvider.getCurrentState()); + makeSuggestion(suggestion); + } + } + } else { + stopProviders(); + + // 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-started). + if (mLastSuggestion != null && mLastSuggestion.getZoneIds() != null) { + GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion( + "Provider is stopped:" + + " primary=" + mPrimaryProvider.getCurrentState()); + makeSuggestion(suggestion); + } + } + } + + 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()); + 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( + "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. + handleProviderUncertainty(provider, "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: { + handleProviderUncertainty(provider, "provider=" + provider + + ", explicit uncertainty. event=" + event); + break; + } + case EVENT_TYPE_SUGGESTION: { + handleProviderSuggestion(provider, event.getSuggestion().getTimeZoneIds(), + "Event received provider=" + provider + ", event=" + 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, + @Nullable List timeZoneIds, + @NonNull String reason) { + // By definition, the controller is now certain. + cancelUncertaintyTimeout(); + + if (provider == mPrimaryProvider) { + stopProviderIfStarted(mSecondaryProvider); + } + + GeolocationTimeZoneSuggestion suggestion = new GeolocationTimeZoneSuggestion(timeZoneIds); + suggestion.addDebugInfo(reason); + // Rely on the receiver to dedupe suggestions. It is better to over-communicate. + makeSuggestion(suggestion); + } + + @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, @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 delay = mEnvironment.getUncertaintyDelay(); + mUncertaintyTimeoutQueue.runDelayed(() -> onProviderUncertaintyTimeout(provider), + delay.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) { + mThreadingDomain.assertCurrentThread(); + + synchronized (mSharedLock) { + GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion( + "Uncertainty timeout triggered for " + provider.getName() + ":" + + " primary=" + mPrimaryProvider + + ", secondary=" + mSecondaryProvider); + makeSuggestion(suggestion); + } + } + + @NonNull + private static GeolocationTimeZoneSuggestion createUncertainSuggestion(@NonNull String reason) { + GeolocationTimeZoneSuggestion suggestion = new GeolocationTimeZoneSuggestion(null); + suggestion.addDebugInfo(reason); + return suggestion; + } + + /** + * Passes a test command to the specified provider. If the provider name does not match a + * known provider, then the command is logged and discarded. + */ + void handleProviderTestCommand( + @NonNull String providerName, @NonNull TestCommand testCommand, + @Nullable RemoteCallback callback) { + mThreadingDomain.assertCurrentThread(); + + LocationTimeZoneProvider targetProvider = getLocationTimeZoneProvider(providerName); + if (targetProvider == null) { + warnLog("Unable to process test command:" + + " providerName=" + providerName + ", testCommand=" + testCommand); + return; + } + + synchronized (mSharedLock) { + try { + targetProvider.handleTestCommand(testCommand, callback); + } catch (Exception e) { + warnLog("Unable to process test command:" + + " providerName=" + providerName + ", testCommand=" + testCommand, e); + } + } + } + + /** + * Sets whether the controller should record provider state changes for later dumping via + * {@link #getStateForTests()}. + */ + void setProviderStateRecordingEnabled(boolean enabled) { + mThreadingDomain.assertCurrentThread(); + + synchronized (mSharedLock) { + mPrimaryProvider.setStateChangeRecordingEnabled(enabled); + mSecondaryProvider.setStateChangeRecordingEnabled(enabled); + } + } + + /** + * 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(); + } + } + + @Nullable + private LocationTimeZoneProvider getLocationTimeZoneProvider(@NonNull String providerName) { + LocationTimeZoneProvider targetProvider; + if (Objects.equals(mPrimaryProvider.getName(), providerName)) { + targetProvider = mPrimaryProvider; + } else if (Objects.equals(mSecondaryProvider.getName(), providerName)) { + targetProvider = mSecondaryProvider; + } else { + warnLog("Bad providerName=" + providerName); + targetProvider = null; + } + return targetProvider; + } +} diff --git a/services/core/java/com/android/server/timezonedetector/location/HandlerThreadingDomain.java b/services/core/java/com/android/server/timezonedetector/location/HandlerThreadingDomain.java new file mode 100644 index 000000000000..0dd2922bb240 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/location/HandlerThreadingDomain.java @@ -0,0 +1,110 @@ +/* + * 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 android.annotation.DurationMillisLong; +import android.annotation.NonNull; +import android.os.Handler; + +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * The real implementation of {@link ThreadingDomain} that uses a {@link Handler}. + */ +final class HandlerThreadingDomain extends ThreadingDomain { + + @NonNull private final Handler mHandler; + + HandlerThreadingDomain(Handler handler) { + mHandler = Objects.requireNonNull(handler); + } + + /** + * Returns the {@link Handler} associated with this threading domain. The same {@link Handler} + * may be associated with multiple threading domains, e.g. multiple threading domains could + * choose to use the {@link com.android.server.FgThread} handler. + * + *

If you find yourself making this public because you need a {@link Handler}, then it may + * cause problems with testability. Try to avoid using this method and use methods like {@link + * #post(Runnable)} instead. + */ + @NonNull + Handler getHandler() { + return mHandler; + } + + @NonNull + Thread getThread() { + return getHandler().getLooper().getThread(); + } + + @Override + void post(@NonNull Runnable r) { + getHandler().post(r); + } + + @Override + V postAndWait(@NonNull Callable callable, @DurationMillisLong long durationMillis) + throws Exception { + // Calling this on this domain's thread would lead to deadlock. + assertNotCurrentThread(); + + AtomicReference resultReference = new AtomicReference<>(); + AtomicReference exceptionReference = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + post(() -> { + try { + resultReference.set(callable.call()); + } catch (Exception e) { + exceptionReference.set(e); + } finally { + latch.countDown(); + } + }); + + try { + if (!latch.await(durationMillis, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("Timed out"); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + if (exceptionReference.get() != null) { + throw exceptionReference.get(); + } + return resultReference.get(); + } + + @Override + void postDelayed(@NonNull Runnable r, @DurationMillisLong long delayMillis) { + getHandler().postDelayed(r, delayMillis); + } + + @Override + void postDelayed(Runnable r, Object token, @DurationMillisLong long delayMillis) { + getHandler().postDelayed(r, token, delayMillis); + } + + @Override + void removeQueuedRunnables(Object token) { + getHandler().removeCallbacksAndMessages(token); + } +} diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java new file mode 100644 index 000000000000..5bee7ee9d4b2 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java @@ -0,0 +1,491 @@ +/* + * 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.app.time.LocationTimeZoneManager.PRIMARY_PROVIDER_NAME; +import static android.app.time.LocationTimeZoneManager.PROVIDER_MODE_OVERRIDE_DISABLED; +import static android.app.time.LocationTimeZoneManager.PROVIDER_MODE_OVERRIDE_NONE; +import static android.app.time.LocationTimeZoneManager.PROVIDER_MODE_OVERRIDE_SIMULATED; +import static android.app.time.LocationTimeZoneManager.SECONDARY_PROVIDER_NAME; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteCallback; +import android.os.ResultReceiver; +import android.os.ShellCallback; +import android.service.timezone.TimeZoneProviderService; +import android.util.IndentingPrintWriter; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.DumpUtils; +import com.android.internal.util.Preconditions; +import com.android.server.FgThread; +import com.android.server.SystemService; +import com.android.server.timezonedetector.TimeZoneDetectorInternal; +import com.android.server.timezonedetector.TimeZoneDetectorService; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * A service class that acts as a container for the {@link LocationTimeZoneProviderController}, + * which determines what {@link com.android.server.timezonedetector.GeolocationTimeZoneSuggestion} + * are made to the {@link TimeZoneDetectorInternal}, and the {@link LocationTimeZoneProvider}s that + * (indirectly) generate {@link TimeZoneProviderEvent}s. + * + *

For details of the time zone suggestion behavior, see {@link + * LocationTimeZoneProviderController}. + * + *

Implementation details: + * + *

For simplicity, with the exception of a few outliers like {@link #dump}, all processing in + * this service (and package-private helper objects) takes place on a single thread / handler, the + * one indicated by {@link ThreadingDomain}. Because methods like {@link #dump} can be invoked on + * another thread, the service and its related objects must still be thread-safe. + * + *

For testing / reproduction of bugs, it is possible to put providers into "simulation + * mode" where the real binder clients are replaced by {@link + * SimulatedLocationTimeZoneProviderProxy}. This means that the real client providers are never + * bound (ensuring no real location events will be received) and simulated events / behaviors + * can be injected via the command line. To enter simulation mode for a provider, use + * "{@code adb shell setprop persist.sys.location_tz_simulation_mode. 1}" and reboot. + * e.g. "{@code adb shell setprop persist.sys.location_tz_simulation_mode.primary 1}}" + * Then use "{@code adb shell cmd location_time_zone_manager help}" for injection. Set the system + * properties to "0" and reboot to return to exit simulation mode. + */ +public class LocationTimeZoneManagerService extends Binder { + + /** + * Controls lifecycle of the {@link LocationTimeZoneManagerService}. + */ + public static class Lifecycle extends SystemService { + + private LocationTimeZoneManagerService mService; + + public Lifecycle(@NonNull Context context) { + super(Objects.requireNonNull(context)); + } + + @Override + public void onStart() { + Context context = getContext(); + if (TimeZoneDetectorService.isGeoLocationTimeZoneDetectionEnabled(context)) { + mService = new LocationTimeZoneManagerService(context); + + // The service currently exposes no LocalService or Binder API, but it extends + // Binder and is registered as a binder service so it can receive shell commands. + publishBinderService("location_time_zone_manager", mService); + } else { + Slog.i(TAG, getClass() + " is disabled"); + } + } + + @Override + public void onBootPhase(int phase) { + Context context = getContext(); + if (TimeZoneDetectorService.isGeoLocationTimeZoneDetectionEnabled(context)) { + if (phase == PHASE_SYSTEM_SERVICES_READY) { + // The location service must be functioning after this boot phase. + mService.onSystemReady(); + } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { + // Some providers rely on non-platform code (e.g. gcore), so we wait to + // initialize providers until third party code is allowed to run. + mService.onSystemThirdPartyAppsCanStart(); + } + } + } + } + + static final String TAG = "LocationTZDetector"; + + private static final long BLOCKING_OP_WAIT_DURATION_MILLIS = Duration.ofSeconds(20).toMillis(); + + private static final String ATTRIBUTION_TAG = "LocationTimeZoneService"; + + private static final String PRIMARY_LOCATION_TIME_ZONE_SERVICE_ACTION = + TimeZoneProviderService.PRIMARY_LOCATION_TIME_ZONE_PROVIDER_SERVICE_INTERFACE; + private static final String SECONDARY_LOCATION_TIME_ZONE_SERVICE_ACTION = + TimeZoneProviderService.SECONDARY_LOCATION_TIME_ZONE_PROVIDER_SERVICE_INTERFACE; + + + @NonNull private final Context mContext; + + /** + * The {@link ThreadingDomain} used to supply the shared lock object used by the controller and + * related components. + * + *

Most operations are executed on the associated handler thread but not all, hence + * the requirement for additional synchronization using a shared lock. + */ + @NonNull private final ThreadingDomain mThreadingDomain; + + /** A handler associated with the {@link #mThreadingDomain}. */ + @NonNull private final Handler mHandler; + + /** The shared lock from {@link #mThreadingDomain}. */ + @NonNull private final Object mSharedLock; + + // Lazily initialized. Can be null if the service has been stopped. + @GuardedBy("mSharedLock") + private ControllerImpl mLocationTimeZoneDetectorController; + + // Lazily initialized. Can be null if the service has been stopped. + @GuardedBy("mSharedLock") + private ControllerEnvironmentImpl mEnvironment; + + @GuardedBy("mSharedLock") + @NonNull + private String mPrimaryProviderModeOverride = PROVIDER_MODE_OVERRIDE_NONE; + + @GuardedBy("mSharedLock") + @NonNull + private String mSecondaryProviderModeOverride = PROVIDER_MODE_OVERRIDE_NONE; + + LocationTimeZoneManagerService(Context context) { + mContext = context.createAttributionContext(ATTRIBUTION_TAG); + mHandler = FgThread.getHandler(); + mThreadingDomain = new HandlerThreadingDomain(mHandler); + mSharedLock = mThreadingDomain.getLockObject(); + } + + void onSystemReady() { + // Called on an arbitrary thread during initialization. + synchronized (mSharedLock) { + // TODO(b/152744911): LocationManagerService watches for packages disappearing. Need to + // do anything here? + + // TODO(b/152744911): LocationManagerService watches for foreground app changes. Need to + // do anything here? + // TODO(b/152744911): LocationManagerService watches screen state. Need to do anything + // here? + } + } + + void onSystemThirdPartyAppsCanStart() { + // Called on an arbitrary thread during initialization. We do not want to wait for + // completion as it would delay boot. + final boolean waitForCompletion = false; + startInternal(waitForCompletion); + } + + /** + * Starts the service during server initialization or during tests after a call to + * {@link #stop()}. + */ + void start() { + enforceManageTimeZoneDetectorPermission(); + + final boolean waitForCompletion = true; + startInternal(waitForCompletion); + } + + /** + * Starts the service during server initialization or during tests after a call to + * {@link #stop()}. + * + *

To avoid tests needing to sleep, when {@code waitForCompletion} is {@code true}, this + * method will not return until all the system server components have started. + */ + private void startInternal(boolean waitForCompletion) { + Runnable runnable = () -> { + synchronized (mSharedLock) { + if (mLocationTimeZoneDetectorController == null) { + LocationTimeZoneProvider primary = createPrimaryProvider(); + LocationTimeZoneProvider secondary = createSecondaryProvider(); + mLocationTimeZoneDetectorController = + new ControllerImpl(mThreadingDomain, primary, secondary); + ControllerCallbackImpl callback = new ControllerCallbackImpl( + mThreadingDomain); + mEnvironment = new ControllerEnvironmentImpl( + mThreadingDomain, mLocationTimeZoneDetectorController); + mLocationTimeZoneDetectorController.initialize(mEnvironment, callback); + } + } + }; + if (waitForCompletion) { + mThreadingDomain.postAndWait(runnable, BLOCKING_OP_WAIT_DURATION_MILLIS); + } else { + mThreadingDomain.post(runnable); + } + } + + private LocationTimeZoneProvider createPrimaryProvider() { + LocationTimeZoneProviderProxy proxy; + if (isProviderInSimulationMode(PRIMARY_PROVIDER_NAME)) { + proxy = new SimulatedLocationTimeZoneProviderProxy(mContext, mThreadingDomain); + } else if (isProviderDisabled(PRIMARY_PROVIDER_NAME)) { + proxy = new NullLocationTimeZoneProviderProxy(mContext, mThreadingDomain); + } else { + proxy = new RealLocationTimeZoneProviderProxy( + mContext, + mHandler, + mThreadingDomain, + PRIMARY_LOCATION_TIME_ZONE_SERVICE_ACTION, + R.bool.config_enablePrimaryLocationTimeZoneOverlay, + R.string.config_primaryLocationTimeZoneProviderPackageName + ); + } + return new BinderLocationTimeZoneProvider(mThreadingDomain, PRIMARY_PROVIDER_NAME, proxy); + } + + private LocationTimeZoneProvider createSecondaryProvider() { + LocationTimeZoneProviderProxy proxy; + if (isProviderInSimulationMode(SECONDARY_PROVIDER_NAME)) { + proxy = new SimulatedLocationTimeZoneProviderProxy(mContext, mThreadingDomain); + } else if (isProviderDisabled(SECONDARY_PROVIDER_NAME)) { + proxy = new NullLocationTimeZoneProviderProxy(mContext, mThreadingDomain); + } else { + proxy = new RealLocationTimeZoneProviderProxy( + mContext, + mHandler, + mThreadingDomain, + SECONDARY_LOCATION_TIME_ZONE_SERVICE_ACTION, + R.bool.config_enableSecondaryLocationTimeZoneOverlay, + R.string.config_secondaryLocationTimeZoneProviderPackageName + ); + } + return new BinderLocationTimeZoneProvider(mThreadingDomain, SECONDARY_PROVIDER_NAME, proxy); + } + + /** Used for bug triage and in tests to simulate provider events. */ + private boolean isProviderInSimulationMode(String providerName) { + return isProviderModeOverrideSet(providerName, PROVIDER_MODE_OVERRIDE_SIMULATED); + } + + /** Used for bug triage, tests and experiments to remove a provider. */ + private boolean isProviderDisabled(String providerName) { + return !isProviderEnabledInConfig(providerName) + || isProviderModeOverrideSet(providerName, PROVIDER_MODE_OVERRIDE_DISABLED); + } + + private boolean isProviderEnabledInConfig(String providerName) { + int providerEnabledConfigId; + switch (providerName) { + case PRIMARY_PROVIDER_NAME: { + providerEnabledConfigId = R.bool.config_enablePrimaryLocationTimeZoneProvider; + break; + } + case SECONDARY_PROVIDER_NAME: { + providerEnabledConfigId = R.bool.config_enableSecondaryLocationTimeZoneProvider; + break; + } + default: { + throw new IllegalArgumentException(providerName); + } + } + Resources resources = mContext.getResources(); + return resources.getBoolean(providerEnabledConfigId); + } + + private boolean isProviderModeOverrideSet(@NonNull String providerName, @NonNull String mode) { + switch (providerName) { + case PRIMARY_PROVIDER_NAME: { + return Objects.equals(mPrimaryProviderModeOverride, mode); + } + case SECONDARY_PROVIDER_NAME: { + return Objects.equals(mSecondaryProviderModeOverride, mode); + } + default: { + throw new IllegalArgumentException(providerName); + } + } + } + + /** + * Stops the service for tests. To avoid tests needing to sleep, this method will not return + * until all the system server components have stopped. + */ + void stop() { + enforceManageTimeZoneDetectorPermission(); + + mThreadingDomain.postAndWait(() -> { + synchronized (mSharedLock) { + if (mLocationTimeZoneDetectorController != null) { + mLocationTimeZoneDetectorController.destroy(); + mLocationTimeZoneDetectorController = null; + mEnvironment.destroy(); + mEnvironment = null; + } + } + }, BLOCKING_OP_WAIT_DURATION_MILLIS); + } + + @Override + public void onShellCommand(FileDescriptor in, FileDescriptor out, + FileDescriptor err, String[] args, ShellCallback callback, + ResultReceiver resultReceiver) { + (new LocationTimeZoneManagerShellCommand(this)).exec( + this, in, out, err, args, callback, resultReceiver); + } + + /** Sets this service into provider state recording mode for tests. */ + void setProviderModeOverride(@NonNull String providerName, @NonNull String mode) { + enforceManageTimeZoneDetectorPermission(); + + Preconditions.checkArgument( + PRIMARY_PROVIDER_NAME.equals(providerName) + || SECONDARY_PROVIDER_NAME.equals(providerName)); + Preconditions.checkArgument(PROVIDER_MODE_OVERRIDE_DISABLED.equals(mode) + || PROVIDER_MODE_OVERRIDE_SIMULATED.equals(mode) + || PROVIDER_MODE_OVERRIDE_NONE.equals(mode)); + + mThreadingDomain.postAndWait(() -> { + synchronized (mSharedLock) { + switch (providerName) { + case PRIMARY_PROVIDER_NAME: { + mPrimaryProviderModeOverride = mode; + break; + } + case SECONDARY_PROVIDER_NAME: { + mSecondaryProviderModeOverride = mode; + break; + } + } + } + }, BLOCKING_OP_WAIT_DURATION_MILLIS); + } + + /** Sets this service into provider state recording mode for tests. */ + void setProviderStateRecordingEnabled(boolean enabled) { + enforceManageTimeZoneDetectorPermission(); + + mThreadingDomain.postAndWait(() -> { + synchronized (mSharedLock) { + if (mLocationTimeZoneDetectorController != null) { + mLocationTimeZoneDetectorController.setProviderStateRecordingEnabled(enabled); + } + } + }, BLOCKING_OP_WAIT_DURATION_MILLIS); + } + + /** Returns a snapshot of the current controller state for tests. */ + @NonNull + LocationTimeZoneManagerServiceState getStateForTests() { + enforceManageTimeZoneDetectorPermission(); + + try { + return mThreadingDomain.postAndWait( + () -> { + synchronized (mSharedLock) { + if (mLocationTimeZoneDetectorController == null) { + return null; + } + return mLocationTimeZoneDetectorController.getStateForTests(); + } + }, + BLOCKING_OP_WAIT_DURATION_MILLIS); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Passes a {@link TestCommand} to the specified provider and waits for the response. + */ + @NonNull + Bundle handleProviderTestCommand( + @NonNull String providerName, @NonNull TestCommand testCommand) { + enforceManageTimeZoneDetectorPermission(); + + // Because this method blocks and posts work to the threading domain thread, it would cause + // a deadlock if it were called by the threading domain thread. + mThreadingDomain.assertNotCurrentThread(); + + AtomicReference resultReference = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + RemoteCallback remoteCallback = new RemoteCallback(x -> { + resultReference.set(x); + latch.countDown(); + }); + + mThreadingDomain.post(() -> { + synchronized (mSharedLock) { + if (mLocationTimeZoneDetectorController == null) { + remoteCallback.sendResult(null); + return; + } + mLocationTimeZoneDetectorController.handleProviderTestCommand( + providerName, testCommand, remoteCallback); + } + }); + + try { + // Wait, but not indefinitely. + if (!latch.await(BLOCKING_OP_WAIT_DURATION_MILLIS, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("Command did not complete in time"); + } + } catch (InterruptedException e) { + throw new AssertionError(e); + } + + return resultReference.get(); + } + + @Override + protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, + @Nullable String[] args) { + if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; + + IndentingPrintWriter ipw = new IndentingPrintWriter(pw); + // Called on an arbitrary thread at any time. + synchronized (mSharedLock) { + ipw.println("LocationTimeZoneManagerService:"); + ipw.increaseIndent(); + if (mLocationTimeZoneDetectorController == null) { + ipw.println("{Stopped}"); + } else { + mLocationTimeZoneDetectorController.dump(ipw, args); + } + ipw.decreaseIndent(); + } + } + + static void debugLog(String msg) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Slog.d(TAG, msg); + } + } + + static void warnLog(String msg) { + warnLog(msg, null); + } + + static void warnLog(String msg, @Nullable Throwable t) { + if (Log.isLoggable(TAG, Log.WARN)) { + Slog.w(TAG, msg, t); + } + } + + private void enforceManageTimeZoneDetectorPermission() { + mContext.enforceCallingPermission( + android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION, + "manage time and time zone detection"); + } +} diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerServiceState.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerServiceState.java new file mode 100644 index 000000000000..113926a265f5 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerServiceState.java @@ -0,0 +1,97 @@ +/* + * 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 android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion; +import com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** A snapshot of the location time zone manager service's state for tests. */ +final class LocationTimeZoneManagerServiceState { + + @Nullable private final GeolocationTimeZoneSuggestion mLastSuggestion; + @NonNull private final List mPrimaryProviderStates; + @NonNull private final List mSecondaryProviderStates; + + LocationTimeZoneManagerServiceState(@NonNull Builder builder) { + mLastSuggestion = builder.mLastSuggestion; + mPrimaryProviderStates = Objects.requireNonNull(builder.mPrimaryProviderStates); + mSecondaryProviderStates = Objects.requireNonNull(builder.mSecondaryProviderStates); + } + + @Nullable + public GeolocationTimeZoneSuggestion getLastSuggestion() { + return mLastSuggestion; + } + + @NonNull + public List getPrimaryProviderStates() { + return Collections.unmodifiableList(mPrimaryProviderStates); + } + + @NonNull + public List getSecondaryProviderStates() { + return Collections.unmodifiableList(mSecondaryProviderStates); + } + + @Override + public String toString() { + return "LocationTimeZoneManagerServiceState{" + + "mLastSuggestion=" + mLastSuggestion + + ", mPrimaryProviderStates=" + mPrimaryProviderStates + + ", mSecondaryProviderStates=" + mSecondaryProviderStates + + '}'; + } + + static final class Builder { + + private GeolocationTimeZoneSuggestion mLastSuggestion; + private List mPrimaryProviderStates; + private List mSecondaryProviderStates; + + @NonNull + Builder setLastSuggestion(@NonNull GeolocationTimeZoneSuggestion lastSuggestion) { + mLastSuggestion = Objects.requireNonNull(lastSuggestion); + return this; + } + + @NonNull + Builder setPrimaryProviderStateChanges(@NonNull List primaryProviderStates) { + mPrimaryProviderStates = new ArrayList<>(primaryProviderStates); + return this; + } + + @NonNull + Builder setSecondaryProviderStateChanges( + @NonNull List secondaryProviderStates) { + mSecondaryProviderStates = new ArrayList<>(secondaryProviderStates); + return this; + } + + @NonNull + LocationTimeZoneManagerServiceState build() { + return new LocationTimeZoneManagerServiceState(this); + } + } +} diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerShellCommand.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerShellCommand.java new file mode 100644 index 000000000000..b53150c729bc --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerShellCommand.java @@ -0,0 +1,330 @@ +/* + * 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.app.time.LocationTimeZoneManager.DUMP_STATE_OPTION_PROTO; +import static android.app.time.LocationTimeZoneManager.PRIMARY_PROVIDER_NAME; +import static android.app.time.LocationTimeZoneManager.PROVIDER_MODE_OVERRIDE_DISABLED; +import static android.app.time.LocationTimeZoneManager.PROVIDER_MODE_OVERRIDE_NONE; +import static android.app.time.LocationTimeZoneManager.PROVIDER_MODE_OVERRIDE_SIMULATED; +import static android.app.time.LocationTimeZoneManager.SECONDARY_PROVIDER_NAME; +import static android.app.time.LocationTimeZoneManager.SHELL_COMMAND_DUMP_STATE; +import static android.app.time.LocationTimeZoneManager.SHELL_COMMAND_RECORD_PROVIDER_STATES; +import static android.app.time.LocationTimeZoneManager.SHELL_COMMAND_SEND_PROVIDER_TEST_COMMAND; +import static android.app.time.LocationTimeZoneManager.SHELL_COMMAND_SET_PROVIDER_MODE_OVERRIDE; +import static android.app.time.LocationTimeZoneManager.SHELL_COMMAND_START; +import static android.app.time.LocationTimeZoneManager.SHELL_COMMAND_STOP; + +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 static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_UNKNOWN; + +import android.annotation.NonNull; +import android.app.time.GeolocationTimeZoneSuggestionProto; +import android.app.time.LocationTimeZoneManagerProto; +import android.app.time.LocationTimeZoneManagerServiceStateProto; +import android.app.time.TimeZoneProviderStateProto; +import android.os.Bundle; +import android.os.ShellCommand; +import android.util.IndentingPrintWriter; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.dump.DualDumpOutputStream; +import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion; +import com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.ProviderStateEnum; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** Implements the shell command interface for {@link LocationTimeZoneManagerService}. */ +class LocationTimeZoneManagerShellCommand extends ShellCommand { + + private static final List VALID_PROVIDER_NAMES = + Arrays.asList(PRIMARY_PROVIDER_NAME, SECONDARY_PROVIDER_NAME); + + private final LocationTimeZoneManagerService mService; + + LocationTimeZoneManagerShellCommand(LocationTimeZoneManagerService service) { + mService = service; + } + + @Override + public int onCommand(String cmd) { + if (cmd == null) { + return handleDefaultCommands(cmd); + } + + switch (cmd) { + case SHELL_COMMAND_START: { + return runStart(); + } + case SHELL_COMMAND_STOP: { + return runStop(); + } + case SHELL_COMMAND_SET_PROVIDER_MODE_OVERRIDE: { + return runSetProviderModeOverride(); + } + case SHELL_COMMAND_SEND_PROVIDER_TEST_COMMAND: { + return runSendProviderTestCommand(); + } + case SHELL_COMMAND_RECORD_PROVIDER_STATES: { + return runRecordProviderStates(); + } + case SHELL_COMMAND_DUMP_STATE: { + return runDumpControllerState(); + } + default: { + return handleDefaultCommands(cmd); + } + } + } + + @Override + public void onHelp() { + final PrintWriter pw = getOutPrintWriter(); + pw.println("Location Time Zone Manager (location_time_zone_manager) commands for tests:"); + pw.println(" help"); + pw.println(" Print this help text."); + pw.printf(" %s\n", SHELL_COMMAND_START); + pw.println(" Starts the location_time_zone_manager, creating time zone providers."); + pw.printf(" %s\n", SHELL_COMMAND_STOP); + pw.println(" Stops the location_time_zone_manager, destroying time zone providers."); + pw.printf(" %s \n", SHELL_COMMAND_SET_PROVIDER_MODE_OVERRIDE); + pw.println(" Sets a provider into a test mode next time the service started."); + pw.printf(" Values: %s|%s|%s\n", PROVIDER_MODE_OVERRIDE_NONE, + PROVIDER_MODE_OVERRIDE_DISABLED, PROVIDER_MODE_OVERRIDE_SIMULATED); + pw.printf(" %s (true|false)\n", SHELL_COMMAND_RECORD_PROVIDER_STATES); + pw.printf(" Enables / disables provider state recording mode. See also %s. The default" + + " state is always \"false\".\n", SHELL_COMMAND_DUMP_STATE); + pw.println(" Note: When enabled, this mode consumes memory and it is only intended for" + + " testing."); + pw.println(" It should be disabled after use, or the device can be rebooted to" + + " reset the mode to disabled."); + pw.println(" Disabling (or enabling repeatedly) clears any existing stored states."); + pw.printf(" %s [%s]\n", SHELL_COMMAND_DUMP_STATE, DUMP_STATE_OPTION_PROTO); + pw.println(" Dumps Location Time Zone Manager state for tests as text or binary proto" + + " form."); + pw.println(" See the LocationTimeZoneManagerServiceStateProto definition for details."); + pw.printf(" %s \n", + SHELL_COMMAND_SEND_PROVIDER_TEST_COMMAND); + pw.println(" Passes a test command to the named provider."); + pw.println(); + pw.printf(" = One of %s\n", VALID_PROVIDER_NAMES); + pw.println(); + pw.printf("%s details:\n", SHELL_COMMAND_SEND_PROVIDER_TEST_COMMAND); + pw.println(); + pw.println("Provider encoding:"); + pw.println(); + TestCommand.printShellCommandEncodingHelp(pw); + pw.println(); + pw.println("Simulated provider mode can be used to test the system server behavior or to" + + " reproduce bugs without the complexity of using real providers."); + pw.println(); + pw.println("The test commands for simulated providers are:"); + SimulatedLocationTimeZoneProviderProxy.printTestCommandShellHelp(pw); + pw.println(); + pw.println("Test commands cannot currently be passed to real provider implementations."); + pw.println(); + } + + private int runStart() { + try { + mService.start(); + } catch (RuntimeException e) { + reportError(e); + return 1; + } + PrintWriter outPrintWriter = getOutPrintWriter(); + outPrintWriter.println("Service started"); + return 0; + } + + private int runStop() { + try { + mService.stop(); + } catch (RuntimeException e) { + reportError(e); + return 1; + } + PrintWriter outPrintWriter = getOutPrintWriter(); + outPrintWriter.println("Service stopped"); + return 0; + } + + private int runSetProviderModeOverride() { + PrintWriter outPrintWriter = getOutPrintWriter(); + try { + String providerName = getNextArgRequired(); + String modeOverride = getNextArgRequired(); + outPrintWriter.println("Setting provider mode override for " + providerName + + " to " + modeOverride); + mService.setProviderModeOverride(providerName, modeOverride); + } catch (RuntimeException e) { + reportError(e); + return 1; + } + return 0; + } + + private int runRecordProviderStates() { + PrintWriter outPrintWriter = getOutPrintWriter(); + boolean enabled; + try { + String nextArg = getNextArgRequired(); + enabled = Boolean.parseBoolean(nextArg); + } catch (RuntimeException e) { + reportError(e); + return 1; + } + + outPrintWriter.println("Setting provider state recording to " + enabled); + try { + mService.setProviderStateRecordingEnabled(enabled); + } catch (IllegalStateException e) { + reportError(e); + return 2; + } + return 0; + } + + private int runDumpControllerState() { + LocationTimeZoneManagerServiceState state; + try { + state = mService.getStateForTests(); + } catch (RuntimeException e) { + reportError(e); + return 1; + } + + DualDumpOutputStream outputStream; + boolean useProto = Objects.equals(DUMP_STATE_OPTION_PROTO, getNextOption()); + if (useProto) { + FileDescriptor outFd = getOutFileDescriptor(); + outputStream = new DualDumpOutputStream(new ProtoOutputStream(outFd)); + } else { + outputStream = new DualDumpOutputStream( + new IndentingPrintWriter(getOutPrintWriter(), " ")); + } + if (state.getLastSuggestion() != null) { + GeolocationTimeZoneSuggestion lastSuggestion = state.getLastSuggestion(); + long lastSuggestionToken = outputStream.start( + "last_suggestion", LocationTimeZoneManagerServiceStateProto.LAST_SUGGESTION); + for (String zoneId : lastSuggestion.getZoneIds()) { + outputStream.write( + "zone_ids" , GeolocationTimeZoneSuggestionProto.ZONE_IDS, zoneId); + } + for (String debugInfo : lastSuggestion.getDebugInfo()) { + outputStream.write( + "debug_info", GeolocationTimeZoneSuggestionProto.DEBUG_INFO, debugInfo); + } + outputStream.end(lastSuggestionToken); + } + + writeProviderStates(outputStream, state.getPrimaryProviderStates(), + "primary_provider_states", + LocationTimeZoneManagerServiceStateProto.PRIMARY_PROVIDER_STATES); + writeProviderStates(outputStream, state.getSecondaryProviderStates(), + "secondary_provider_states", + LocationTimeZoneManagerServiceStateProto.SECONDARY_PROVIDER_STATES); + outputStream.flush(); + + return 0; + } + + private static void writeProviderStates(DualDumpOutputStream outputStream, + List providerStates, String fieldName, + long fieldId) { + for (LocationTimeZoneProvider.ProviderState providerState : providerStates) { + long providerStateToken = outputStream.start(fieldName, fieldId); + outputStream.write("state", TimeZoneProviderStateProto.STATE, + convertProviderStateEnumToProtoEnum(providerState.stateEnum)); + outputStream.end(providerStateToken); + } + } + + private static int convertProviderStateEnumToProtoEnum(@ProviderStateEnum int stateEnum) { + switch (stateEnum) { + case PROVIDER_STATE_UNKNOWN: + return LocationTimeZoneManagerProto.TIME_ZONE_PROVIDER_STATE_UNKNOWN; + case PROVIDER_STATE_STARTED_INITIALIZING: + return LocationTimeZoneManagerProto.TIME_ZONE_PROVIDER_STATE_INITIALIZING; + case PROVIDER_STATE_STARTED_CERTAIN: + return LocationTimeZoneManagerProto.TIME_ZONE_PROVIDER_STATE_CERTAIN; + case PROVIDER_STATE_STARTED_UNCERTAIN: + return LocationTimeZoneManagerProto.TIME_ZONE_PROVIDER_STATE_UNCERTAIN; + case PROVIDER_STATE_STOPPED: + return LocationTimeZoneManagerProto.TIME_ZONE_PROVIDER_STATE_DISABLED; + case PROVIDER_STATE_PERM_FAILED: + return LocationTimeZoneManagerProto.TIME_ZONE_PROVIDER_STATE_PERM_FAILED; + case PROVIDER_STATE_DESTROYED: + return LocationTimeZoneManagerProto.TIME_ZONE_PROVIDER_STATE_DESTROYED; + default: { + throw new IllegalArgumentException("Unknown stateEnum=" + stateEnum); + } + } + } + + private int runSendProviderTestCommand() { + PrintWriter outPrintWriter = getOutPrintWriter(); + + String providerName; + TestCommand testCommand; + try { + providerName = validateProviderName(getNextArgRequired()); + testCommand = createTestCommandFromNextShellArg(); + } catch (RuntimeException e) { + reportError(e); + return 1; + } + + outPrintWriter.println("Injecting testCommand=" + testCommand + + " to providerName=" + providerName); + try { + Bundle result = mService.handleProviderTestCommand(providerName, testCommand); + outPrintWriter.println(result); + } catch (RuntimeException e) { + reportError(e); + return 2; + } + return 0; + } + + @NonNull + private TestCommand createTestCommandFromNextShellArg() { + return TestCommand.createFromShellCommandArgs(this); + } + + private void reportError(Throwable e) { + PrintWriter errPrintWriter = getErrPrintWriter(); + errPrintWriter.println("Error: "); + e.printStackTrace(errPrintWriter); + } + + @NonNull + static String validateProviderName(@NonNull String value) { + if (!VALID_PROVIDER_NAMES.contains(value)) { + throw new IllegalArgumentException("Unknown provider name=" + value); + } + return value; + } +} diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java new file mode 100644 index 000000000000..ef2f357b8c3e --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java @@ -0,0 +1,740 @@ +/* + * 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.TimeZoneProviderService.TEST_COMMAND_RESULT_ERROR_KEY; +import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_SUCCESS_KEY; + +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.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 static com.android.server.timezonedetector.location.TimeZoneProviderEvent.EVENT_TYPE_PERMANENT_FAILURE; +import static com.android.server.timezonedetector.location.TimeZoneProviderEvent.EVENT_TYPE_SUGGESTION; +import static com.android.server.timezonedetector.location.TimeZoneProviderEvent.EVENT_TYPE_UNCERTAIN; + +import android.annotation.ElapsedRealtimeLong; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteCallback; +import android.os.SystemClock; + +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.ReferenceWithHistory; +import com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.ProviderStateEnum; +import com.android.server.timezonedetector.location.ThreadingDomain.SingleRunnableQueue; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * A facade used by the {@link LocationTimeZoneProviderController} to interact with a location time + * zone provider. The provider implementation will typically have logic running in another process. + * + *

The provider is supplied with a {@link ProviderListener} via {@link + * #initialize(ProviderListener)}. This starts communication of asynchronous detection / error + * events back to the {@link LocationTimeZoneProviderController} via the {@link + * ProviderListener#onProviderStateChange} method. This call must be made on the + * {@link Handler} thread from the {@link ThreadingDomain} passed to the constructor. + * + *

This class is also responsible for monitoring the initialization timeout for a provider. i.e. + * if the provider fails to send its first suggestion within a certain time, this is the component + * responsible for generating the necessary "uncertain" event. + * + *

All incoming calls from the controller except for {@link + * LocationTimeZoneProvider#dump(android.util.IndentingPrintWriter, String[])} will be made on the + * {@link Handler} thread of the {@link ThreadingDomain} passed to the constructor. + */ +abstract class LocationTimeZoneProvider implements Dumpable { + + /** + * Listener interface used by the {@link LocationTimeZoneProviderController} to register an + * interest in provider events. + */ + interface ProviderListener { + /** + * Indicated that a provider changed states. The {@code providerState} indicates which one + */ + void onProviderStateChange(@NonNull ProviderState providerState); + } + + /** + * Information about the provider's current state. + */ + static class ProviderState { + + @IntDef(prefix = "PROVIDER_STATE_", + value = { PROVIDER_STATE_UNKNOWN, PROVIDER_STATE_STARTED_INITIALIZING, + PROVIDER_STATE_STARTED_CERTAIN, PROVIDER_STATE_STARTED_UNCERTAIN, + PROVIDER_STATE_STOPPED, PROVIDER_STATE_PERM_FAILED, PROVIDER_STATE_DESTROYED }) + @interface ProviderStateEnum {} + + /** + * Uninitialized value. Must not be used afte {@link LocationTimeZoneProvider#initialize}. + */ + static final int PROVIDER_STATE_UNKNOWN = 0; + + /** + * The provider is started and has not reported its first event. + */ + static final int PROVIDER_STATE_STARTED_INITIALIZING = 1; + + /** + * The provider is started and most recently reported a "suggestion" event. + */ + static final int PROVIDER_STATE_STARTED_CERTAIN = 2; + + /** + * The provider is started and most recently reported an "uncertain" event. + */ + static final int PROVIDER_STATE_STARTED_UNCERTAIN = 3; + + /** + * The provider is stopped. + * + * This is the state after {@link #initialize} is called. + */ + static final int PROVIDER_STATE_STOPPED = 4; + + /** + * The provider has failed and cannot be restarted. This is a terminated state triggered by + * the provider itself. + * + * Providers may enter this state any time after a provider is started. + */ + static final int PROVIDER_STATE_PERM_FAILED = 5; + + /** + * The provider has been destroyed by the controller and cannot be restarted. Similar to + * {@link #PROVIDER_STATE_PERM_FAILED} except that a provider is set into this state. + */ + static final int PROVIDER_STATE_DESTROYED = 6; + + /** The {@link LocationTimeZoneProvider} the state is for. */ + public final @NonNull LocationTimeZoneProvider provider; + + /** The state enum value of the current state. */ + public final @ProviderStateEnum int stateEnum; + + /** + * The last {@link TimeZoneProviderEvent} received. Only populated when {@link #stateEnum} + * is either {@link #PROVIDER_STATE_STARTED_CERTAIN} or {@link + * #PROVIDER_STATE_STARTED_UNCERTAIN}, but it can be {@code null} then too if no event has + * yet been received. + */ + @Nullable public final TimeZoneProviderEvent event; + + /** + * The user configuration associated with the current state. Only and always present when + * {@link #stateEnum} is one of the started states. + */ + @Nullable public final ConfigurationInternal currentUserConfiguration; + + /** + * The time according to the elapsed realtime clock when the provider entered the current + * state. Included for debugging, not used for equality. + */ + @ElapsedRealtimeLong + private final long mStateEntryTimeMillis; + + /** + * Debug information providing context for the transition to this state. Included for + * debugging, not used for equality. + */ + @Nullable private final String mDebugInfo; + + + private ProviderState(@NonNull LocationTimeZoneProvider provider, + @ProviderStateEnum int stateEnum, + @Nullable TimeZoneProviderEvent event, + @Nullable ConfigurationInternal currentUserConfiguration, + @Nullable String debugInfo) { + this.provider = Objects.requireNonNull(provider); + this.stateEnum = stateEnum; + this.event = event; + this.currentUserConfiguration = currentUserConfiguration; + this.mStateEntryTimeMillis = SystemClock.elapsedRealtime(); + this.mDebugInfo = debugInfo; + } + + /** Creates the bootstrap state, uses {@link #PROVIDER_STATE_UNKNOWN}. */ + static ProviderState createStartingState( + @NonNull LocationTimeZoneProvider provider) { + return new ProviderState( + provider, PROVIDER_STATE_UNKNOWN, null, null, "Initial state"); + } + + /** + * Create a new state from this state. Validates that the state transition is valid + * and that the required parameters for the new state are present / absent. + */ + ProviderState newState(@ProviderStateEnum int newStateEnum, + @Nullable TimeZoneProviderEvent event, + @Nullable ConfigurationInternal currentUserConfig, + @Nullable String debugInfo) { + + // Check valid "from" transitions. + switch (this.stateEnum) { + case PROVIDER_STATE_UNKNOWN: { + if (newStateEnum != PROVIDER_STATE_STOPPED) { + throw new IllegalArgumentException( + "Must transition from " + prettyPrintStateEnum( + PROVIDER_STATE_UNKNOWN) + + " to " + prettyPrintStateEnum(PROVIDER_STATE_STOPPED)); + } + break; + } + case PROVIDER_STATE_STOPPED: + case PROVIDER_STATE_STARTED_INITIALIZING: + case PROVIDER_STATE_STARTED_CERTAIN: + case PROVIDER_STATE_STARTED_UNCERTAIN: { + // These can go to each other or either of PROVIDER_STATE_PERM_FAILED and + // PROVIDER_STATE_DESTROYED. + break; + } + case PROVIDER_STATE_PERM_FAILED: + case PROVIDER_STATE_DESTROYED: { + throw new IllegalArgumentException("Illegal transition out of " + + prettyPrintStateEnum(this.stateEnum)); + } + default: { + throw new IllegalArgumentException("Invalid this.stateEnum=" + this.stateEnum); + } + } + + // Validate "to" transitions / arguments. + switch (newStateEnum) { + case PROVIDER_STATE_UNKNOWN: { + throw new IllegalArgumentException("Cannot transition to " + + prettyPrintStateEnum(PROVIDER_STATE_UNKNOWN)); + } + case PROVIDER_STATE_STOPPED: { + if (event != null || currentUserConfig != null) { + throw new IllegalArgumentException( + "Stopped state: event and currentUserConfig must be null" + + ", event=" + event + + ", currentUserConfig=" + currentUserConfig); + } + break; + } + case PROVIDER_STATE_STARTED_INITIALIZING: + case PROVIDER_STATE_STARTED_CERTAIN: + case PROVIDER_STATE_STARTED_UNCERTAIN: { + if (currentUserConfig == null) { + throw new IllegalArgumentException( + "Started state: currentUserConfig must not be null"); + } + break; + } + case PROVIDER_STATE_PERM_FAILED: + case PROVIDER_STATE_DESTROYED: { + if (event != null || currentUserConfig != null) { + throw new IllegalArgumentException( + "Terminal state: event and currentUserConfig must be null" + + ", newStateEnum=" + newStateEnum + + ", event=" + event + + ", currentUserConfig=" + currentUserConfig); + } + break; + } + default: { + throw new IllegalArgumentException("Unknown newStateEnum=" + newStateEnum); + } + } + return new ProviderState(provider, newStateEnum, event, currentUserConfig, debugInfo); + } + + /** Returns {@code true} if {@link #stateEnum} is one of the started states. */ + boolean isStarted() { + return stateEnum == PROVIDER_STATE_STARTED_INITIALIZING + || stateEnum == PROVIDER_STATE_STARTED_CERTAIN + || stateEnum == PROVIDER_STATE_STARTED_UNCERTAIN; + } + + /** Returns {@code true} if {@link #stateEnum} is one of the terminated states. */ + boolean isTerminated() { + return stateEnum == PROVIDER_STATE_PERM_FAILED + || stateEnum == PROVIDER_STATE_DESTROYED; + } + + @Override + public String toString() { + // this.provider is omitted deliberately to avoid recursion, since the provider holds + // a reference to its state. + return "ProviderState{" + + "stateEnum=" + prettyPrintStateEnum(stateEnum) + + ", event=" + event + + ", currentUserConfiguration=" + currentUserConfiguration + + ", mStateEntryTimeMillis=" + mStateEntryTimeMillis + + ", mDebugInfo=" + mDebugInfo + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ProviderState state = (ProviderState) o; + return stateEnum == state.stateEnum + && Objects.equals(event, state.event) + && Objects.equals(currentUserConfiguration, state.currentUserConfiguration); + } + + @Override + public int hashCode() { + return Objects.hash(stateEnum, event, currentUserConfiguration); + } + + private static String prettyPrintStateEnum(@ProviderStateEnum int state) { + switch (state) { + case PROVIDER_STATE_STOPPED: + return "Stopped (" + PROVIDER_STATE_STOPPED + ")"; + case PROVIDER_STATE_STARTED_INITIALIZING: + return "Started initializing (" + PROVIDER_STATE_STARTED_INITIALIZING + ")"; + case PROVIDER_STATE_STARTED_CERTAIN: + return "Started certain (" + PROVIDER_STATE_STARTED_CERTAIN + ")"; + case PROVIDER_STATE_STARTED_UNCERTAIN: + return "Started uncertain (" + PROVIDER_STATE_STARTED_UNCERTAIN + ")"; + case PROVIDER_STATE_PERM_FAILED: + return "Perm failure (" + PROVIDER_STATE_PERM_FAILED + ")"; + case PROVIDER_STATE_DESTROYED: + return "Destroyed (" + PROVIDER_STATE_DESTROYED + ")"; + case PROVIDER_STATE_UNKNOWN: + default: + return "Unknown (" + state + ")"; + } + } + } + + @NonNull final ThreadingDomain mThreadingDomain; + @NonNull final Object mSharedLock; + @NonNull final String mProviderName; + + /** + * Usually {@code false} but can be set to {@code true} for testing. + */ + @GuardedBy("mSharedLock") + private boolean mStateChangeRecording; + + @GuardedBy("mSharedLock") + @NonNull + private final ArrayList mRecordedStates = new ArrayList<>(0); + + /** + * The current state (with history for debugging). + */ + @GuardedBy("mSharedLock") + final ReferenceWithHistory mCurrentState = new ReferenceWithHistory<>(10); + + /** + * Used for scheduling initialization timeouts, i.e. for providers that have just been started. + */ + @NonNull private final SingleRunnableQueue mInitializationTimeoutQueue; + + // Non-null and effectively final after initialize() is called. + ProviderListener mProviderListener; + + /** Creates the instance. */ + LocationTimeZoneProvider(@NonNull ThreadingDomain threadingDomain, + @NonNull String providerName) { + mThreadingDomain = Objects.requireNonNull(threadingDomain); + mInitializationTimeoutQueue = threadingDomain.createSingleRunnableQueue(); + mSharedLock = threadingDomain.getLockObject(); + mProviderName = Objects.requireNonNull(providerName); + } + + /** + * Initializes the provider. Called before the provider is first used. + */ + final void initialize(@NonNull ProviderListener providerListener) { + mThreadingDomain.assertCurrentThread(); + + synchronized (mSharedLock) { + if (mProviderListener != null) { + throw new IllegalStateException("initialize already called"); + } + mProviderListener = Objects.requireNonNull(providerListener); + ProviderState currentState = ProviderState.createStartingState(this); + currentState = currentState.newState( + PROVIDER_STATE_STOPPED, null, null, + "initialize() called"); + setCurrentState(currentState, false); + + // Guard against uncaught exceptions due to initialization problems. + try { + onInitialize(); + } catch (RuntimeException e) { + warnLog("Unable to initialize the provider", e); + currentState = currentState + .newState(PROVIDER_STATE_PERM_FAILED, null, null, + "Provider failed to initialize"); + setCurrentState(currentState, true); + } + } + } + + /** + * Implemented by subclasses to do work during {@link #initialize}. + */ + @GuardedBy("mSharedLock") + abstract void onInitialize(); + + /** + * Destroys the provider. Called after the provider is stopped. This instance will not be called + * again by the {@link LocationTimeZoneProviderController}. + */ + final void destroy() { + mThreadingDomain.assertCurrentThread(); + + synchronized (mSharedLock) { + ProviderState currentState = mCurrentState.get(); + if (!currentState.isTerminated()) { + ProviderState destroyedState = currentState + .newState(PROVIDER_STATE_DESTROYED, null, null, "destroy() called"); + setCurrentState(destroyedState, false); + onDestroy(); + } + } + } + + /** + * Implemented by subclasses to do work during {@link #destroy()}. + */ + @GuardedBy("mSharedLock") + abstract void onDestroy(); + + /** + * Sets the provider into state recording mode for tests. + */ + final void setStateChangeRecordingEnabled(boolean enabled) { + mThreadingDomain.assertCurrentThread(); + synchronized (mSharedLock) { + mStateChangeRecording = enabled; + mRecordedStates.clear(); + mRecordedStates.trimToSize(); + } + } + + /** + * Returns recorded states. + */ + final List getRecordedStates() { + mThreadingDomain.assertCurrentThread(); + synchronized (mSharedLock) { + return new ArrayList<>(mRecordedStates); + } + } + + /** + * Set the current state, for use by this class and subclasses only. If {@code #notifyChanges} + * is {@code true} and {@code newState} is not equal to the old state, then {@link + * ProviderListener#onProviderStateChange(ProviderState)} must be called on + * {@link #mProviderListener}. + */ + final void setCurrentState(@NonNull ProviderState newState, boolean notifyChanges) { + mThreadingDomain.assertCurrentThread(); + synchronized (mSharedLock) { + ProviderState oldState = mCurrentState.get(); + mCurrentState.set(newState); + onSetCurrentState(newState); + if (!Objects.equals(newState, oldState)) { + if (mStateChangeRecording) { + mRecordedStates.add(newState); + } + if (notifyChanges) { + mProviderListener.onProviderStateChange(newState); + } + } + } + } + + /** + * Overridden by subclasses to do work during {@link #setCurrentState}. + */ + @GuardedBy("mSharedLock") + void onSetCurrentState(ProviderState newState) { + // Default no-op. + } + + /** + * Returns the current state of the provider. This method must be called using the handler + * thread from the {@link ThreadingDomain}. + */ + @NonNull + final ProviderState getCurrentState() { + mThreadingDomain.assertCurrentThread(); + synchronized (mSharedLock) { + return mCurrentState.get(); + } + } + + /** + * Returns the name of the provider. This method must be called using the handler thread from + * the {@link ThreadingDomain}. + */ + final String getName() { + mThreadingDomain.assertCurrentThread(); + return mProviderName; + } + + /** + * Starts the provider. It is an error to call this method except when the {@link + * #getCurrentState()} is at {@link ProviderState#PROVIDER_STATE_STOPPED}. This method must be + * called using the handler thread from the {@link ThreadingDomain}. + */ + final void startUpdates(@NonNull ConfigurationInternal currentUserConfiguration, + @NonNull Duration initializationTimeout, @NonNull Duration initializationTimeoutFuzz) { + mThreadingDomain.assertCurrentThread(); + + synchronized (mSharedLock) { + assertCurrentState(PROVIDER_STATE_STOPPED); + + ProviderState currentState = mCurrentState.get(); + ProviderState newState = currentState.newState( + PROVIDER_STATE_STARTED_INITIALIZING, null /* event */, + currentUserConfiguration, "startUpdates() called"); + setCurrentState(newState, false); + + Duration delay = initializationTimeout.plus(initializationTimeoutFuzz); + mInitializationTimeoutQueue.runDelayed( + this::handleInitializationTimeout, delay.toMillis()); + + onStartUpdates(initializationTimeout); + } + } + + private void handleInitializationTimeout() { + mThreadingDomain.assertCurrentThread(); + + synchronized (mSharedLock) { + ProviderState currentState = mCurrentState.get(); + if (currentState.stateEnum == PROVIDER_STATE_STARTED_INITIALIZING) { + // On initialization timeout the provider becomes uncertain. + ProviderState newState = currentState.newState( + PROVIDER_STATE_STARTED_UNCERTAIN, null /* event */, + currentState.currentUserConfiguration, "initialization timeout"); + setCurrentState(newState, true); + } else { + warnLog("handleInitializationTimeout: Initialization timeout triggered when in" + + " an unexpected state=" + currentState); + } + } + } + + /** + * Implemented by subclasses to do work during {@link #startUpdates}. This is where the logic + * to start the real provider should be implemented. + * + * @param initializationTimeout the initialization timeout to pass to the real provider + */ + abstract void onStartUpdates(@NonNull Duration initializationTimeout); + + /** + * Stops the provider. It is an error to call this method except when the {@link + * #getCurrentState()} is one of the started states. This method must be + * called using the handler thread from the {@link ThreadingDomain}. + */ + final void stopUpdates() { + mThreadingDomain.assertCurrentThread(); + + synchronized (mSharedLock) { + assertIsStarted(); + + ProviderState currentState = mCurrentState.get(); + ProviderState newState = currentState.newState( + PROVIDER_STATE_STOPPED, null, null, "stopUpdates() called"); + setCurrentState(newState, false); + + if (mInitializationTimeoutQueue.hasQueued()) { + mInitializationTimeoutQueue.cancel(); + } + + onStopUpdates(); + } + } + + /** + * Implemented by subclasses to do work during {@link #stopUpdates}. + */ + abstract void onStopUpdates(); + + /** + * Overridden by subclasses to handle the supplied {@link TestCommand}. If {@code callback} is + * non-null, the default implementation sends a result {@link Bundle} with {@link + * android.service.timezone.TimeZoneProviderService#TEST_COMMAND_RESULT_SUCCESS_KEY} set to + * {@code false} and a "Not implemented" error message. + */ + void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) { + Objects.requireNonNull(testCommand); + + if (callback != null) { + Bundle result = new Bundle(); + result.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, false); + result.putString(TEST_COMMAND_RESULT_ERROR_KEY, "Not implemented"); + callback.sendResult(result); + } + } + + /** For subclasses to invoke when a {@link TimeZoneProviderEvent} has been received. */ + final void handleTimeZoneProviderEvent(@NonNull TimeZoneProviderEvent timeZoneProviderEvent) { + mThreadingDomain.assertCurrentThread(); + Objects.requireNonNull(timeZoneProviderEvent); + + synchronized (mSharedLock) { + debugLog("handleTimeZoneProviderEvent: mProviderName=" + mProviderName + + ", timeZoneProviderEvent=" + timeZoneProviderEvent); + + ProviderState currentState = mCurrentState.get(); + int eventType = timeZoneProviderEvent.getType(); + switch (currentState.stateEnum) { + case PROVIDER_STATE_DESTROYED: + case PROVIDER_STATE_PERM_FAILED: { + // After entering a terminated state, there is nothing to do. The remote peer is + // supposed to stop sending events after it has reported perm failure. + warnLog("handleTimeZoneProviderEvent: Event=" + timeZoneProviderEvent + + " received for provider=" + this + " when in terminated state"); + return; + } + case PROVIDER_STATE_STOPPED: { + switch (eventType) { + case EVENT_TYPE_PERMANENT_FAILURE: { + String msg = "handleTimeZoneProviderEvent:" + + " Failure event=" + timeZoneProviderEvent + + " received for stopped provider=" + this + + ", entering permanently failed state"; + warnLog(msg); + ProviderState newState = currentState.newState( + PROVIDER_STATE_PERM_FAILED, null, null, msg); + setCurrentState(newState, true); + if (mInitializationTimeoutQueue.hasQueued()) { + mInitializationTimeoutQueue.cancel(); + } + return; + } + case EVENT_TYPE_SUGGESTION: + case EVENT_TYPE_UNCERTAIN: { + // Any geolocation-related events received for a stopped provider are + // ignored: they should not happen. + warnLog("handleTimeZoneProviderEvent:" + + " event=" + timeZoneProviderEvent + + " received for stopped provider=" + this + + ", ignoring"); + + return; + } + default: { + throw new IllegalStateException( + "Unknown eventType=" + timeZoneProviderEvent); + } + } + } + case PROVIDER_STATE_STARTED_INITIALIZING: + case PROVIDER_STATE_STARTED_CERTAIN: + case PROVIDER_STATE_STARTED_UNCERTAIN: { + switch (eventType) { + case EVENT_TYPE_PERMANENT_FAILURE: { + String msg = "handleTimeZoneProviderEvent:" + + " Failure event=" + timeZoneProviderEvent + + " received for provider=" + this + + ", entering permanently failed state"; + warnLog(msg); + 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_SUGGESTION: { + @ProviderStateEnum int providerStateEnum; + if (eventType == EVENT_TYPE_UNCERTAIN) { + providerStateEnum = PROVIDER_STATE_STARTED_UNCERTAIN; + } else { + providerStateEnum = PROVIDER_STATE_STARTED_CERTAIN; + } + ProviderState newState = currentState.newState(providerStateEnum, + timeZoneProviderEvent, currentState.currentUserConfiguration, + "handleTimeZoneProviderEvent() when started"); + setCurrentState(newState, true); + if (mInitializationTimeoutQueue.hasQueued()) { + mInitializationTimeoutQueue.cancel(); + } + return; + } + default: { + throw new IllegalStateException( + "Unknown eventType=" + timeZoneProviderEvent); + } + } + } + default: { + throw new IllegalStateException("Unknown providerType=" + currentState); + } + } + } + } + + @GuardedBy("mSharedLock") + private void assertIsStarted() { + ProviderState currentState = mCurrentState.get(); + if (!currentState.isStarted()) { + throw new IllegalStateException("Required a started state, but was " + currentState); + } + } + + @GuardedBy("mSharedLock") + private void assertCurrentState(@ProviderStateEnum int requiredState) { + ProviderState currentState = mCurrentState.get(); + if (currentState.stateEnum != requiredState) { + throw new IllegalStateException( + "Required stateEnum=" + requiredState + ", but was " + currentState); + } + } + + @VisibleForTesting + boolean isInitializationTimeoutSet() { + synchronized (mSharedLock) { + return mInitializationTimeoutQueue.hasQueued(); + } + } + + @VisibleForTesting + Duration getInitializationTimeoutDelay() { + synchronized (mSharedLock) { + return Duration.ofMillis(mInitializationTimeoutQueue.getQueuedDelayMillis()); + } + } +} diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java new file mode 100644 index 000000000000..b4aff3e005ad --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java @@ -0,0 +1,158 @@ +/* + * 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 android.annotation.DurationMillisLong; +import android.annotation.NonNull; +import android.os.Handler; + +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 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 controller interacts with the following components: + *

+ * + *

All incoming calls except for {@link + * LocationTimeZoneProviderController#dump(android.util.IndentingPrintWriter, String[])} must be + * made on the {@link Handler} thread of the {@link ThreadingDomain} passed to {@link + * #LocationTimeZoneProviderController(ThreadingDomain)}. + * + *

Provider / controller integration notes: + * + *

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), where as certainty means it should not. + * + *

A provider can fail permanently. A permanent failure will stop the provider until next + * boot. + */ +abstract class LocationTimeZoneProviderController implements Dumpable { + + @NonNull protected final ThreadingDomain mThreadingDomain; + @NonNull protected final Object mSharedLock; + + LocationTimeZoneProviderController(@NonNull ThreadingDomain threadingDomain) { + mThreadingDomain = Objects.requireNonNull(threadingDomain); + mSharedLock = threadingDomain.getLockObject(); + } + + /** + * Called to initialize the controller during boot. Called once only. + * {@link LocationTimeZoneProvider#initialize} must be called by this method. + */ + abstract void initialize(@NonNull Environment environment, @NonNull Callback callback); + + /** + * Called when any settings or other device state that affect location-based time zone detection + * 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. + */ + abstract void onConfigChanged(); + + @VisibleForTesting + abstract boolean isUncertaintyTimeoutSet(); + + @VisibleForTesting + @DurationMillisLong + abstract long getUncertaintyTimeoutDelayMillis(); + + /** Called if the geolocation time zone detection is being reconfigured. */ + abstract void destroy(); + + /** + * 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 delay allowed after receiving uncertainty from a provider before it should be + * passed on. + */ + abstract Duration getUncertaintyDelay(); + } + + /** + * 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; + @NonNull protected final Object mSharedLock; + + Callback(@NonNull ThreadingDomain threadingDomain) { + mThreadingDomain = Objects.requireNonNull(threadingDomain); + mSharedLock = threadingDomain.getLockObject(); + } + + /** + * Suggests the latest time zone state for the device. + */ + abstract void suggest(@NonNull GeolocationTimeZoneSuggestion suggestion); + } +} diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderProxy.java new file mode 100644 index 000000000000..43b1b5f017b2 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderProxy.java @@ -0,0 +1,150 @@ +/* + * 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 android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.os.Handler; +import android.os.RemoteCallback; +import android.util.IndentingPrintWriter; + +import com.android.internal.annotations.GuardedBy; +import com.android.server.timezonedetector.Dumpable; + +import java.util.Objects; + +/** + * System server-side proxy for ITimeZoneProvider implementations, i.e. this provides the system + * server object used to communicate with a remote TimeZoneProvider over Binder, which could be + * running in a different process. As TimeZoneProviders are bound / unbound this proxy will rebind + * to the "best" available remote process. + * + *

Threading guarantees provided / required by this interface: + *

+ * + *

This class exists to enable the introduction of test implementations of {@link + * LocationTimeZoneProviderProxy} that can be used when a device is in a test mode to inject test + * events / behavior that are otherwise difficult to simulate. + */ +abstract class LocationTimeZoneProviderProxy implements Dumpable { + + @NonNull protected final Context mContext; + @NonNull protected final ThreadingDomain mThreadingDomain; + @NonNull protected final Object mSharedLock; + + // Non-null and effectively final after setListener() is called. + @GuardedBy("mSharedLock") + @Nullable + protected Listener mListener; + + LocationTimeZoneProviderProxy( + @NonNull Context context, @NonNull ThreadingDomain threadingDomain) { + mContext = Objects.requireNonNull(context); + mThreadingDomain = Objects.requireNonNull(threadingDomain); + mSharedLock = threadingDomain.getLockObject(); + } + + /** + * Initializes the proxy. The supplied listener can expect to receive all events after this + * point. This method calls {@link #onInitialize()} for subclasses to handle their own + * initialization. + */ + void initialize(@NonNull Listener listener) { + Objects.requireNonNull(listener); + synchronized (mSharedLock) { + if (mListener != null) { + throw new IllegalStateException("listener already set"); + } + this.mListener = listener; + onInitialize(); + } + } + + /** + * Implemented by subclasses to initializes the proxy. This is called after {@link #mListener} + * is set. + */ + @GuardedBy("mSharedLock") + abstract void onInitialize(); + + /** + * Destroys the proxy. This method calls {@link #onDestroy()} for subclasses to handle their own + * destruction. + */ + void destroy() { + synchronized (mSharedLock) { + onDestroy(); + } + } + + /** + * Implemented by subclasses to destroy the proxy. + */ + @GuardedBy("mSharedLock") + abstract void onDestroy(); + + /** + * Sets a new request for the provider. + */ + abstract void setRequest(@NonNull TimeZoneProviderRequest request); + + /** + * Processes the supplied test command. An optional callback can be supplied to listen for a + * response. + */ + abstract void handleTestCommand(@NonNull TestCommand testCommand, + @Nullable RemoteCallback callback); + + /** + * Handles a {@link TimeZoneProviderEvent} from a remote process. + */ + final void handleTimeZoneProviderEvent(@NonNull TimeZoneProviderEvent timeZoneProviderEvent) { + // These calls are invoked on a binder thread. Move to the mThreadingDomain thread as + // required by the guarantees for this class. + mThreadingDomain.post(() -> mListener.onReportTimeZoneProviderEvent(timeZoneProviderEvent)); + } + + /** + * Interface for listening to location time zone providers. See {@link + * LocationTimeZoneProviderProxy} for threading guarantees. + */ + interface Listener { + + /** + * Called when a provider receives a {@link TimeZoneProviderEvent}. + */ + void onReportTimeZoneProviderEvent(@NonNull TimeZoneProviderEvent timeZoneProviderEvent); + + /** + * Called when a provider is (re)bound. + */ + void onProviderBound(); + + /** Called when a provider is unbound. */ + void onProviderUnbound(); + } +} diff --git a/services/core/java/com/android/server/timezonedetector/location/NullLocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/timezonedetector/location/NullLocationTimeZoneProviderProxy.java new file mode 100644 index 000000000000..1f45e828aad4 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/location/NullLocationTimeZoneProviderProxy.java @@ -0,0 +1,86 @@ +/* + * 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 android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.os.Bundle; +import android.os.RemoteCallback; +import android.service.timezone.TimeZoneProviderService; +import android.util.IndentingPrintWriter; + +/** + * A {@link LocationTimeZoneProviderProxy} that provides minimal responses needed for the {@link + * BinderLocationTimeZoneProvider} to operate correctly when there is no "real" provider + * configured / enabled. This can be used during development / testing, or in a production build + * when the platform supports more providers than are needed for an Android deployment. + * + *

For example, if the {@link LocationTimeZoneProviderController} supports a primary + * and a secondary {@link LocationTimeZoneProvider}, but only a primary is configured, the secondary + * config will be left null and the {@link LocationTimeZoneProviderProxy} implementation will be + * defaulted to a {@link NullLocationTimeZoneProviderProxy}. The {@link + * NullLocationTimeZoneProviderProxy} sends a "permanent failure" event immediately after being + * started for the first time, which ensures the {@link LocationTimeZoneProviderController} won't + * expect any further {@link TimeZoneProviderEvent}s to come from it, and won't attempt to use it + * again. + */ +class NullLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy { + + /** Creates the instance. */ + NullLocationTimeZoneProviderProxy( + @NonNull Context context, @NonNull ThreadingDomain threadingDomain) { + super(context, threadingDomain); + } + + @Override + void onInitialize() { + // No-op + } + + @Override + void onDestroy() { + // No-op + } + + @Override + void setRequest(@NonNull TimeZoneProviderRequest request) { + if (request.sendUpdates()) { + TimeZoneProviderEvent event = TimeZoneProviderEvent.createPermanentFailureEvent( + "Provider is disabled"); + handleTimeZoneProviderEvent(event); + } + } + + @Override + void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) { + if (callback != null) { + Bundle result = new Bundle(); + result.putBoolean(TimeZoneProviderService.TEST_COMMAND_RESULT_SUCCESS_KEY, false); + result.putString(TimeZoneProviderService.TEST_COMMAND_RESULT_ERROR_KEY, + "Provider is disabled"); + callback.sendResult(result); + } + } + + @Override + public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) { + synchronized (mSharedLock) { + ipw.println("{NullLocationTimeZoneProviderProxy}"); + } + } +} diff --git a/services/core/java/com/android/server/timezonedetector/location/RealLocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/timezonedetector/location/RealLocationTimeZoneProviderProxy.java new file mode 100644 index 000000000000..38211efc1c63 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/location/RealLocationTimeZoneProviderProxy.java @@ -0,0 +1,244 @@ +/* + * 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.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_ERROR_KEY; +import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_SUCCESS_KEY; + +import static com.android.server.timezonedetector.location.LocationTimeZoneManagerService.warnLog; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteCallback; +import android.service.timezone.ITimeZoneProvider; +import android.service.timezone.ITimeZoneProviderManager; +import android.service.timezone.TimeZoneProviderSuggestion; +import android.util.IndentingPrintWriter; + +import com.android.internal.annotations.GuardedBy; +import com.android.server.ServiceWatcher; + +import java.util.Objects; +import java.util.function.Predicate; + +/** + * System server-side proxy for ITimeZoneProvider implementations, i.e. this provides the + * system server object used to communicate with a remote {@link + * android.service.timezone.TimeZoneProviderService} over Binder, which could be running in a + * different process. As "remote" providers are bound / unbound this proxy will rebind to the "best" + * available remote process. + */ +class RealLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy { + + @NonNull private final ServiceWatcher mServiceWatcher; + + @GuardedBy("mSharedLock") + @Nullable private ManagerProxy mManagerProxy; + + @GuardedBy("mSharedLock") + @NonNull private TimeZoneProviderRequest mRequest; + + RealLocationTimeZoneProviderProxy( + @NonNull Context context, @NonNull Handler handler, + @NonNull ThreadingDomain threadingDomain, @NonNull String action, + int enableOverlayResId, int nonOverlayPackageResId) { + super(context, threadingDomain); + mManagerProxy = null; + mRequest = TimeZoneProviderRequest.createStopUpdatesRequest(); + + // A predicate that is used to confirm that an intent service can be used as a + // location-based TimeZoneProvider. The service must: + // 1) Declare android:permission="android.permission.BIND_TIME_ZONE_PROVIDER_SERVICE" - this + // ensures that the provider will only communicate with the system server. + // 2) Be in an application that has been granted the + // android.permission.INSTALL_LOCATION_TIME_ZONE_PROVIDER_SERVICE permission. This + // ensures only trusted time zone providers will be discovered. + final String requiredClientPermission = Manifest.permission.BIND_TIME_ZONE_PROVIDER_SERVICE; + final String requiredPermission = + Manifest.permission.INSTALL_LOCATION_TIME_ZONE_PROVIDER_SERVICE; + Predicate intentServiceCheckPredicate = resolveInfo -> { + ServiceInfo serviceInfo = resolveInfo.serviceInfo; + + boolean hasClientPermissionRequirement = + requiredClientPermission.equals(serviceInfo.permission); + + String packageName = serviceInfo.packageName; + PackageManager packageManager = context.getPackageManager(); + int checkResult = packageManager.checkPermission(requiredPermission, packageName); + boolean hasRequiredPermission = checkResult == PERMISSION_GRANTED; + + boolean result = hasClientPermissionRequirement && hasRequiredPermission; + if (!result) { + warnLog("resolveInfo=" + resolveInfo + " does not meet requirements:" + + " hasClientPermissionRequirement=" + hasClientPermissionRequirement + + ", hasRequiredPermission=" + hasRequiredPermission); + } + return result; + }; + mServiceWatcher = new ServiceWatcher(context, handler, action, this::onBind, this::onUnbind, + enableOverlayResId, nonOverlayPackageResId, intentServiceCheckPredicate); + } + + @Override + void onInitialize() { + if (!register()) { + throw new IllegalStateException("Unable to register binder proxy"); + } + } + + @Override + void onDestroy() { + mServiceWatcher.unregister(); + } + + private boolean register() { + boolean resolves = mServiceWatcher.checkServiceResolves(); + if (resolves) { + mServiceWatcher.register(); + } + return resolves; + } + + private void onBind(IBinder binder, ComponentName componentName) { + mThreadingDomain.assertCurrentThread(); + + synchronized (mSharedLock) { + // When a new remote is first bound we create the ManagerProxy that will be passed to + // it. By creating a new one for each bind the ManagerProxy can check whether it is + // still the current proxy and if not it can ignore incoming calls. + mManagerProxy = new ManagerProxy(); + mListener.onProviderBound(); + + // Send the current request to the remote. + trySendCurrentRequest(); + } + } + + private void onUnbind() { + mThreadingDomain.assertCurrentThread(); + + synchronized (mSharedLock) { + // Clear the ManagerProxy used with the old remote so we will ignore calls from any old + // remotes that somehow hold a reference to it. + mManagerProxy = null; + mListener.onProviderUnbound(); + } + } + + @Override + final void setRequest(@NonNull TimeZoneProviderRequest request) { + mThreadingDomain.assertCurrentThread(); + + Objects.requireNonNull(request); + synchronized (mSharedLock) { + mRequest = request; + + // Two possible outcomes here: Either we are already bound to a remote service, in + // which case trySendCurrentRequest() will communicate the request immediately, or we + // are not bound to the remote service yet, in which case it will be sent during + // onBindOnHandlerThread() instead. + trySendCurrentRequest(); + } + } + + @GuardedBy("mSharedLock") + private void trySendCurrentRequest() { + ManagerProxy managerProxy = mManagerProxy; + TimeZoneProviderRequest request = mRequest; + mServiceWatcher.runOnBinder(binder -> { + ITimeZoneProvider service = ITimeZoneProvider.Stub.asInterface(binder); + if (request.sendUpdates()) { + service.startUpdates(managerProxy, request.getInitializationTimeout().toMillis()); + } else { + service.stopUpdates(); + } + }); + } + + /** + * A stubbed implementation. + */ + @Override + void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) { + mThreadingDomain.assertCurrentThread(); + + if (callback != null) { + Bundle result = new Bundle(); + result.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, false); + result.putString(TEST_COMMAND_RESULT_ERROR_KEY, "Not implemented"); + callback.sendResult(result); + } + } + + @Override + public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) { + synchronized (mSharedLock) { + ipw.println("{RealLocationTimeZoneProviderProxy}"); + ipw.println("mRequest=" + mRequest); + mServiceWatcher.dump(null, ipw, args); + } + } + + /** + * A system Server-side proxy for the ITimeZoneProviderManager, i.e. this is a local binder stub + * Each "remote" TimeZoneProvider is passed a binder instance that it then uses to communicate + * back with the system server, invoking the logic here. + */ + private class ManagerProxy extends ITimeZoneProviderManager.Stub { + + // executed on binder thread + @Override + public void onTimeZoneProviderSuggestion(TimeZoneProviderSuggestion suggestion) { + onTimeZoneProviderEvent(TimeZoneProviderEvent.createSuggestionEvent(suggestion)); + } + + // executed on binder thread + @Override + public void onTimeZoneProviderUncertain() { + onTimeZoneProviderEvent(TimeZoneProviderEvent.createUncertainEvent()); + + } + + // executed on binder thread + @Override + public void onTimeZoneProviderPermanentFailure(String failureReason) { + onTimeZoneProviderEvent( + TimeZoneProviderEvent.createPermanentFailureEvent(failureReason)); + } + + private void onTimeZoneProviderEvent(TimeZoneProviderEvent event) { + synchronized (mSharedLock) { + if (mManagerProxy != this) { + // Ignore incoming calls if this instance is no longer the current + // mManagerProxy. + return; + } + } + handleTimeZoneProviderEvent(event); + } + } +} diff --git a/services/core/java/com/android/server/timezonedetector/location/SimulatedLocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/timezonedetector/location/SimulatedLocationTimeZoneProviderProxy.java new file mode 100644 index 000000000000..02b0a849c1b1 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/location/SimulatedLocationTimeZoneProviderProxy.java @@ -0,0 +1,211 @@ +/* + * 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.app.time.LocationTimeZoneManager.SIMULATED_PROVIDER_TEST_COMMAND_ON_BIND; +import static android.app.time.LocationTimeZoneManager.SIMULATED_PROVIDER_TEST_COMMAND_ON_UNBIND; +import static android.app.time.LocationTimeZoneManager.SIMULATED_PROVIDER_TEST_COMMAND_PERM_FAILURE; +import static android.app.time.LocationTimeZoneManager.SIMULATED_PROVIDER_TEST_COMMAND_SUCCESS; +import static android.app.time.LocationTimeZoneManager.SIMULATED_PROVIDER_TEST_COMMAND_SUCCESS_ARG_KEY_TZ; +import static android.app.time.LocationTimeZoneManager.SIMULATED_PROVIDER_TEST_COMMAND_UNCERTAIN; +import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_ERROR_KEY; +import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_SUCCESS_KEY; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.os.Bundle; +import android.os.RemoteCallback; +import android.os.SystemClock; +import android.service.timezone.TimeZoneProviderSuggestion; +import android.util.IndentingPrintWriter; + +import com.android.internal.annotations.GuardedBy; +import com.android.server.timezonedetector.ReferenceWithHistory; + +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.Objects; + +/** + * A replacement for a real binder proxy for use during integration testing + * that can be used to inject simulated {@link LocationTimeZoneProviderProxy} behavior. + */ +class SimulatedLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy { + + @GuardedBy("mSharedLock") + @NonNull private TimeZoneProviderRequest mRequest; + + @GuardedBy("mSharedLock") + @NonNull private final ReferenceWithHistory mLastEvent = new ReferenceWithHistory<>(50); + + SimulatedLocationTimeZoneProviderProxy( + @NonNull Context context, @NonNull ThreadingDomain threadingDomain) { + super(context, threadingDomain); + mRequest = TimeZoneProviderRequest.createStopUpdatesRequest(); + } + + @Override + void onInitialize() { + // No-op - nothing to do for the simulated provider. + } + + @Override + void onDestroy() { + // No-op - nothing to do for the simulated provider. + } + + void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) { + mThreadingDomain.assertCurrentThread(); + + Objects.requireNonNull(testCommand); + + synchronized (mSharedLock) { + Bundle resultBundle = new Bundle(); + switch (testCommand.getName()) { + case SIMULATED_PROVIDER_TEST_COMMAND_ON_BIND: { + mLastEvent.set("Simulating onProviderBound(), testCommand=" + testCommand); + mThreadingDomain.post(this::onBindOnHandlerThread); + resultBundle.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, true); + break; + } + case SIMULATED_PROVIDER_TEST_COMMAND_ON_UNBIND: { + mLastEvent.set("Simulating onProviderUnbound(), testCommand=" + testCommand); + mThreadingDomain.post(this::onUnbindOnHandlerThread); + resultBundle.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, true); + break; + } + case SIMULATED_PROVIDER_TEST_COMMAND_PERM_FAILURE: + case SIMULATED_PROVIDER_TEST_COMMAND_UNCERTAIN: + case SIMULATED_PROVIDER_TEST_COMMAND_SUCCESS: { + if (!mRequest.sendUpdates()) { + String errorMsg = "testCommand=" + testCommand + + " is testing an invalid case:" + + " updates are off. mRequest=" + mRequest; + mLastEvent.set(errorMsg); + resultBundle.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, false); + resultBundle.putString(TEST_COMMAND_RESULT_ERROR_KEY, errorMsg); + break; + } + mLastEvent.set("Simulating TimeZoneProviderEvent, testCommand=" + testCommand); + TimeZoneProviderEvent timeZoneProviderEvent = + createTimeZoneProviderEventFromTestCommand(testCommand); + handleTimeZoneProviderEvent(timeZoneProviderEvent); + resultBundle.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, true); + break; + } + default: { + String errorMsg = "Unknown test event type. testCommand=" + testCommand; + mLastEvent.set(errorMsg); + resultBundle.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, false); + resultBundle.putString(TEST_COMMAND_RESULT_ERROR_KEY, errorMsg); + break; + } + } + if (callback != null) { + callback.sendResult(resultBundle); + } + } + } + + private void onBindOnHandlerThread() { + mThreadingDomain.assertCurrentThread(); + + synchronized (mSharedLock) { + mListener.onProviderBound(); + } + } + + private void onUnbindOnHandlerThread() { + mThreadingDomain.assertCurrentThread(); + + synchronized (mSharedLock) { + mListener.onProviderUnbound(); + } + } + + @Override + final void setRequest(@NonNull TimeZoneProviderRequest request) { + mThreadingDomain.assertCurrentThread(); + + Objects.requireNonNull(request); + synchronized (mSharedLock) { + mLastEvent.set("Request received: " + request); + mRequest = request; + } + } + + @Override + public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) { + synchronized (mSharedLock) { + ipw.println("{SimulatedLocationTimeZoneProviderProxy}"); + ipw.println("mRequest=" + mRequest); + ipw.println("mLastEvent=" + mLastEvent); + + ipw.increaseIndent(); + ipw.println("Last event history:"); + mLastEvent.dump(ipw); + ipw.decreaseIndent(); + } + } + + /** + * Prints the command line options that to create a {@link TestCommand} that can be passed to + * {@link #createTimeZoneProviderEventFromTestCommand(TestCommand)}. + */ + static void printTestCommandShellHelp(@NonNull PrintWriter pw) { + pw.printf("%s\n", SIMULATED_PROVIDER_TEST_COMMAND_ON_BIND); + pw.printf("%s\n", SIMULATED_PROVIDER_TEST_COMMAND_ON_UNBIND); + pw.printf("%s\n", SIMULATED_PROVIDER_TEST_COMMAND_PERM_FAILURE); + pw.printf("%s\n", SIMULATED_PROVIDER_TEST_COMMAND_UNCERTAIN); + pw.printf("%s %s=string_array:

{@link TestCommand}s can be encoded as arguments in a shell command. See + * {@link #createFromShellCommandArgs(ShellCommand)} and {@link + * #printShellCommandEncodingHelp(PrintWriter)}. + */ +final class TestCommand { + + private static final Pattern SHELL_ARG_PATTERN = Pattern.compile("([^=]+)=([^:]+):(.*)"); + private static final Pattern SHELL_ARG_VALUE_SPLIT_PATTERN = Pattern.compile("&"); + + @NonNull private final String mName; + @NonNull private final Bundle mArgs; + + /** Creates a {@link TestCommand} from components. */ + private TestCommand(@NonNull String type, @NonNull Bundle args) { + mName = Objects.requireNonNull(type); + mArgs = Objects.requireNonNull(args); + } + + @VisibleForTesting + @NonNull + public static TestCommand createForTests(@NonNull String type, @NonNull Bundle args) { + return new TestCommand(type, args); + } + + /** + * Creates a {@link TestCommand} from a {@link ShellCommand}'s remaining arguments. + * + * See {@link #printShellCommandEncodingHelp(PrintWriter)} for encoding details. + */ + @NonNull + public static TestCommand createFromShellCommandArgs(@NonNull ShellCommand shellCommand) { + String name = shellCommand.getNextArgRequired(); + Bundle args = new Bundle(); + String argKeyAndValue; + while ((argKeyAndValue = shellCommand.getNextArg()) != null) { + Matcher matcher = SHELL_ARG_PATTERN.matcher(argKeyAndValue); + if (!matcher.matches()) { + throw new IllegalArgumentException( + argKeyAndValue + " does not match " + SHELL_ARG_PATTERN); + } + String key = matcher.group(1); + String type = matcher.group(2); + String encodedValue = matcher.group(3); + Object value = getTypedValue(type, encodedValue); + args.putObject(key, value); + } + return new TestCommand(name, args); + } + + /** + * Returns the command's name. + */ + @NonNull + public String getName() { + return mName; + } + + /** + * Returns the arg values. Returns an empty bundle if there are no args. + */ + @NonNull + public Bundle getArgs() { + return mArgs.deepCopy(); + } + + @Override + public String toString() { + return "TestCommand{" + + "mName=" + mName + + ", mArgs=" + mArgs + + '}'; + } + + /** + * Prints the text format that {@link #createFromShellCommandArgs(ShellCommand)} understands. + */ + public static void printShellCommandEncodingHelp(@NonNull PrintWriter pw) { + pw.println("Test commands are encoded on the command line as: *"); + pw.println(); + pw.println("The is a string"); + pw.println("The encoding is: \"key=type:value\""); + pw.println(); + pw.println("e.g. \"myKey=string:myValue\" represents an argument with the key \"myKey\"" + + " and a string value of \"myValue\""); + pw.println("Values are one or more URI-encoded strings separated by & characters. Only some" + + " types support multiple values, e.g. string arrays."); + pw.println(); + pw.println("Recognized types are: string, boolean, double, long, string_array."); + pw.println(); + pw.println("When passing test commands via adb shell, the & can be escaped by quoting the" + + " and escaping the & with \\"); + pw.println("For example:"); + pw.println(" $ adb shell ... my-command \"key1=string_array:value1\\&value2\""); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TestCommand that = (TestCommand) o; + return mName.equals(that.mName) + && mArgs.kindofEquals(that.mArgs); + } + + @Override + public int hashCode() { + return Objects.hash(mName, mArgs); + } + + + private static Object getTypedValue(String type, String encodedValue) { + // The value is stored in a URL encoding. Multiple value types have values separated with + // a & character. + String[] values = SHELL_ARG_VALUE_SPLIT_PATTERN.split(encodedValue); + + // URI decode the values. + for (int i = 0; i < values.length; i++) { + values[i] = Uri.decode(values[i]); + } + + switch (type) { + case "boolean": { + checkSingleValue(values); + return Boolean.parseBoolean(values[0]); + } + case "double": { + checkSingleValue(values); + return Double.parseDouble(values[0]); + } + case "long": { + checkSingleValue(values); + return Long.parseLong(values[0]); + } + case "string": { + checkSingleValue(values); + return values[0]; + } + case "string_array": { + return values; + } + default: { + throw new IllegalArgumentException("Unknown type: " + type); + } + } + + } + + private static void checkSingleValue(String[] values) { + if (values.length != 1) { + throw new IllegalArgumentException("Expected a single value, but there were multiple: " + + Arrays.toString(values)); + } + } +} diff --git a/services/core/java/com/android/server/timezonedetector/location/ThreadingDomain.java b/services/core/java/com/android/server/timezonedetector/location/ThreadingDomain.java new file mode 100644 index 000000000000..9e3497f92dc0 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/location/ThreadingDomain.java @@ -0,0 +1,190 @@ +/* + * 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 android.annotation.DurationMillisLong; +import android.annotation.NonNull; + +import com.android.internal.util.Preconditions; + +import java.util.concurrent.Callable; + +/** + * A class that can be used to enforce / indicate a set of components that need to share threading + * behavior such as a shared lock object and a common thread, with async execution support. + * + *

It is not essential that the object returned by {@link #getLockObject()} is only used + * when executing on the domain's thread, but users should be careful to avoid deadlocks when + * multiple locks / threads are in use. Generally sticking to a single thread / lock is safest. + */ +abstract class ThreadingDomain { + + @NonNull private final Object mLockObject; + + ThreadingDomain() { + mLockObject = new Object(); + } + + /** + * Returns the common lock object for this threading domain that can be used for synchronized () + * blocks. The lock is unique to this threading domain. + */ + @NonNull + Object getLockObject() { + return mLockObject; + } + + /** + * Returns the Thread associated with this threading domain. + */ + @NonNull + abstract Thread getThread(); + + /** + * Asserts the currently executing thread is the one associated with this threading domain. + * Generally useful for documenting expectations in the code. By asserting a single thread is + * being used within a set of components, a lot of races can be avoided. + */ + void assertCurrentThread() { + Preconditions.checkState(Thread.currentThread() == getThread()); + } + + /** + * Asserts the currently executing thread is not the one associated with this threading domain. + * Generally useful for documenting expectations in the code and avoiding deadlocks. + */ + void assertNotCurrentThread() { + Preconditions.checkState(Thread.currentThread() != getThread()); + } + + /** + * Execute the supplied runnable on the threading domain's thread. + */ + abstract void post(@NonNull Runnable runnable); + + /** + * Executes the supplied runnable and waits for up to the duration specified for it to be + * executed. This is only intended for use by test and/or shell command code as it consumes + * multiple threads and could lead to deadlocks. + * + *

An {@link IllegalStateException} will be thrown if calling this method would cause a + * deadlock, e.g. if it is called using the threading domain's own thread. + */ + final void postAndWait(@NonNull Runnable runnable, @DurationMillisLong long durationMillis) { + try { + postAndWait(() -> { + runnable.run(); + return null; + }, durationMillis); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Executes the supplied callable and waits for up to the duration specified for it to be + * executed. This is only intended for use by test and/or shell command code as it consumes + * multiple threads and could lead to deadlocks. + * + *

An {@link IllegalStateException} will be thrown if calling this method would cause a + * deadlock, e.g. if it is called using the threading domain's own thread. + */ + abstract V postAndWait( + @NonNull Callable callable, @DurationMillisLong long durationMillis) + throws Exception; + + /** + * Execute the supplied runnable on the threading domain's thread with a delay. + */ + abstract void postDelayed(@NonNull Runnable runnable, @DurationMillisLong long delayMillis); + + abstract void postDelayed(Runnable r, Object token, @DurationMillisLong long delayMillis); + + abstract void removeQueuedRunnables(Object token); + + /** + * Creates a new {@link SingleRunnableQueue} that can be used to ensure that (at most) a + * single runnable for a given purpose is ever queued. Create new ones for different purposes. + */ + SingleRunnableQueue createSingleRunnableQueue() { + return new SingleRunnableQueue(); + } + + /** + * 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 { + + private boolean mIsQueued; + @DurationMillisLong + private long mDelayMillis; + + /** + * Posts the supplied {@link Runnable} asynchronously and delayed on the threading domain + * handler 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 runDelayed(Runnable r, @DurationMillisLong long delayMillis) { + cancel(); + 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. + */ + @DurationMillisLong + 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(); + if (mIsQueued) { + removeQueuedRunnables(this); + } + mIsQueued = false; + mDelayMillis = -1; + } + } +} diff --git a/services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderEvent.java b/services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderEvent.java new file mode 100644 index 000000000000..3e224e03fda0 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderEvent.java @@ -0,0 +1,143 @@ +/* + * 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 android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.service.timezone.TimeZoneProviderService; +import android.service.timezone.TimeZoneProviderSuggestion; + +import java.util.Objects; + +/** + * An event from a {@link TimeZoneProviderService}. + */ +final class TimeZoneProviderEvent { + + @IntDef(prefix = "EVENT_TYPE_", + value = { EVENT_TYPE_PERMANENT_FAILURE, EVENT_TYPE_SUGGESTION, EVENT_TYPE_UNCERTAIN }) + public @interface EventType {} + + /** + * The provider failed permanently. See {@link + * TimeZoneProviderService#reportPermanentFailure(Throwable)} + */ + public static final int EVENT_TYPE_PERMANENT_FAILURE = 1; + + /** + * The provider made a suggestion. See {@link + * TimeZoneProviderService#reportSuggestion(TimeZoneProviderSuggestion)} + */ + public static final int EVENT_TYPE_SUGGESTION = 2; + + /** + * The provider was uncertain about the time zone. See {@link + * TimeZoneProviderService#reportUncertain()} + */ + public static final int EVENT_TYPE_UNCERTAIN = 3; + + private static final TimeZoneProviderEvent UNCERTAIN_EVENT = + new TimeZoneProviderEvent(EVENT_TYPE_UNCERTAIN, null, null); + + @EventType + private final int mType; + + @Nullable + private final TimeZoneProviderSuggestion mSuggestion; + + @Nullable + private final String mFailureCause; + + private TimeZoneProviderEvent(@EventType int type, + @Nullable TimeZoneProviderSuggestion suggestion, + @Nullable String failureCause) { + mType = type; + mSuggestion = suggestion; + mFailureCause = failureCause; + } + + /** Returns a event of type {@link #EVENT_TYPE_SUGGESTION}. */ + public static TimeZoneProviderEvent createSuggestionEvent( + @NonNull TimeZoneProviderSuggestion suggestion) { + return new TimeZoneProviderEvent(EVENT_TYPE_SUGGESTION, + Objects.requireNonNull(suggestion), null); + } + + /** Returns a event of type {@link #EVENT_TYPE_UNCERTAIN}. */ + public static TimeZoneProviderEvent createUncertainEvent() { + return UNCERTAIN_EVENT; + } + + /** Returns a event of type {@link #EVENT_TYPE_PERMANENT_FAILURE}. */ + public static TimeZoneProviderEvent createPermanentFailureEvent(@NonNull String cause) { + return new TimeZoneProviderEvent(EVENT_TYPE_PERMANENT_FAILURE, null, + Objects.requireNonNull(cause)); + } + + /** + * Returns the event type. + */ + public @EventType int getType() { + return mType; + } + + /** + * Returns the suggestion. Populated when {@link #getType()} is {@link #EVENT_TYPE_SUGGESTION}. + */ + @Nullable + public TimeZoneProviderSuggestion getSuggestion() { + return mSuggestion; + } + + /** + * Returns the failure cauese. Populated when {@link #getType()} is {@link + * #EVENT_TYPE_PERMANENT_FAILURE}. + */ + @Nullable + public String getFailureCause() { + return mFailureCause; + } + + @Override + public String toString() { + return "TimeZoneProviderEvent{" + + "mType=" + mType + + ", mSuggestion=" + mSuggestion + + ", mFailureCause=" + mFailureCause + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TimeZoneProviderEvent that = (TimeZoneProviderEvent) o; + return mType == that.mType + && Objects.equals(mSuggestion, that.mSuggestion) + && Objects.equals(mFailureCause, that.mFailureCause); + } + + @Override + public int hashCode() { + return Objects.hash(mType, mSuggestion, mFailureCause); + } +} diff --git a/services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderRequest.java b/services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderRequest.java new file mode 100644 index 000000000000..14820319d9df --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderRequest.java @@ -0,0 +1,105 @@ +/* + * 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 android.annotation.NonNull; +import android.annotation.Nullable; + +import java.time.Duration; +import java.util.Objects; + +/** + * This class encapsulates a request to a provider. + */ +final class TimeZoneProviderRequest { + + @NonNull + private static final TimeZoneProviderRequest STOP_UPDATES = + new TimeZoneProviderRequest( + false /* sendUpdates */, + null /* initializationTimeout */); + + private final boolean mSendUpdates; + + @Nullable + private final Duration mInitializationTimeout; + + private TimeZoneProviderRequest( + boolean sendUpdates, @Nullable Duration initializationTimeout) { + mSendUpdates = sendUpdates; + mInitializationTimeout = initializationTimeout; + } + + /** Creates a request to start updates with the specified timeout. */ + public static TimeZoneProviderRequest createStartUpdatesRequest( + @NonNull Duration initializationTimeout) { + return new TimeZoneProviderRequest(true, Objects.requireNonNull(initializationTimeout)); + } + + /** Creates a request to stop updates. */ + public static TimeZoneProviderRequest createStopUpdatesRequest() { + return STOP_UPDATES; + } + + /** + * Returns {@code true} if the provider should send updates related to the device's current + * time zone, {@code false} otherwise. + */ + public boolean sendUpdates() { + return mSendUpdates; + } + + // TODO(b/152744911) - once there are a couple of implementations, decide whether this needs to + // be passed to the TimeZoneProviderService and remove if it is not useful. + /** + * Returns the maximum time that the provider is allowed to initialize before it is expected to + * send an event of any sort. Only valid when {@link #sendUpdates()} is {@code true}. Failure to + * send an event in this time (with some fuzz) may be interpreted as if the provider is + * uncertain of the time zone, and/or it could lead to the provider being stopped. + */ + @Nullable + public Duration getInitializationTimeout() { + return mInitializationTimeout; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TimeZoneProviderRequest + that = (TimeZoneProviderRequest) o; + return mSendUpdates == that.mSendUpdates + && mInitializationTimeout == that.mInitializationTimeout; + } + + @Override + public int hashCode() { + return Objects.hash(mSendUpdates, mInitializationTimeout); + } + + @Override + public String toString() { + return "TimeZoneProviderRequest{" + + "mSendUpdates=" + mSendUpdates + + ", mInitializationTimeout=" + mInitializationTimeout + + "}"; + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 12595af1db7f..0768bb93839d 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -326,7 +326,7 @@ public final class SystemServer implements Dumpable { private static final String TIME_ZONE_DETECTOR_SERVICE_CLASS = "com.android.server.timezonedetector.TimeZoneDetectorService$Lifecycle"; private static final String LOCATION_TIME_ZONE_MANAGER_SERVICE_CLASS = - "com.android.server.location.timezone.LocationTimeZoneManagerService$Lifecycle"; + "com.android.server.timezonedetector.location.LocationTimeZoneManagerService$Lifecycle"; private static final String GNSS_TIME_UPDATE_SERVICE_CLASS = "com.android.server.timedetector.GnssTimeUpdateService$Lifecycle"; private static final String ACCESSIBILITY_MANAGER_SERVICE_CLASS = diff --git a/services/tests/servicestests/src/com/android/server/location/timezone/ControllerImplTest.java b/services/tests/servicestests/src/com/android/server/location/timezone/ControllerImplTest.java deleted file mode 100644 index 972b3bb8b459..000000000000 --- a/services/tests/servicestests/src/com/android/server/location/timezone/ControllerImplTest.java +++ /dev/null @@ -1,1316 +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.location.timezone; - -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_CERTAIN; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_UNCERTAIN; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STOPPED; -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.USER2_CONFIG_GEO_DETECTION_ENABLED; -import static com.android.server.location.timezone.TimeZoneProviderEvent.createPermanentFailureEvent; -import static com.android.server.location.timezone.TimeZoneProviderEvent.createUncertainEvent; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import static java.util.Arrays.asList; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.platform.test.annotations.Presubmit; -import android.service.timezone.TimeZoneProviderSuggestion; -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; - -import org.junit.Before; -import org.junit.Test; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -/** Tests for {@link ControllerImpl}. */ -@Presubmit -public class ControllerImplTest { - - private static final long ARBITRARY_TIME_MILLIS = 12345L; - - private static final TimeZoneProviderEvent USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1 = - createSuggestionEvent(asList("Europe/London")); - private static final TimeZoneProviderEvent USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2 = - createSuggestionEvent(asList("Europe/Paris")); - private static final TimeZoneProviderEvent USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT = - createUncertainEvent(); - private static final TimeZoneProviderEvent USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT = - createPermanentFailureEvent("Test"); - - private TestThreadingDomain mTestThreadingDomain; - private TestCallback mTestCallback; - private TestLocationTimeZoneProvider mTestPrimaryLocationTimeZoneProvider; - private TestLocationTimeZoneProvider mTestSecondaryLocationTimeZoneProvider; - - @Before - public void setUp() { - // For simplicity, the TestThreadingDomain uses the test's main thread. To execute posted - // runnables, the test must call methods on mTestThreadingDomain otherwise those runnables - // will never get a chance to execute. - mTestThreadingDomain = new TestThreadingDomain(); - mTestCallback = new TestCallback(mTestThreadingDomain); - mTestPrimaryLocationTimeZoneProvider = - new TestLocationTimeZoneProvider(mTestThreadingDomain, "primary"); - mTestSecondaryLocationTimeZoneProvider = - new TestLocationTimeZoneProvider(mTestThreadingDomain, "secondary"); - } - - @Test - public void initializationFailure_primary() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - Duration expectedInitTimeout = testEnvironment.getProviderInitializationTimeout() - .plus(testEnvironment.getProviderInitializationTimeoutFuzz()); - - mTestPrimaryLocationTimeZoneProvider.setFailDuringInitialization(true); - - // Initialize. After initialization the providers must be initialized and one should be - // started. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertInitialized(); - mTestSecondaryLocationTimeZoneProvider.assertInitialized(); - - mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertInitializationTimeoutSet(expectedInitTimeout); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void initializationFailure_secondary() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - Duration expectedInitTimeout = testEnvironment.getProviderInitializationTimeout() - .plus(testEnvironment.getProviderInitializationTimeoutFuzz()); - - mTestSecondaryLocationTimeZoneProvider.setFailDuringInitialization(true); - - // Initialize. After initialization the providers must be initialized and one should be - // started. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertInitialized(); - mTestSecondaryLocationTimeZoneProvider.assertInitialized(); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestPrimaryLocationTimeZoneProvider.assertInitializationTimeoutSet(expectedInitTimeout); - mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void initializationFailure_both() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - - mTestPrimaryLocationTimeZoneProvider.setFailDuringInitialization(true); - mTestSecondaryLocationTimeZoneProvider.setFailDuringInitialization(true); - - // Initialize. After initialization the providers must be initialized and one should be - // started. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertInitialized(); - mTestSecondaryLocationTimeZoneProvider.assertInitialized(); - - mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestCallback.assertUncertainSuggestionMadeAndCommit(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void initialState_started() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - Duration expectedInitTimeout = testEnvironment.getProviderInitializationTimeout() - .plus(testEnvironment.getProviderInitializationTimeoutFuzz()); - - // Initialize. After initialization the providers must be initialized and one should be - // started. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertInitialized(); - mTestSecondaryLocationTimeZoneProvider.assertInitialized(); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestPrimaryLocationTimeZoneProvider.assertInitializationTimeoutSet(expectedInitTimeout); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void initialState_disabled() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_DISABLED); - - // Initialize. After initialization the providers must be initialized but neither should be - // started. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertInitialized(); - mTestSecondaryLocationTimeZoneProvider.assertInitialized(); - - mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void enabled_uncertaintySuggestionSentIfNoEventReceived() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - - // Initialize and check initial state. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate time passing with no provider event being received from the primary. - mTestThreadingDomain.executeNext(); - - // The primary should have reported uncertainty, which should trigger the controller to - // start the uncertainty timeout and start the secondary. - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertNoSuggestionMade(); - assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); - - // Simulate time passing with no provider event being received from either the primary or - // secondary. - mTestThreadingDomain.executeNext(); - - // Now both initialization timeouts should have triggered. The uncertainty timeout should - // still not be triggered. - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_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_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertUncertainSuggestionMadeAndCommit(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void enabled_eventReceivedBeforeInitializationTimeout() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - - // Initialize and check initial state. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate a location event being received from the primary provider. This should cause a - // suggestion to be made. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertSuggestionMadeAndCommit( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds()); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void enabled_eventReceivedFromPrimaryAfterInitializationTimeout() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - - // Initialize and check initial state. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate time passing with no provider event being received from the primary. - mTestThreadingDomain.executeNext(); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, 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 and the secondary to be shut down. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertSuggestionMadeAndCommit( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds()); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void enabled_eventReceivedFromSecondaryAfterInitializationTimeout() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - - // Initialize and check initial state. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate time passing with no provider event being received from the primary. - mTestThreadingDomain.executeNext(); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertNoSuggestionMade(); - assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); - - // Simulate a location event being received from the secondary provider. This should cause a - // suggestion to be made. - mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertSuggestionMadeAndCommit( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds()); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void enabled_repeatedPrimaryCertainty() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - - // Initialize and check initial state. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate a location event being received from the primary provider. This should cause a - // suggestion to be made. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertSuggestionMadeAndCommit( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds()); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // A second, identical event should not cause another suggestion. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // And a third, different event should cause another suggestion. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertSuggestionMadeAndCommit( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2.getSuggestion().getTimeZoneIds()); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void enabled_repeatedSecondaryCertainty() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - - // Initialize and check initial state. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate time passing with no provider event being received from the primary. - mTestThreadingDomain.executeNext(); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertNoSuggestionMade(); - assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); - - // Simulate a location event being received from the secondary provider. This should cause a - // suggestion to be made. - mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertSuggestionMadeAndCommit( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds()); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // A second, identical event should not cause another suggestion. - mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // And a third, different event should cause another suggestion. - mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertSuggestionMadeAndCommit( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2.getSuggestion().getTimeZoneIds()); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void enabled_uncertaintyTriggersASuggestionAfterUncertaintyTimeout() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - - // Initialize and check initial state. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - 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.simulateTimeZoneProviderEvent( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertSuggestionMadeAndCommit( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().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 and the secondary should be started. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertNoSuggestionMade(); - assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); - - // Simulate a location event being received from the secondary provider. This should cause a - // suggestion to be made, cancel the uncertainty timeout and ensure the secondary is - // considered initialized. - mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertSuggestionMadeAndCommit( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2.getSuggestion().getTimeZoneIds()); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate an uncertain event being received from the secondary provider. This should not - // cause a suggestion to be made straight away, but the uncertainty timeout should be - // started. Both providers are now started, with no initialization timeout set. - mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_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_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertUncertainSuggestionMadeAndCommit(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void enabled_briefUncertaintyTriggersNoSuggestion() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - - // Initialize and check initial state. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate a location event being received from the primary provider. This should cause a - // suggestion to be made. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertSuggestionMadeAndCommit( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds()); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Uncertainty should not cause a suggestion to be made straight away, but the uncertainty - // timeout should be started and the secondary should be started. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertNoSuggestionMade(); - assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); - - // And a success event from the primary provider should cause the controller to make another - // suggestion, the uncertainty timeout should be cancelled and the secondary should be - // stopped again. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertSuggestionMadeAndCommit( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2.getSuggestion().getTimeZoneIds()); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void configChanges_enableAndDisableWithNoPreviousSuggestion() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_DISABLED); - - // Initialize and check initial state. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Now signal a config change so that geo detection is enabled. - testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_ENABLED); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Now signal a config change so that geo detection is disabled. - testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_DISABLED); - - mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void configChanges_enableAndDisableWithPreviousSuggestion() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_DISABLED); - - // Initialize and check initial state. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Now signal a config change so that geo detection is enabled. - testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_ENABLED); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate a success event being received from the primary provider. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertSuggestionMadeAndCommit( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds()); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // 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. - testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_DISABLED); - - mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertUncertainSuggestionMadeAndCommit(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void configChanges_userSwitch_enabledToEnabled() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - - // Initialize and check initial state. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate the primary provider suggesting a time zone. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - 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. - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertSuggestionMadeAndCommit( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().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_STARTED_INITIALIZING, but it should - // have been stopped when the user changed. - int[] expectedStateTransitions = - { PROVIDER_STATE_STOPPED, PROVIDER_STATE_STARTED_INITIALIZING }; - mTestPrimaryLocationTimeZoneProvider.assertStateChangesAndCommit(expectedStateTransitions); - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfig( - PROVIDER_STATE_STARTED_INITIALIZING, USER2_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void primaryPermFailure_secondaryEventsReceived() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - - // Initialize and check initial state. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate a failure location event being received from the primary provider. This should - // cause the secondary to be started. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT); - - mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate uncertainty from the secondary. - mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); - - mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertNoSuggestionMade(); - assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); - - // And a success event from the secondary provider should cause the controller to make - // another suggestion, the uncertainty timeout should be cancelled. - mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); - - mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertSuggestionMadeAndCommit( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2.getSuggestion().getTimeZoneIds()); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate uncertainty from the secondary. - mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); - - mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertNoSuggestionMade(); - assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); - } - - @Test - public void primaryPermFailure_disableAndEnable() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - - // Initialize and check initial state. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate a failure location event being received from the primary provider. This should - // cause the secondary to be started. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT); - - mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_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); - - mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - 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(); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void secondaryPermFailure_primaryEventsReceived() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - - // Initialize and check initial state. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate an uncertain event from the primary. This will start the secondary, which will - // give this test the opportunity to simulate its failure. Then it will be possible to - // demonstrate controller behavior with only the primary working. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertNoSuggestionMade(); - assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); - - // Simulate failure event from the secondary. This should just affect the secondary's state. - mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); - - // And a success event from the primary provider should cause the controller to make - // a suggestion, the uncertainty timeout should be cancelled. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestCallback.assertSuggestionMadeAndCommit( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2.getSuggestion().getTimeZoneIds()); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate uncertainty from the primary. The secondary cannot be started. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); - } - - @Test - public void secondaryPermFailure_disableAndEnable() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - - // Initialize and check initial state. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate an uncertain event from the primary. This will start the secondary, which will - // give this test the opportunity to simulate its failure. Then it will be possible to - // demonstrate controller behavior with only the primary working. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertNoSuggestionMade(); - assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); - - // Simulate failure event from the secondary. This should just affect the secondary's state. - mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); - - // Now signal a config change so that geo detection is disabled. - testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_DISABLED); - - mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Now signal a config change so that geo detection is enabled. Only the primary can be - // started. - testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_ENABLED); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void bothPermFailure_disableAndEnable() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - - // Initialize and check initial state. - controllerImpl.initialize(testEnvironment, mTestCallback); - - mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate a failure event from the primary. This will start the secondary. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT); - - mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( - PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); - mTestCallback.assertNoSuggestionMade(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - - // Simulate failure event from the secondary. - mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT); - - mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); - mTestCallback.assertUncertainSuggestionMadeAndCommit(); - assertFalse(controllerImpl.isUncertaintyTimeoutSet()); - } - - @Test - public void stateRecording() { - ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, - mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); - TestEnvironment testEnvironment = new TestEnvironment( - mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); - - // Initialize and check initial state. - controllerImpl.initialize(testEnvironment, mTestCallback); - - { - LocationTimeZoneManagerServiceState state = controllerImpl.getStateForTests(); - assertNull(state.getLastSuggestion()); - assertTrue(state.getPrimaryProviderStates().isEmpty()); - assertTrue(state.getSecondaryProviderStates().isEmpty()); - } - - // State recording and simulate some provider behavior that will show up in the state - // recording. - controllerImpl.setProviderStateRecordingEnabled(true); - - // Simulate an uncertain event from the primary. This will start the secondary. - mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); - - { - LocationTimeZoneManagerServiceState state = controllerImpl.getStateForTests(); - assertNull(state.getLastSuggestion()); - List primaryProviderStates = - state.getPrimaryProviderStates(); - assertEquals(1, primaryProviderStates.size()); - assertEquals(PROVIDER_STATE_STARTED_UNCERTAIN, - primaryProviderStates.get(0).stateEnum); - List secondaryProviderStates = - state.getSecondaryProviderStates(); - assertEquals(1, secondaryProviderStates.size()); - assertEquals(PROVIDER_STATE_STARTED_INITIALIZING, - secondaryProviderStates.get(0).stateEnum); - } - - // Simulate an uncertain event from the primary. This will start the secondary. - mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( - USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); - - { - LocationTimeZoneManagerServiceState state = controllerImpl.getStateForTests(); - assertEquals(USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds(), - state.getLastSuggestion().getZoneIds()); - List primaryProviderStates = - state.getPrimaryProviderStates(); - assertEquals(1, primaryProviderStates.size()); - assertEquals(PROVIDER_STATE_STARTED_UNCERTAIN, primaryProviderStates.get(0).stateEnum); - List secondaryProviderStates = - state.getSecondaryProviderStates(); - assertEquals(2, secondaryProviderStates.size()); - assertEquals(PROVIDER_STATE_STARTED_CERTAIN, secondaryProviderStates.get(1).stateEnum); - } - - controllerImpl.setProviderStateRecordingEnabled(false); - { - LocationTimeZoneManagerServiceState state = controllerImpl.getStateForTests(); - assertEquals(USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds(), - state.getLastSuggestion().getZoneIds()); - assertTrue(state.getPrimaryProviderStates().isEmpty()); - assertTrue(state.getSecondaryProviderStates().isEmpty()); - } - } - - private static void assertUncertaintyTimeoutSet( - LocationTimeZoneProviderController.Environment environment, - LocationTimeZoneProviderController controller) { - assertTrue(controller.isUncertaintyTimeoutSet()); - assertEquals(environment.getUncertaintyDelay().toMillis(), - controller.getUncertaintyTimeoutDelayMillis()); - } - - private static TimeZoneProviderEvent createSuggestionEvent(@NonNull List timeZoneIds) { - return TimeZoneProviderEvent.createSuggestionEvent( - new TimeZoneProviderSuggestion.Builder() - .setElapsedRealtimeMillis(ARBITRARY_TIME_MILLIS) - .setTimeZoneIds(timeZoneIds) - .build()); - } - - 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(15); - - private final LocationTimeZoneProviderController mController; - private ConfigurationInternal mConfigurationInternal; - - TestEnvironment(ThreadingDomain threadingDomain, - LocationTimeZoneProviderController controller, - ConfigurationInternal configurationInternal) { - super(threadingDomain); - mController = Objects.requireNonNull(controller); - mConfigurationInternal = Objects.requireNonNull(configurationInternal); - } - - @Override - void destroy() { - // No-op test impl. - } - - @Override - ConfigurationInternal getCurrentUserConfigurationInternal() { - return mConfigurationInternal; - } - - @Override - Duration getProviderInitializationTimeout() { - return PROVIDER_INITIALIZATION_TIMEOUT; - } - - @Override - Duration getProviderInitializationTimeoutFuzz() { - return PROVIDER_INITIALIZATION_TIMEOUT_FUZZ; - } - - @Override - Duration getUncertaintyDelay() { - return UNCERTAINTY_DELAY; - } - - void simulateConfigChange(ConfigurationInternal newConfig) { - ConfigurationInternal oldConfig = mConfigurationInternal; - mConfigurationInternal = Objects.requireNonNull(newConfig); - if (Objects.equals(oldConfig, newConfig)) { - fail("Bad test? No config change when one was expected"); - } - mController.onConfigChanged(); - } - } - - private static class TestCallback extends LocationTimeZoneProviderController.Callback { - - private TestState mLatestSuggestion = new TestState<>(); - - TestCallback(ThreadingDomain threadingDomain) { - super(threadingDomain); - } - - @Override - void suggest(GeolocationTimeZoneSuggestion suggestion) { - mLatestSuggestion.set(suggestion); - } - - void assertSuggestionMadeAndCommit(@Nullable List expectedZoneIds) { - mLatestSuggestion.assertHasBeenSet(); - assertEquals(expectedZoneIds, mLatestSuggestion.getLatest().getZoneIds()); - mLatestSuggestion.commitLatest(); - } - - void assertNoSuggestionMade() { - mLatestSuggestion.assertHasNotBeenSet(); - } - - void assertUncertainSuggestionMadeAndCommit() { - // An "uncertain" suggestion has null time zone IDs. - assertSuggestionMadeAndCommit(null); - } - } - - private static class TestLocationTimeZoneProvider extends LocationTimeZoneProvider { - - /** Used to track historic provider states for tests. */ - private final TestState mTestProviderState = new TestState<>(); - private boolean mFailDuringInitialization; - private boolean mInitialized; - private boolean mDestroyed; - - /** - * Creates the instance. - */ - TestLocationTimeZoneProvider(ThreadingDomain threadingDomain, String providerName) { - super(threadingDomain, providerName); - } - - public void setFailDuringInitialization(boolean failInitialization) { - mFailDuringInitialization = failInitialization; - } - - @Override - void onInitialize() { - mInitialized = true; - if (mFailDuringInitialization) { - throw new RuntimeException("Simulated initialization failure"); - } - } - - @Override - void onDestroy() { - mDestroyed = true; - } - - @Override - void onSetCurrentState(ProviderState newState) { - mTestProviderState.set(newState); - } - - @Override - void onStartUpdates(Duration initializationTimeout) { - // Nothing needed for tests. - } - - @Override - void onStopUpdates() { - // Nothing needed for tests. - } - - @Override - public void dump(IndentingPrintWriter pw, String[] args) { - // Nothing needed for tests. - } - - /** Asserts that {@link #initialize(ProviderListener)} has been called. */ - void assertInitialized() { - assertTrue(mInitialized); - } - - public void assertIsPermFailedAndCommit() { - // A failed provider doesn't hold config. - assertStateEnumAndConfig(PROVIDER_STATE_PERM_FAILED, null /* config */); - mTestProviderState.commitLatest(); - } - - void assertIsStoppedAndCommit() { - // A stopped provider doesn't hold config. - assertStateEnumAndConfig(PROVIDER_STATE_STOPPED, 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_STARTED_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 simulateTimeZoneProviderEvent(@NonNull TimeZoneProviderEvent event) { - handleTimeZoneProviderEvent(event); - } - - /** - * Asserts the most recent state changes. The ordering is such that the last element in the - * provided array is expected to be the current state. - */ - void assertStateChangesAndCommit(int... expectedProviderStates) { - if (expectedProviderStates.length == 0) { - mTestProviderState.assertHasNotBeenSet(); - } else { - mTestProviderState.assertChangeCount(expectedProviderStates.length); - - List previousProviderStates = new ArrayList<>(); - for (int i = 0; i < expectedProviderStates.length; i++) { - previousProviderStates.add(mTestProviderState.getPrevious(i)); - } - // The loop above will produce a list with the most recent state in element 0. So, - // reverse the list as the arguments to this method are expected to be in order - // oldest...latest. - Collections.reverse(previousProviderStates); - - boolean allMatch = true; - for (int i = 0; i < expectedProviderStates.length; i++) { - allMatch = allMatch && expectedProviderStates[i] - == previousProviderStates.get(i).stateEnum; - } - if (!allMatch) { - fail("Provider state enums expected=" + Arrays.toString(expectedProviderStates) - + " but states were" - + " actually=" + previousProviderStates); - } - } - mTestProviderState.commitLatest(); - } - } -} diff --git a/services/tests/servicestests/src/com/android/server/location/timezone/HandlerThreadingDomainTest.java b/services/tests/servicestests/src/com/android/server/location/timezone/HandlerThreadingDomainTest.java deleted file mode 100644 index 02de24de435e..000000000000 --- a/services/tests/servicestests/src/com/android/server/location/timezone/HandlerThreadingDomainTest.java +++ /dev/null @@ -1,260 +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.location.timezone; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import android.os.Handler; -import android.os.HandlerThread; -import android.platform.test.annotations.Presubmit; - -import com.android.server.location.timezone.ThreadingDomain.SingleRunnableQueue; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.time.Duration; -import java.util.Objects; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -/** Tests for {@link HandlerThreadingDomain}. */ -@Presubmit -public class HandlerThreadingDomainTest { - - private HandlerThread mHandlerThread; - private Handler mTestHandler; - - @Before - public void setUp() { - mHandlerThread = new HandlerThread("HandlerThreadingDomainTest"); - mHandlerThread.start(); - mTestHandler = new Handler(mHandlerThread.getLooper()); - } - - @After - public void tearDown() throws Exception { - mHandlerThread.quit(); - mHandlerThread.join(); - } - - @Test - public void getLockObject() { - ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler); - assertSame("LockObject must be consistent", domain.getLockObject(), domain.getLockObject()); - } - - @Test - public void assertCurrentThread() throws Exception { - ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler); - - // Expect an exception (current thread != handler thread) - try { - domain.assertCurrentThread(); - fail("Expected exception"); - } catch (RuntimeException expected) { - // Expected - } - - // Expect no exception (current thread == handler thread) - AtomicBoolean exceptionThrown = new AtomicBoolean(true); - LatchedRunnable testCode = new LatchedRunnable(() -> { - domain.assertCurrentThread(); - exceptionThrown.set(false); - }); - mTestHandler.post(testCode); - testCode.assertCompletesWithin(60, TimeUnit.SECONDS); - assertFalse(exceptionThrown.get()); - } - - @Test - public void assertNotCurrentThread() throws Exception { - ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler); - - // Expect no exception (current thread != handler thread) - domain.assertNotCurrentThread(); - - AtomicBoolean exceptionThrown = new AtomicBoolean(false); - LatchedRunnable testCode = new LatchedRunnable(() -> { - // Expect an exception (current thread == handler thread) - try { - domain.assertNotCurrentThread(); - fail("Expected exception"); - } catch (RuntimeException expected) { - exceptionThrown.set(true); - } - }); - mTestHandler.post(testCode); - testCode.assertCompletesWithin(60, TimeUnit.SECONDS); - assertTrue(exceptionThrown.get()); - } - - @Test - public void post() throws Exception { - ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler); - AtomicBoolean ranOnExpectedThread = new AtomicBoolean(false); - LatchedRunnable testLogic = new LatchedRunnable(() -> { - ranOnExpectedThread.set(Thread.currentThread() == mTestHandler.getLooper().getThread()); - }); - domain.post(testLogic); - testLogic.assertCompletesWithin(60, TimeUnit.SECONDS); - assertTrue(testLogic.isComplete()); - assertTrue(ranOnExpectedThread.get()); - } - - @Test - public void postDelayed() throws Exception { - ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler); - - long beforeExecutionNanos = System.nanoTime(); - Duration executionDelay = Duration.ofSeconds(5); - - AtomicReference executionNanosHolder = new AtomicReference<>(); - AtomicBoolean ranOnExpectedThread = new AtomicBoolean(false); - LatchedRunnable testLogic = new LatchedRunnable(() -> { - ranOnExpectedThread.set(Thread.currentThread() == mTestHandler.getLooper().getThread()); - executionNanosHolder.set(System.nanoTime()); - }); - - domain.postDelayed(testLogic, executionDelay.toMillis()); - long afterPostNanos = System.nanoTime(); - - testLogic.assertCompletesWithin( - executionDelay.multipliedBy(10).toMillis(), TimeUnit.MILLISECONDS); - long afterWaitNanos = System.nanoTime(); - - assertTrue(testLogic.isComplete()); - assertTrue(ranOnExpectedThread.get()); - - // The execution should not take place until at least delayDuration after postDelayed(). - Duration actualExecutionDelay = - Duration.ofNanos(executionNanosHolder.get() - beforeExecutionNanos); - assertTrue(actualExecutionDelay.compareTo(executionDelay) >= 0); - - // The time taken in postDelayed() should be negligible. Certainly less than the - // executionDelay. - Duration postDuration = Duration.ofNanos(afterPostNanos - beforeExecutionNanos); - assertTrue(postDuration.compareTo(executionDelay) < 0); - - // The result should not be ready until at least executionDelay has elapsed. - Duration delayBeforeExecuted = Duration.ofNanos(afterWaitNanos - beforeExecutionNanos); - assertTrue(delayBeforeExecuted.compareTo(executionDelay) >= 0); - } - - @Test - public void postAndWait() throws Exception { - ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler); - - Duration workDuration = Duration.ofSeconds(5); - AtomicBoolean ranOnExpectedThread = new AtomicBoolean(false); - LatchedRunnable testLogic = new LatchedRunnable(() -> { - ranOnExpectedThread.set(Thread.currentThread() == mTestHandler.getLooper().getThread()); - - // The work takes workDuration to complete. - try { - Thread.sleep(workDuration.toMillis()); - } catch (InterruptedException e) { - throw new AssertionError(e); - } - }); - - long beforeExecutionNanos = System.nanoTime(); - domain.postAndWait(testLogic, workDuration.multipliedBy(10).toMillis()); - long afterExecutionNanos = System.nanoTime(); - Duration waitDuration = Duration.ofNanos(afterExecutionNanos - beforeExecutionNanos); - - assertTrue(waitDuration.compareTo(workDuration) >= 0); - assertTrue(testLogic.isComplete()); - assertTrue(ranOnExpectedThread.get()); - } - - @Test - public void singleRunnableQueue_runDelayed() throws Exception { - ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler); - SingleRunnableQueue singleRunnableQueue = domain.createSingleRunnableQueue(); - - long beforeExecutionNanos = System.nanoTime(); - - Runnable noOpRunnable = () -> { - // Deliberately do nothing - }; - LatchedRunnable firstRunnable = new LatchedRunnable(noOpRunnable); - LatchedRunnable secondRunnable = new LatchedRunnable(noOpRunnable); - - // Calls to SingleRunnableQueue must be made on the handler thread it is associated with, - // so this uses runWithScissors() to block until the runDelayedTestRunnable has completed. - Runnable runDelayedTestRunnable = () -> { - singleRunnableQueue.runDelayed(firstRunnable, TimeUnit.SECONDS.toMillis(10)); - - // The second runnable posted must clear the first. - singleRunnableQueue.runDelayed(secondRunnable, TimeUnit.SECONDS.toMillis(10)); - }; - mTestHandler.runWithScissors(runDelayedTestRunnable, TimeUnit.SECONDS.toMillis(60)); - - // Now wait for the second runnable to complete - secondRunnable.assertCompletesWithin(60, TimeUnit.SECONDS); - assertFalse(firstRunnable.isComplete()); - - long afterExecutionNanos = System.nanoTime(); - assertTrue(afterExecutionNanos - beforeExecutionNanos >= TimeUnit.SECONDS.toNanos(10)); - } - - private static boolean awaitWithRuntimeException( - CountDownLatch latch, long timeout, TimeUnit timeUnit) { - try { - return latch.await(timeout, timeUnit); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - private static class LatchedRunnable implements Runnable { - - private final CountDownLatch mLatch = new CountDownLatch(1); - private final Runnable mRunnable; - - LatchedRunnable(Runnable mRunnable) { - this.mRunnable = Objects.requireNonNull(mRunnable); - } - - @Override - public void run() { - try { - mRunnable.run(); - } finally { - mLatch.countDown(); - } - } - - boolean isComplete() { - return mLatch.getCount() == 0; - } - - boolean waitForCompletion(long timeout, TimeUnit unit) { - return awaitWithRuntimeException(mLatch, timeout, unit); - } - - void assertCompletesWithin(long timeout, TimeUnit unit) { - assertTrue("Runnable did not execute in time", waitForCompletion(timeout, unit)); - } - } -} diff --git a/services/tests/servicestests/src/com/android/server/location/timezone/LocationTimeZoneProviderTest.java b/services/tests/servicestests/src/com/android/server/location/timezone/LocationTimeZoneProviderTest.java deleted file mode 100644 index cb292db50115..000000000000 --- a/services/tests/servicestests/src/com/android/server/location/timezone/LocationTimeZoneProviderTest.java +++ /dev/null @@ -1,311 +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.location.timezone; - -import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_ERROR_KEY; -import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_SUCCESS_KEY; - -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_CERTAIN; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_UNCERTAIN; -import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STOPPED; -import static com.android.server.location.timezone.TestSupport.USER1_CONFIG_GEO_DETECTION_ENABLED; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.os.Bundle; -import android.os.RemoteCallback; -import android.platform.test.annotations.Presubmit; -import android.service.timezone.TimeZoneProviderSuggestion; -import android.util.IndentingPrintWriter; - -import com.android.server.location.timezone.LocationTimeZoneProvider.ProviderListener; -import com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState; -import com.android.server.timezonedetector.ConfigurationInternal; -import com.android.server.timezonedetector.TestState; - -import org.junit.Before; -import org.junit.Test; - -import java.time.Duration; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; - -/** - * Tests for {@link LocationTimeZoneProvider}. - */ -@Presubmit -public class LocationTimeZoneProviderTest { - - private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 123456789L; - - private TestThreadingDomain mTestThreadingDomain; - - private TestProviderListener mProviderListener; - - @Before - public void setUp() { - mTestThreadingDomain = new TestThreadingDomain(); - mProviderListener = new TestProviderListener(); - } - - @Test - public void lifecycle() { - String providerName = "arbitrary"; - TestLocationTimeZoneProvider provider = - new TestLocationTimeZoneProvider(mTestThreadingDomain, providerName); - - // initialize() - provider.initialize(mProviderListener); - provider.assertOnInitializeCalled(); - - ProviderState currentState = provider.getCurrentState(); - assertEquals(PROVIDER_STATE_STOPPED, currentState.stateEnum); - assertNull(currentState.currentUserConfiguration); - assertSame(provider, currentState.provider); - mTestThreadingDomain.assertQueueEmpty(); - - // startUpdates() - ConfigurationInternal config = USER1_CONFIG_GEO_DETECTION_ENABLED; - Duration arbitraryInitializationTimeout = Duration.ofMinutes(5); - Duration arbitraryInitializationTimeoutFuzz = Duration.ofMinutes(2); - provider.startUpdates(config, arbitraryInitializationTimeout, - arbitraryInitializationTimeoutFuzz); - - provider.assertOnStartCalled(arbitraryInitializationTimeout); - - currentState = provider.getCurrentState(); - assertSame(provider, currentState.provider); - assertEquals(PROVIDER_STATE_STARTED_INITIALIZING, currentState.stateEnum); - assertEquals(config, currentState.currentUserConfiguration); - assertNull(currentState.event); - // The initialization timeout should be queued. - Duration expectedInitializationTimeout = - arbitraryInitializationTimeout.plus(arbitraryInitializationTimeoutFuzz); - mTestThreadingDomain.assertSingleDelayedQueueItem(expectedInitializationTimeout); - // We don't intend to trigger the timeout, so clear it. - mTestThreadingDomain.removeAllQueuedRunnables(); - - // Entering started does not trigger an onProviderStateChanged() as it is requested by the - // controller. - mProviderListener.assertProviderChangeNotReported(); - - // Simulate a suggestion event being received. - TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder() - .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS) - .setTimeZoneIds(Arrays.asList("Europe/London")) - .build(); - TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(suggestion); - provider.simulateProviderEventReceived(event); - - currentState = provider.getCurrentState(); - assertSame(provider, currentState.provider); - assertEquals(PROVIDER_STATE_STARTED_CERTAIN, currentState.stateEnum); - assertEquals(event, currentState.event); - assertEquals(config, currentState.currentUserConfiguration); - mTestThreadingDomain.assertQueueEmpty(); - mProviderListener.assertProviderChangeReported(PROVIDER_STATE_STARTED_CERTAIN); - - // Simulate an uncertain event being received. - event = TimeZoneProviderEvent.createUncertainEvent(); - provider.simulateProviderEventReceived(event); - - currentState = provider.getCurrentState(); - assertSame(provider, currentState.provider); - assertEquals(PROVIDER_STATE_STARTED_UNCERTAIN, currentState.stateEnum); - assertEquals(event, currentState.event); - assertEquals(config, currentState.currentUserConfiguration); - mTestThreadingDomain.assertQueueEmpty(); - mProviderListener.assertProviderChangeReported(PROVIDER_STATE_STARTED_UNCERTAIN); - - // stopUpdates() - provider.stopUpdates(); - provider.assertOnStopUpdatesCalled(); - - currentState = provider.getCurrentState(); - assertSame(provider, currentState.provider); - assertEquals(PROVIDER_STATE_STOPPED, currentState.stateEnum); - assertNull(currentState.event); - assertNull(currentState.currentUserConfiguration); - mTestThreadingDomain.assertQueueEmpty(); - // Entering stopped does not trigger an onProviderStateChanged() as it is requested by the - // controller. - mProviderListener.assertProviderChangeNotReported(); - - // destroy() - provider.destroy(); - provider.assertOnDestroyCalled(); - } - - @Test - public void defaultHandleTestCommandImpl() { - String providerName = "primary"; - TestLocationTimeZoneProvider provider = - new TestLocationTimeZoneProvider(mTestThreadingDomain, providerName); - - TestCommand testCommand = TestCommand.createForTests("test", new Bundle()); - AtomicReference resultReference = new AtomicReference<>(); - RemoteCallback callback = new RemoteCallback(resultReference::set); - provider.handleTestCommand(testCommand, callback); - - Bundle result = resultReference.get(); - assertNotNull(result); - assertFalse(result.getBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY)); - assertNotNull(result.getString(TEST_COMMAND_RESULT_ERROR_KEY)); - } - - @Test - public void stateRecording() { - String providerName = "primary"; - TestLocationTimeZoneProvider provider = - new TestLocationTimeZoneProvider(mTestThreadingDomain, providerName); - provider.setStateChangeRecordingEnabled(true); - - // initialize() - provider.initialize(mProviderListener); - provider.assertLatestRecordedState(PROVIDER_STATE_STOPPED); - - // startUpdates() - ConfigurationInternal config = USER1_CONFIG_GEO_DETECTION_ENABLED; - Duration arbitraryInitializationTimeout = Duration.ofMinutes(5); - Duration arbitraryInitializationTimeoutFuzz = Duration.ofMinutes(2); - provider.startUpdates(config, arbitraryInitializationTimeout, - arbitraryInitializationTimeoutFuzz); - provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_INITIALIZING); - - // Simulate a suggestion event being received. - TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder() - .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS) - .setTimeZoneIds(Arrays.asList("Europe/London")) - .build(); - TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(suggestion); - provider.simulateProviderEventReceived(event); - provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_CERTAIN); - - // Simulate an uncertain event being received. - event = TimeZoneProviderEvent.createUncertainEvent(); - provider.simulateProviderEventReceived(event); - provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_UNCERTAIN); - - // stopUpdates() - provider.stopUpdates(); - provider.assertLatestRecordedState(PROVIDER_STATE_STOPPED); - - // destroy() - provider.destroy(); - provider.assertLatestRecordedState(PROVIDER_STATE_DESTROYED); - } - - /** A test stand-in for the real {@link LocationTimeZoneProviderController}'s listener. */ - private static class TestProviderListener implements ProviderListener { - - private final TestState mReportedProviderStateChanges = new TestState<>(); - - @Override - public void onProviderStateChange(ProviderState providerState) { - mReportedProviderStateChanges.set(providerState); - } - - void assertProviderChangeReported(int expectedStateEnum) { - mReportedProviderStateChanges.assertChangeCount(1); - - ProviderState latest = mReportedProviderStateChanges.getLatest(); - assertEquals(expectedStateEnum, latest.stateEnum); - mReportedProviderStateChanges.commitLatest(); - } - - public void assertProviderChangeNotReported() { - mReportedProviderStateChanges.assertHasNotBeenSet(); - } - } - - private static class TestLocationTimeZoneProvider extends LocationTimeZoneProvider { - - private boolean mOnInitializeCalled; - private boolean mOnDestroyCalled; - private boolean mOnStartUpdatesCalled; - private Duration mInitializationTimeout; - private boolean mOnStopUpdatesCalled; - - /** Creates the instance. */ - TestLocationTimeZoneProvider(@NonNull ThreadingDomain threadingDomain, - @NonNull String providerName) { - super(threadingDomain, providerName); - } - - @Override - void onInitialize() { - mOnInitializeCalled = true; - } - - @Override - void onDestroy() { - mOnDestroyCalled = true; - } - - @Override - void onStartUpdates(@NonNull Duration initializationTimeout) { - mOnStartUpdatesCalled = true; - mInitializationTimeout = initializationTimeout; - } - - @Override - void onStopUpdates() { - mOnStopUpdatesCalled = true; - } - - @Override - public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) { - // No-op for tests - } - - void assertOnInitializeCalled() { - assertTrue(mOnInitializeCalled); - } - - void assertOnStartCalled(Duration expectedInitializationTimeout) { - assertTrue(mOnStartUpdatesCalled); - assertEquals(expectedInitializationTimeout, mInitializationTimeout); - } - - void simulateProviderEventReceived(TimeZoneProviderEvent event) { - handleTimeZoneProviderEvent(event); - } - - void assertOnStopUpdatesCalled() { - assertTrue(mOnStopUpdatesCalled); - } - - void assertOnDestroyCalled() { - assertTrue(mOnDestroyCalled); - } - - void assertLatestRecordedState(@ProviderState.ProviderStateEnum int expectedStateEnum) { - List recordedStates = getRecordedStates(); - assertEquals(expectedStateEnum, - recordedStates.get(recordedStates.size() - 1).stateEnum); - } - } -} diff --git a/services/tests/servicestests/src/com/android/server/location/timezone/TestSupport.java b/services/tests/servicestests/src/com/android/server/location/timezone/TestSupport.java deleted file mode 100644 index 48105634c69d..000000000000 --- a/services/tests/servicestests/src/com/android/server/location/timezone/TestSupport.java +++ /dev/null @@ -1,54 +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.location.timezone; - -import android.annotation.UserIdInt; - -import com.android.server.timezonedetector.ConfigurationInternal; - -/** Shared test support code for this package. */ -final class TestSupport { - static final @UserIdInt int USER1_ID = 9999; - - static final ConfigurationInternal USER1_CONFIG_GEO_DETECTION_ENABLED = - createUserConfig(USER1_ID, true); - - static final ConfigurationInternal USER1_CONFIG_GEO_DETECTION_DISABLED = - createUserConfig(USER1_ID, false); - - static final @UserIdInt int USER2_ID = 1234567890; - - static final ConfigurationInternal USER2_CONFIG_GEO_DETECTION_ENABLED = - createUserConfig(USER2_ID, true); - - static final ConfigurationInternal USER2_CONFIG_GEO_DETECTION_DISABLED = - createUserConfig(USER2_ID, false); - - private TestSupport() { - } - - private static ConfigurationInternal createUserConfig( - @UserIdInt int userId, boolean geoDetectionEnabled) { - return new ConfigurationInternal.Builder(userId) - .setUserConfigAllowed(true) - .setAutoDetectionSupported(true) - .setGeoDetectionSupported(true) - .setAutoDetectionEnabled(true) - .setLocationEnabled(true) - .setGeoDetectionEnabled(geoDetectionEnabled) - .build(); - } -} diff --git a/services/tests/servicestests/src/com/android/server/location/timezone/TestThreadingDomain.java b/services/tests/servicestests/src/com/android/server/location/timezone/TestThreadingDomain.java deleted file mode 100644 index b1a5ff9b549c..000000000000 --- a/services/tests/servicestests/src/com/android/server/location/timezone/TestThreadingDomain.java +++ /dev/null @@ -1,145 +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.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.ArrayList; -import java.util.Comparator; -import java.util.Objects; -import java.util.concurrent.Callable; - -/** - * A ThreadingDomain that simulates idealized post() semantics. Execution takes place in zero time, - * exactly when scheduled, when the test code explicitly requests it. Execution takes place on the - * test's main thread. - */ -class TestThreadingDomain extends ThreadingDomain { - - static class QueuedRunnable { - - static final Comparator COMPARATOR = - (o1, o2) -> (int) (o1.executionTimeMillis - o2.executionTimeMillis); - - @NonNull public final Runnable runnable; - @Nullable public final Object token; - public final long executionTimeMillis; - - QueuedRunnable(@NonNull Runnable runnable, @Nullable Object token, - long executionTimeMillis) { - this.runnable = Objects.requireNonNull(runnable); - this.token = token; - this.executionTimeMillis = executionTimeMillis; - } - - @Override - public String toString() { - return "QueuedRunnable{" - + "runnable=" + runnable - + ", token=" + token - + ", executionTimeMillis=" + executionTimeMillis - + '}'; - } - } - - private long mCurrentTimeMillis; - private ArrayList mQueue = new ArrayList<>(); - - TestThreadingDomain() { - // Pick an arbitrary time. - mCurrentTimeMillis = 123456L; - } - - @Override - Thread getThread() { - return Thread.currentThread(); - } - - @Override - void post(Runnable r) { - postDelayed(r, null, 0); - } - - @Override - V postAndWait(Callable callable, long durationMillis) { - throw new UnsupportedOperationException("Not implemented"); - } - - @Override - void postDelayed(Runnable r, long 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 == token); - } - - void removeAllQueuedRunnables() { - mQueue.clear(); - } - - void assertSingleDelayedQueueItem(Duration expectedDelay) { - assertQueueLength(1); - assertNextQueueItemIsDelayed(expectedDelay); - } - - void assertSingleImmediateQueueItem() { - assertQueueLength(1); - assertNextQueueItemIsImmediate(); - } - - void assertQueueLength(int expectedLength) { - assertEquals(expectedLength, mQueue.size()); - } - - void assertNextQueueItemIsImmediate() { - assertTrue(getNextQueueItemDelayMillis() == 0); - } - - private void assertNextQueueItemIsDelayed(Duration expectedDelay) { - assertEquals(getNextQueueItemDelayMillis(), expectedDelay.toMillis()); - } - - void assertQueueEmpty() { - assertTrue(mQueue.isEmpty()); - } - - long getNextQueueItemDelayMillis() { - assertFalse(mQueue.isEmpty()); - return mQueue.get(0).executionTimeMillis - mCurrentTimeMillis; - } - - void executeNext() { - assertFalse(mQueue.isEmpty()); - QueuedRunnable queued = mQueue.remove(0); - - mCurrentTimeMillis = queued.executionTimeMillis; - queued.runnable.run(); - } -} diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/ControllerImplTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/ControllerImplTest.java new file mode 100644 index 000000000000..4284240c72b4 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/ControllerImplTest.java @@ -0,0 +1,1314 @@ +/* + * 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 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 static com.android.server.timezonedetector.location.TestSupport.USER1_CONFIG_GEO_DETECTION_DISABLED; +import static com.android.server.timezonedetector.location.TestSupport.USER1_CONFIG_GEO_DETECTION_ENABLED; +import static com.android.server.timezonedetector.location.TestSupport.USER2_CONFIG_GEO_DETECTION_ENABLED; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import static java.util.Arrays.asList; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.platform.test.annotations.Presubmit; +import android.service.timezone.TimeZoneProviderSuggestion; +import android.util.IndentingPrintWriter; + +import com.android.server.timezonedetector.ConfigurationInternal; +import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion; +import com.android.server.timezonedetector.TestState; +import com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.ProviderStateEnum; + +import org.junit.Before; +import org.junit.Test; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** Tests for {@link ControllerImpl}. */ +@Presubmit +public class ControllerImplTest { + + private static final long ARBITRARY_TIME_MILLIS = 12345L; + + private static final TimeZoneProviderEvent USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1 = + createSuggestionEvent(asList("Europe/London")); + private static final TimeZoneProviderEvent USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2 = + createSuggestionEvent(asList("Europe/Paris")); + private static final TimeZoneProviderEvent USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT = + TimeZoneProviderEvent.createUncertainEvent(); + private static final TimeZoneProviderEvent USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT = + TimeZoneProviderEvent.createPermanentFailureEvent("Test"); + + private TestThreadingDomain mTestThreadingDomain; + private TestCallback mTestCallback; + private TestLocationTimeZoneProvider mTestPrimaryLocationTimeZoneProvider; + private TestLocationTimeZoneProvider mTestSecondaryLocationTimeZoneProvider; + + @Before + public void setUp() { + // For simplicity, the TestThreadingDomain uses the test's main thread. To execute posted + // runnables, the test must call methods on mTestThreadingDomain otherwise those runnables + // will never get a chance to execute. + mTestThreadingDomain = new TestThreadingDomain(); + mTestCallback = new TestCallback(mTestThreadingDomain); + mTestPrimaryLocationTimeZoneProvider = + new TestLocationTimeZoneProvider(mTestThreadingDomain, "primary"); + mTestSecondaryLocationTimeZoneProvider = + new TestLocationTimeZoneProvider(mTestThreadingDomain, "secondary"); + } + + @Test + public void initializationFailure_primary() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + Duration expectedInitTimeout = testEnvironment.getProviderInitializationTimeout() + .plus(testEnvironment.getProviderInitializationTimeoutFuzz()); + + mTestPrimaryLocationTimeZoneProvider.setFailDuringInitialization(true); + + // Initialize. After initialization the providers must be initialized and one should be + // started. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertInitialized(); + mTestSecondaryLocationTimeZoneProvider.assertInitialized(); + + mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertInitializationTimeoutSet(expectedInitTimeout); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void initializationFailure_secondary() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + Duration expectedInitTimeout = testEnvironment.getProviderInitializationTimeout() + .plus(testEnvironment.getProviderInitializationTimeoutFuzz()); + + mTestSecondaryLocationTimeZoneProvider.setFailDuringInitialization(true); + + // Initialize. After initialization the providers must be initialized and one should be + // started. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertInitialized(); + mTestSecondaryLocationTimeZoneProvider.assertInitialized(); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestPrimaryLocationTimeZoneProvider.assertInitializationTimeoutSet(expectedInitTimeout); + mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void initializationFailure_both() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + mTestPrimaryLocationTimeZoneProvider.setFailDuringInitialization(true); + mTestSecondaryLocationTimeZoneProvider.setFailDuringInitialization(true); + + // Initialize. After initialization the providers must be initialized and one should be + // started. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertInitialized(); + mTestSecondaryLocationTimeZoneProvider.assertInitialized(); + + mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestCallback.assertUncertainSuggestionMadeAndCommit(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void initialState_started() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + Duration expectedInitTimeout = testEnvironment.getProviderInitializationTimeout() + .plus(testEnvironment.getProviderInitializationTimeoutFuzz()); + + // Initialize. After initialization the providers must be initialized and one should be + // started. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertInitialized(); + mTestSecondaryLocationTimeZoneProvider.assertInitialized(); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestPrimaryLocationTimeZoneProvider.assertInitializationTimeoutSet(expectedInitTimeout); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void initialState_disabled() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_DISABLED); + + // Initialize. After initialization the providers must be initialized but neither should be + // started. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertInitialized(); + mTestSecondaryLocationTimeZoneProvider.assertInitialized(); + + mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void enabled_uncertaintySuggestionSentIfNoEventReceived() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate time passing with no provider event being received from the primary. + mTestThreadingDomain.executeNext(); + + // The primary should have reported uncertainty, which should trigger the controller to + // start the uncertainty timeout and start the secondary. + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); + + // Simulate time passing with no provider event being received from either the primary or + // secondary. + mTestThreadingDomain.executeNext(); + + // Now both initialization timeouts should have triggered. The uncertainty timeout should + // still not be triggered. + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_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_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertUncertainSuggestionMadeAndCommit(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void enabled_eventReceivedBeforeInitializationTimeout() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate a location event being received from the primary provider. This should cause a + // suggestion to be made. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertSuggestionMadeAndCommit( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void enabled_eventReceivedFromPrimaryAfterInitializationTimeout() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate time passing with no provider event being received from the primary. + mTestThreadingDomain.executeNext(); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, 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 and the secondary to be shut down. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertSuggestionMadeAndCommit( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void enabled_eventReceivedFromSecondaryAfterInitializationTimeout() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate time passing with no provider event being received from the primary. + mTestThreadingDomain.executeNext(); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); + + // Simulate a location event being received from the secondary provider. This should cause a + // suggestion to be made. + mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertSuggestionMadeAndCommit( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void enabled_repeatedPrimaryCertainty() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate a location event being received from the primary provider. This should cause a + // suggestion to be made. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertSuggestionMadeAndCommit( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // A second, identical event should not cause another suggestion. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // And a third, different event should cause another suggestion. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertSuggestionMadeAndCommit( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2.getSuggestion().getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void enabled_repeatedSecondaryCertainty() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate time passing with no provider event being received from the primary. + mTestThreadingDomain.executeNext(); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); + + // Simulate a location event being received from the secondary provider. This should cause a + // suggestion to be made. + mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertSuggestionMadeAndCommit( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // A second, identical event should not cause another suggestion. + mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // And a third, different event should cause another suggestion. + mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertSuggestionMadeAndCommit( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2.getSuggestion().getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void enabled_uncertaintyTriggersASuggestionAfterUncertaintyTimeout() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + 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.simulateTimeZoneProviderEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertSuggestionMadeAndCommit( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().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 and the secondary should be started. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); + + // Simulate a location event being received from the secondary provider. This should cause a + // suggestion to be made, cancel the uncertainty timeout and ensure the secondary is + // considered initialized. + mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertSuggestionMadeAndCommit( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2.getSuggestion().getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate an uncertain event being received from the secondary provider. This should not + // cause a suggestion to be made straight away, but the uncertainty timeout should be + // started. Both providers are now started, with no initialization timeout set. + mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_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_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertUncertainSuggestionMadeAndCommit(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void enabled_briefUncertaintyTriggersNoSuggestion() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate a location event being received from the primary provider. This should cause a + // suggestion to be made. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertSuggestionMadeAndCommit( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Uncertainty should not cause a suggestion to be made straight away, but the uncertainty + // timeout should be started and the secondary should be started. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); + + // And a success event from the primary provider should cause the controller to make another + // suggestion, the uncertainty timeout should be cancelled and the secondary should be + // stopped again. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertSuggestionMadeAndCommit( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2.getSuggestion().getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void configChanges_enableAndDisableWithNoPreviousSuggestion() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_DISABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Now signal a config change so that geo detection is enabled. + testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_ENABLED); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Now signal a config change so that geo detection is disabled. + testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_DISABLED); + + mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void configChanges_enableAndDisableWithPreviousSuggestion() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_DISABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Now signal a config change so that geo detection is enabled. + testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_ENABLED); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate a success event being received from the primary provider. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertSuggestionMadeAndCommit( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // 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. + testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_DISABLED); + + mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertUncertainSuggestionMadeAndCommit(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void configChanges_userSwitch_enabledToEnabled() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate the primary provider suggesting a time zone. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + 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. + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertSuggestionMadeAndCommit( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().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_STARTED_INITIALIZING, but it should + // have been stopped when the user changed. + int[] expectedStateTransitions = + { PROVIDER_STATE_STOPPED, PROVIDER_STATE_STARTED_INITIALIZING }; + mTestPrimaryLocationTimeZoneProvider.assertStateChangesAndCommit(expectedStateTransitions); + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfig( + PROVIDER_STATE_STARTED_INITIALIZING, USER2_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void primaryPermFailure_secondaryEventsReceived() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate a failure location event being received from the primary provider. This should + // cause the secondary to be started. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT); + + mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate uncertainty from the secondary. + mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); + + mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); + + // And a success event from the secondary provider should cause the controller to make + // another suggestion, the uncertainty timeout should be cancelled. + mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); + + mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertSuggestionMadeAndCommit( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2.getSuggestion().getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate uncertainty from the secondary. + mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); + + mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); + } + + @Test + public void primaryPermFailure_disableAndEnable() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate a failure location event being received from the primary provider. This should + // cause the secondary to be started. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT); + + mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_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); + + mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + 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(); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void secondaryPermFailure_primaryEventsReceived() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate an uncertain event from the primary. This will start the secondary, which will + // give this test the opportunity to simulate its failure. Then it will be possible to + // demonstrate controller behavior with only the primary working. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); + + // Simulate failure event from the secondary. This should just affect the secondary's state. + mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); + + // And a success event from the primary provider should cause the controller to make + // a suggestion, the uncertainty timeout should be cancelled. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestCallback.assertSuggestionMadeAndCommit( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2.getSuggestion().getTimeZoneIds()); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate uncertainty from the primary. The secondary cannot be started. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); + } + + @Test + public void secondaryPermFailure_disableAndEnable() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate an uncertain event from the primary. This will start the secondary, which will + // give this test the opportunity to simulate its failure. Then it will be possible to + // demonstrate controller behavior with only the primary working. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); + + // Simulate failure event from the secondary. This should just affect the secondary's state. + mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertUncertaintyTimeoutSet(testEnvironment, controllerImpl); + + // Now signal a config change so that geo detection is disabled. + testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_DISABLED); + + mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Now signal a config change so that geo detection is enabled. Only the primary can be + // started. + testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_ENABLED); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void bothPermFailure_disableAndEnable() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate a failure event from the primary. This will start the secondary. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT); + + mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( + PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); + mTestCallback.assertNoSuggestionMade(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + + // Simulate failure event from the secondary. + mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT); + + mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); + mTestCallback.assertUncertainSuggestionMadeAndCommit(); + assertFalse(controllerImpl.isUncertaintyTimeoutSet()); + } + + @Test + public void stateRecording() { + ControllerImpl controllerImpl = new ControllerImpl(mTestThreadingDomain, + mTestPrimaryLocationTimeZoneProvider, mTestSecondaryLocationTimeZoneProvider); + TestEnvironment testEnvironment = new TestEnvironment( + mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED); + + // Initialize and check initial state. + controllerImpl.initialize(testEnvironment, mTestCallback); + + { + LocationTimeZoneManagerServiceState state = controllerImpl.getStateForTests(); + assertNull(state.getLastSuggestion()); + assertTrue(state.getPrimaryProviderStates().isEmpty()); + assertTrue(state.getSecondaryProviderStates().isEmpty()); + } + + // State recording and simulate some provider behavior that will show up in the state + // recording. + controllerImpl.setProviderStateRecordingEnabled(true); + + // Simulate an uncertain event from the primary. This will start the secondary. + mTestPrimaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); + + { + LocationTimeZoneManagerServiceState state = controllerImpl.getStateForTests(); + assertNull(state.getLastSuggestion()); + List primaryProviderStates = + state.getPrimaryProviderStates(); + assertEquals(1, primaryProviderStates.size()); + assertEquals(PROVIDER_STATE_STARTED_UNCERTAIN, + primaryProviderStates.get(0).stateEnum); + List secondaryProviderStates = + state.getSecondaryProviderStates(); + assertEquals(1, secondaryProviderStates.size()); + assertEquals(PROVIDER_STATE_STARTED_INITIALIZING, + secondaryProviderStates.get(0).stateEnum); + } + + // Simulate an uncertain event from the primary. This will start the secondary. + mTestSecondaryLocationTimeZoneProvider.simulateTimeZoneProviderEvent( + USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); + + { + LocationTimeZoneManagerServiceState state = controllerImpl.getStateForTests(); + assertEquals(USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds(), + state.getLastSuggestion().getZoneIds()); + List primaryProviderStates = + state.getPrimaryProviderStates(); + assertEquals(1, primaryProviderStates.size()); + assertEquals(PROVIDER_STATE_STARTED_UNCERTAIN, primaryProviderStates.get(0).stateEnum); + List secondaryProviderStates = + state.getSecondaryProviderStates(); + assertEquals(2, secondaryProviderStates.size()); + assertEquals(PROVIDER_STATE_STARTED_CERTAIN, secondaryProviderStates.get(1).stateEnum); + } + + controllerImpl.setProviderStateRecordingEnabled(false); + { + LocationTimeZoneManagerServiceState state = controllerImpl.getStateForTests(); + assertEquals(USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds(), + state.getLastSuggestion().getZoneIds()); + assertTrue(state.getPrimaryProviderStates().isEmpty()); + assertTrue(state.getSecondaryProviderStates().isEmpty()); + } + } + + private static void assertUncertaintyTimeoutSet( + LocationTimeZoneProviderController.Environment environment, + LocationTimeZoneProviderController controller) { + assertTrue(controller.isUncertaintyTimeoutSet()); + assertEquals(environment.getUncertaintyDelay().toMillis(), + controller.getUncertaintyTimeoutDelayMillis()); + } + + private static TimeZoneProviderEvent createSuggestionEvent(@NonNull List timeZoneIds) { + return TimeZoneProviderEvent.createSuggestionEvent( + new TimeZoneProviderSuggestion.Builder() + .setElapsedRealtimeMillis(ARBITRARY_TIME_MILLIS) + .setTimeZoneIds(timeZoneIds) + .build()); + } + + 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(15); + + private final LocationTimeZoneProviderController mController; + private ConfigurationInternal mConfigurationInternal; + + TestEnvironment(ThreadingDomain threadingDomain, + LocationTimeZoneProviderController controller, + ConfigurationInternal configurationInternal) { + super(threadingDomain); + mController = Objects.requireNonNull(controller); + mConfigurationInternal = Objects.requireNonNull(configurationInternal); + } + + @Override + void destroy() { + // No-op test impl. + } + + @Override + ConfigurationInternal getCurrentUserConfigurationInternal() { + return mConfigurationInternal; + } + + @Override + Duration getProviderInitializationTimeout() { + return PROVIDER_INITIALIZATION_TIMEOUT; + } + + @Override + Duration getProviderInitializationTimeoutFuzz() { + return PROVIDER_INITIALIZATION_TIMEOUT_FUZZ; + } + + @Override + Duration getUncertaintyDelay() { + return UNCERTAINTY_DELAY; + } + + void simulateConfigChange(ConfigurationInternal newConfig) { + ConfigurationInternal oldConfig = mConfigurationInternal; + mConfigurationInternal = Objects.requireNonNull(newConfig); + if (Objects.equals(oldConfig, newConfig)) { + fail("Bad test? No config change when one was expected"); + } + mController.onConfigChanged(); + } + } + + private static class TestCallback extends LocationTimeZoneProviderController.Callback { + + private TestState mLatestSuggestion = new TestState<>(); + + TestCallback(ThreadingDomain threadingDomain) { + super(threadingDomain); + } + + @Override + void suggest(GeolocationTimeZoneSuggestion suggestion) { + mLatestSuggestion.set(suggestion); + } + + void assertSuggestionMadeAndCommit(@Nullable List expectedZoneIds) { + mLatestSuggestion.assertHasBeenSet(); + assertEquals(expectedZoneIds, mLatestSuggestion.getLatest().getZoneIds()); + mLatestSuggestion.commitLatest(); + } + + void assertNoSuggestionMade() { + mLatestSuggestion.assertHasNotBeenSet(); + } + + void assertUncertainSuggestionMadeAndCommit() { + // An "uncertain" suggestion has null time zone IDs. + assertSuggestionMadeAndCommit(null); + } + } + + private static class TestLocationTimeZoneProvider extends LocationTimeZoneProvider { + + /** Used to track historic provider states for tests. */ + private final TestState mTestProviderState = new TestState<>(); + private boolean mFailDuringInitialization; + private boolean mInitialized; + private boolean mDestroyed; + + /** + * Creates the instance. + */ + TestLocationTimeZoneProvider(ThreadingDomain threadingDomain, String providerName) { + super(threadingDomain, providerName); + } + + public void setFailDuringInitialization(boolean failInitialization) { + mFailDuringInitialization = failInitialization; + } + + @Override + void onInitialize() { + mInitialized = true; + if (mFailDuringInitialization) { + throw new RuntimeException("Simulated initialization failure"); + } + } + + @Override + void onDestroy() { + mDestroyed = true; + } + + @Override + void onSetCurrentState(ProviderState newState) { + mTestProviderState.set(newState); + } + + @Override + void onStartUpdates(Duration initializationTimeout) { + // Nothing needed for tests. + } + + @Override + void onStopUpdates() { + // Nothing needed for tests. + } + + @Override + public void dump(IndentingPrintWriter pw, String[] args) { + // Nothing needed for tests. + } + + /** Asserts that {@link #initialize(ProviderListener)} has been called. */ + void assertInitialized() { + assertTrue(mInitialized); + } + + public void assertIsPermFailedAndCommit() { + // A failed provider doesn't hold config. + assertStateEnumAndConfig(PROVIDER_STATE_PERM_FAILED, null /* config */); + mTestProviderState.commitLatest(); + } + + void assertIsStoppedAndCommit() { + // A stopped provider doesn't hold config. + assertStateEnumAndConfig(PROVIDER_STATE_STOPPED, 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_STARTED_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 simulateTimeZoneProviderEvent(@NonNull TimeZoneProviderEvent event) { + handleTimeZoneProviderEvent(event); + } + + /** + * Asserts the most recent state changes. The ordering is such that the last element in the + * provided array is expected to be the current state. + */ + void assertStateChangesAndCommit(int... expectedProviderStates) { + if (expectedProviderStates.length == 0) { + mTestProviderState.assertHasNotBeenSet(); + } else { + mTestProviderState.assertChangeCount(expectedProviderStates.length); + + List previousProviderStates = new ArrayList<>(); + for (int i = 0; i < expectedProviderStates.length; i++) { + previousProviderStates.add(mTestProviderState.getPrevious(i)); + } + // The loop above will produce a list with the most recent state in element 0. So, + // reverse the list as the arguments to this method are expected to be in order + // oldest...latest. + Collections.reverse(previousProviderStates); + + boolean allMatch = true; + for (int i = 0; i < expectedProviderStates.length; i++) { + allMatch = allMatch && expectedProviderStates[i] + == previousProviderStates.get(i).stateEnum; + } + if (!allMatch) { + fail("Provider state enums expected=" + Arrays.toString(expectedProviderStates) + + " but states were" + + " actually=" + previousProviderStates); + } + } + mTestProviderState.commitLatest(); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/HandlerThreadingDomainTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/HandlerThreadingDomainTest.java new file mode 100644 index 000000000000..e7dd97949bb0 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/HandlerThreadingDomainTest.java @@ -0,0 +1,260 @@ +/* + * 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 org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.os.Handler; +import android.os.HandlerThread; +import android.platform.test.annotations.Presubmit; + +import com.android.server.timezonedetector.location.ThreadingDomain.SingleRunnableQueue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +/** Tests for {@link HandlerThreadingDomain}. */ +@Presubmit +public class HandlerThreadingDomainTest { + + private HandlerThread mHandlerThread; + private Handler mTestHandler; + + @Before + public void setUp() { + mHandlerThread = new HandlerThread("HandlerThreadingDomainTest"); + mHandlerThread.start(); + mTestHandler = new Handler(mHandlerThread.getLooper()); + } + + @After + public void tearDown() throws Exception { + mHandlerThread.quit(); + mHandlerThread.join(); + } + + @Test + public void getLockObject() { + ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler); + assertSame("LockObject must be consistent", domain.getLockObject(), domain.getLockObject()); + } + + @Test + public void assertCurrentThread() throws Exception { + ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler); + + // Expect an exception (current thread != handler thread) + try { + domain.assertCurrentThread(); + fail("Expected exception"); + } catch (RuntimeException expected) { + // Expected + } + + // Expect no exception (current thread == handler thread) + AtomicBoolean exceptionThrown = new AtomicBoolean(true); + LatchedRunnable testCode = new LatchedRunnable(() -> { + domain.assertCurrentThread(); + exceptionThrown.set(false); + }); + mTestHandler.post(testCode); + testCode.assertCompletesWithin(60, TimeUnit.SECONDS); + assertFalse(exceptionThrown.get()); + } + + @Test + public void assertNotCurrentThread() throws Exception { + ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler); + + // Expect no exception (current thread != handler thread) + domain.assertNotCurrentThread(); + + AtomicBoolean exceptionThrown = new AtomicBoolean(false); + LatchedRunnable testCode = new LatchedRunnable(() -> { + // Expect an exception (current thread == handler thread) + try { + domain.assertNotCurrentThread(); + fail("Expected exception"); + } catch (RuntimeException expected) { + exceptionThrown.set(true); + } + }); + mTestHandler.post(testCode); + testCode.assertCompletesWithin(60, TimeUnit.SECONDS); + assertTrue(exceptionThrown.get()); + } + + @Test + public void post() throws Exception { + ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler); + AtomicBoolean ranOnExpectedThread = new AtomicBoolean(false); + LatchedRunnable testLogic = new LatchedRunnable(() -> { + ranOnExpectedThread.set(Thread.currentThread() == mTestHandler.getLooper().getThread()); + }); + domain.post(testLogic); + testLogic.assertCompletesWithin(60, TimeUnit.SECONDS); + assertTrue(testLogic.isComplete()); + assertTrue(ranOnExpectedThread.get()); + } + + @Test + public void postDelayed() throws Exception { + ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler); + + long beforeExecutionNanos = System.nanoTime(); + Duration executionDelay = Duration.ofSeconds(5); + + AtomicReference executionNanosHolder = new AtomicReference<>(); + AtomicBoolean ranOnExpectedThread = new AtomicBoolean(false); + LatchedRunnable testLogic = new LatchedRunnable(() -> { + ranOnExpectedThread.set(Thread.currentThread() == mTestHandler.getLooper().getThread()); + executionNanosHolder.set(System.nanoTime()); + }); + + domain.postDelayed(testLogic, executionDelay.toMillis()); + long afterPostNanos = System.nanoTime(); + + testLogic.assertCompletesWithin( + executionDelay.multipliedBy(10).toMillis(), TimeUnit.MILLISECONDS); + long afterWaitNanos = System.nanoTime(); + + assertTrue(testLogic.isComplete()); + assertTrue(ranOnExpectedThread.get()); + + // The execution should not take place until at least delayDuration after postDelayed(). + Duration actualExecutionDelay = + Duration.ofNanos(executionNanosHolder.get() - beforeExecutionNanos); + assertTrue(actualExecutionDelay.compareTo(executionDelay) >= 0); + + // The time taken in postDelayed() should be negligible. Certainly less than the + // executionDelay. + Duration postDuration = Duration.ofNanos(afterPostNanos - beforeExecutionNanos); + assertTrue(postDuration.compareTo(executionDelay) < 0); + + // The result should not be ready until at least executionDelay has elapsed. + Duration delayBeforeExecuted = Duration.ofNanos(afterWaitNanos - beforeExecutionNanos); + assertTrue(delayBeforeExecuted.compareTo(executionDelay) >= 0); + } + + @Test + public void postAndWait() throws Exception { + ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler); + + Duration workDuration = Duration.ofSeconds(5); + AtomicBoolean ranOnExpectedThread = new AtomicBoolean(false); + LatchedRunnable testLogic = new LatchedRunnable(() -> { + ranOnExpectedThread.set(Thread.currentThread() == mTestHandler.getLooper().getThread()); + + // The work takes workDuration to complete. + try { + Thread.sleep(workDuration.toMillis()); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + }); + + long beforeExecutionNanos = System.nanoTime(); + domain.postAndWait(testLogic, workDuration.multipliedBy(10).toMillis()); + long afterExecutionNanos = System.nanoTime(); + Duration waitDuration = Duration.ofNanos(afterExecutionNanos - beforeExecutionNanos); + + assertTrue(waitDuration.compareTo(workDuration) >= 0); + assertTrue(testLogic.isComplete()); + assertTrue(ranOnExpectedThread.get()); + } + + @Test + public void singleRunnableQueue_runDelayed() throws Exception { + ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler); + SingleRunnableQueue singleRunnableQueue = domain.createSingleRunnableQueue(); + + long beforeExecutionNanos = System.nanoTime(); + + Runnable noOpRunnable = () -> { + // Deliberately do nothing + }; + LatchedRunnable firstRunnable = new LatchedRunnable(noOpRunnable); + LatchedRunnable secondRunnable = new LatchedRunnable(noOpRunnable); + + // Calls to SingleRunnableQueue must be made on the handler thread it is associated with, + // so this uses runWithScissors() to block until the runDelayedTestRunnable has completed. + Runnable runDelayedTestRunnable = () -> { + singleRunnableQueue.runDelayed(firstRunnable, TimeUnit.SECONDS.toMillis(10)); + + // The second runnable posted must clear the first. + singleRunnableQueue.runDelayed(secondRunnable, TimeUnit.SECONDS.toMillis(10)); + }; + mTestHandler.runWithScissors(runDelayedTestRunnable, TimeUnit.SECONDS.toMillis(60)); + + // Now wait for the second runnable to complete + secondRunnable.assertCompletesWithin(60, TimeUnit.SECONDS); + assertFalse(firstRunnable.isComplete()); + + long afterExecutionNanos = System.nanoTime(); + assertTrue(afterExecutionNanos - beforeExecutionNanos >= TimeUnit.SECONDS.toNanos(10)); + } + + private static boolean awaitWithRuntimeException( + CountDownLatch latch, long timeout, TimeUnit timeUnit) { + try { + return latch.await(timeout, timeUnit); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private static class LatchedRunnable implements Runnable { + + private final CountDownLatch mLatch = new CountDownLatch(1); + private final Runnable mRunnable; + + LatchedRunnable(Runnable mRunnable) { + this.mRunnable = Objects.requireNonNull(mRunnable); + } + + @Override + public void run() { + try { + mRunnable.run(); + } finally { + mLatch.countDown(); + } + } + + boolean isComplete() { + return mLatch.getCount() == 0; + } + + boolean waitForCompletion(long timeout, TimeUnit unit) { + return awaitWithRuntimeException(mLatch, timeout, unit); + } + + void assertCompletesWithin(long timeout, TimeUnit unit) { + assertTrue("Runnable did not execute in time", waitForCompletion(timeout, unit)); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java new file mode 100644 index 000000000000..095c868fc74c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java @@ -0,0 +1,311 @@ +/* + * 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.TimeZoneProviderService.TEST_COMMAND_RESULT_ERROR_KEY; +import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_SUCCESS_KEY; + +import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED; +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 static com.android.server.timezonedetector.location.TestSupport.USER1_CONFIG_GEO_DETECTION_ENABLED; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; +import android.os.RemoteCallback; +import android.platform.test.annotations.Presubmit; +import android.service.timezone.TimeZoneProviderSuggestion; +import android.util.IndentingPrintWriter; + +import com.android.server.timezonedetector.ConfigurationInternal; +import com.android.server.timezonedetector.TestState; +import com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderListener; +import com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState; + +import org.junit.Before; +import org.junit.Test; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Tests for {@link LocationTimeZoneProvider}. + */ +@Presubmit +public class LocationTimeZoneProviderTest { + + private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 123456789L; + + private TestThreadingDomain mTestThreadingDomain; + + private TestProviderListener mProviderListener; + + @Before + public void setUp() { + mTestThreadingDomain = new TestThreadingDomain(); + mProviderListener = new TestProviderListener(); + } + + @Test + public void lifecycle() { + String providerName = "arbitrary"; + TestLocationTimeZoneProvider provider = + new TestLocationTimeZoneProvider(mTestThreadingDomain, providerName); + + // initialize() + provider.initialize(mProviderListener); + provider.assertOnInitializeCalled(); + + ProviderState currentState = provider.getCurrentState(); + assertEquals(PROVIDER_STATE_STOPPED, currentState.stateEnum); + assertNull(currentState.currentUserConfiguration); + assertSame(provider, currentState.provider); + mTestThreadingDomain.assertQueueEmpty(); + + // startUpdates() + ConfigurationInternal config = USER1_CONFIG_GEO_DETECTION_ENABLED; + Duration arbitraryInitializationTimeout = Duration.ofMinutes(5); + Duration arbitraryInitializationTimeoutFuzz = Duration.ofMinutes(2); + provider.startUpdates(config, arbitraryInitializationTimeout, + arbitraryInitializationTimeoutFuzz); + + provider.assertOnStartCalled(arbitraryInitializationTimeout); + + currentState = provider.getCurrentState(); + assertSame(provider, currentState.provider); + assertEquals(PROVIDER_STATE_STARTED_INITIALIZING, currentState.stateEnum); + assertEquals(config, currentState.currentUserConfiguration); + assertNull(currentState.event); + // The initialization timeout should be queued. + Duration expectedInitializationTimeout = + arbitraryInitializationTimeout.plus(arbitraryInitializationTimeoutFuzz); + mTestThreadingDomain.assertSingleDelayedQueueItem(expectedInitializationTimeout); + // We don't intend to trigger the timeout, so clear it. + mTestThreadingDomain.removeAllQueuedRunnables(); + + // Entering started does not trigger an onProviderStateChanged() as it is requested by the + // controller. + mProviderListener.assertProviderChangeNotReported(); + + // Simulate a suggestion event being received. + TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder() + .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS) + .setTimeZoneIds(Arrays.asList("Europe/London")) + .build(); + TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(suggestion); + provider.simulateProviderEventReceived(event); + + currentState = provider.getCurrentState(); + assertSame(provider, currentState.provider); + assertEquals(PROVIDER_STATE_STARTED_CERTAIN, currentState.stateEnum); + assertEquals(event, currentState.event); + assertEquals(config, currentState.currentUserConfiguration); + mTestThreadingDomain.assertQueueEmpty(); + mProviderListener.assertProviderChangeReported(PROVIDER_STATE_STARTED_CERTAIN); + + // Simulate an uncertain event being received. + event = TimeZoneProviderEvent.createUncertainEvent(); + provider.simulateProviderEventReceived(event); + + currentState = provider.getCurrentState(); + assertSame(provider, currentState.provider); + assertEquals(PROVIDER_STATE_STARTED_UNCERTAIN, currentState.stateEnum); + assertEquals(event, currentState.event); + assertEquals(config, currentState.currentUserConfiguration); + mTestThreadingDomain.assertQueueEmpty(); + mProviderListener.assertProviderChangeReported(PROVIDER_STATE_STARTED_UNCERTAIN); + + // stopUpdates() + provider.stopUpdates(); + provider.assertOnStopUpdatesCalled(); + + currentState = provider.getCurrentState(); + assertSame(provider, currentState.provider); + assertEquals(PROVIDER_STATE_STOPPED, currentState.stateEnum); + assertNull(currentState.event); + assertNull(currentState.currentUserConfiguration); + mTestThreadingDomain.assertQueueEmpty(); + // Entering stopped does not trigger an onProviderStateChanged() as it is requested by the + // controller. + mProviderListener.assertProviderChangeNotReported(); + + // destroy() + provider.destroy(); + provider.assertOnDestroyCalled(); + } + + @Test + public void defaultHandleTestCommandImpl() { + String providerName = "primary"; + TestLocationTimeZoneProvider provider = + new TestLocationTimeZoneProvider(mTestThreadingDomain, providerName); + + TestCommand testCommand = TestCommand.createForTests("test", new Bundle()); + AtomicReference resultReference = new AtomicReference<>(); + RemoteCallback callback = new RemoteCallback(resultReference::set); + provider.handleTestCommand(testCommand, callback); + + Bundle result = resultReference.get(); + assertNotNull(result); + assertFalse(result.getBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY)); + assertNotNull(result.getString(TEST_COMMAND_RESULT_ERROR_KEY)); + } + + @Test + public void stateRecording() { + String providerName = "primary"; + TestLocationTimeZoneProvider provider = + new TestLocationTimeZoneProvider(mTestThreadingDomain, providerName); + provider.setStateChangeRecordingEnabled(true); + + // initialize() + provider.initialize(mProviderListener); + provider.assertLatestRecordedState(PROVIDER_STATE_STOPPED); + + // startUpdates() + ConfigurationInternal config = USER1_CONFIG_GEO_DETECTION_ENABLED; + Duration arbitraryInitializationTimeout = Duration.ofMinutes(5); + Duration arbitraryInitializationTimeoutFuzz = Duration.ofMinutes(2); + provider.startUpdates(config, arbitraryInitializationTimeout, + arbitraryInitializationTimeoutFuzz); + provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_INITIALIZING); + + // Simulate a suggestion event being received. + TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder() + .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS) + .setTimeZoneIds(Arrays.asList("Europe/London")) + .build(); + TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(suggestion); + provider.simulateProviderEventReceived(event); + provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_CERTAIN); + + // Simulate an uncertain event being received. + event = TimeZoneProviderEvent.createUncertainEvent(); + provider.simulateProviderEventReceived(event); + provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_UNCERTAIN); + + // stopUpdates() + provider.stopUpdates(); + provider.assertLatestRecordedState(PROVIDER_STATE_STOPPED); + + // destroy() + provider.destroy(); + provider.assertLatestRecordedState(PROVIDER_STATE_DESTROYED); + } + + /** A test stand-in for the real {@link LocationTimeZoneProviderController}'s listener. */ + private static class TestProviderListener implements ProviderListener { + + private final TestState mReportedProviderStateChanges = new TestState<>(); + + @Override + public void onProviderStateChange(ProviderState providerState) { + mReportedProviderStateChanges.set(providerState); + } + + void assertProviderChangeReported(int expectedStateEnum) { + mReportedProviderStateChanges.assertChangeCount(1); + + ProviderState latest = mReportedProviderStateChanges.getLatest(); + assertEquals(expectedStateEnum, latest.stateEnum); + mReportedProviderStateChanges.commitLatest(); + } + + public void assertProviderChangeNotReported() { + mReportedProviderStateChanges.assertHasNotBeenSet(); + } + } + + private static class TestLocationTimeZoneProvider extends LocationTimeZoneProvider { + + private boolean mOnInitializeCalled; + private boolean mOnDestroyCalled; + private boolean mOnStartUpdatesCalled; + private Duration mInitializationTimeout; + private boolean mOnStopUpdatesCalled; + + /** Creates the instance. */ + TestLocationTimeZoneProvider(@NonNull ThreadingDomain threadingDomain, + @NonNull String providerName) { + super(threadingDomain, providerName); + } + + @Override + void onInitialize() { + mOnInitializeCalled = true; + } + + @Override + void onDestroy() { + mOnDestroyCalled = true; + } + + @Override + void onStartUpdates(@NonNull Duration initializationTimeout) { + mOnStartUpdatesCalled = true; + mInitializationTimeout = initializationTimeout; + } + + @Override + void onStopUpdates() { + mOnStopUpdatesCalled = true; + } + + @Override + public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) { + // No-op for tests + } + + void assertOnInitializeCalled() { + assertTrue(mOnInitializeCalled); + } + + void assertOnStartCalled(Duration expectedInitializationTimeout) { + assertTrue(mOnStartUpdatesCalled); + assertEquals(expectedInitializationTimeout, mInitializationTimeout); + } + + void simulateProviderEventReceived(TimeZoneProviderEvent event) { + handleTimeZoneProviderEvent(event); + } + + void assertOnStopUpdatesCalled() { + assertTrue(mOnStopUpdatesCalled); + } + + void assertOnDestroyCalled() { + assertTrue(mOnDestroyCalled); + } + + void assertLatestRecordedState(@ProviderState.ProviderStateEnum int expectedStateEnum) { + List recordedStates = getRecordedStates(); + assertEquals(expectedStateEnum, + recordedStates.get(recordedStates.size() - 1).stateEnum); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/TestSupport.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/TestSupport.java new file mode 100644 index 000000000000..d319488ba73b --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/TestSupport.java @@ -0,0 +1,54 @@ +/* + * 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 android.annotation.UserIdInt; + +import com.android.server.timezonedetector.ConfigurationInternal; + +/** Shared test support code for this package. */ +final class TestSupport { + static final @UserIdInt int USER1_ID = 9999; + + static final ConfigurationInternal USER1_CONFIG_GEO_DETECTION_ENABLED = + createUserConfig(USER1_ID, true); + + static final ConfigurationInternal USER1_CONFIG_GEO_DETECTION_DISABLED = + createUserConfig(USER1_ID, false); + + static final @UserIdInt int USER2_ID = 1234567890; + + static final ConfigurationInternal USER2_CONFIG_GEO_DETECTION_ENABLED = + createUserConfig(USER2_ID, true); + + static final ConfigurationInternal USER2_CONFIG_GEO_DETECTION_DISABLED = + createUserConfig(USER2_ID, false); + + private TestSupport() { + } + + private static ConfigurationInternal createUserConfig( + @UserIdInt int userId, boolean geoDetectionEnabled) { + return new ConfigurationInternal.Builder(userId) + .setUserConfigAllowed(true) + .setAutoDetectionSupported(true) + .setGeoDetectionSupported(true) + .setAutoDetectionEnabled(true) + .setLocationEnabled(true) + .setGeoDetectionEnabled(geoDetectionEnabled) + .build(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/TestThreadingDomain.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/TestThreadingDomain.java new file mode 100644 index 000000000000..e08fea083d81 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/TestThreadingDomain.java @@ -0,0 +1,145 @@ +/* + * 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 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.ArrayList; +import java.util.Comparator; +import java.util.Objects; +import java.util.concurrent.Callable; + +/** + * A ThreadingDomain that simulates idealized post() semantics. Execution takes place in zero time, + * exactly when scheduled, when the test code explicitly requests it. Execution takes place on the + * test's main thread. + */ +class TestThreadingDomain extends ThreadingDomain { + + static class QueuedRunnable { + + static final Comparator COMPARATOR = + (o1, o2) -> (int) (o1.executionTimeMillis - o2.executionTimeMillis); + + @NonNull public final Runnable runnable; + @Nullable public final Object token; + public final long executionTimeMillis; + + QueuedRunnable(@NonNull Runnable runnable, @Nullable Object token, + long executionTimeMillis) { + this.runnable = Objects.requireNonNull(runnable); + this.token = token; + this.executionTimeMillis = executionTimeMillis; + } + + @Override + public String toString() { + return "QueuedRunnable{" + + "runnable=" + runnable + + ", token=" + token + + ", executionTimeMillis=" + executionTimeMillis + + '}'; + } + } + + private long mCurrentTimeMillis; + private ArrayList mQueue = new ArrayList<>(); + + TestThreadingDomain() { + // Pick an arbitrary time. + mCurrentTimeMillis = 123456L; + } + + @Override + Thread getThread() { + return Thread.currentThread(); + } + + @Override + void post(Runnable r) { + postDelayed(r, null, 0); + } + + @Override + V postAndWait(Callable callable, long durationMillis) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + void postDelayed(Runnable r, long 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 == token); + } + + void removeAllQueuedRunnables() { + mQueue.clear(); + } + + void assertSingleDelayedQueueItem(Duration expectedDelay) { + assertQueueLength(1); + assertNextQueueItemIsDelayed(expectedDelay); + } + + void assertSingleImmediateQueueItem() { + assertQueueLength(1); + assertNextQueueItemIsImmediate(); + } + + void assertQueueLength(int expectedLength) { + assertEquals(expectedLength, mQueue.size()); + } + + void assertNextQueueItemIsImmediate() { + assertTrue(getNextQueueItemDelayMillis() == 0); + } + + private void assertNextQueueItemIsDelayed(Duration expectedDelay) { + assertEquals(getNextQueueItemDelayMillis(), expectedDelay.toMillis()); + } + + void assertQueueEmpty() { + assertTrue(mQueue.isEmpty()); + } + + long getNextQueueItemDelayMillis() { + assertFalse(mQueue.isEmpty()); + return mQueue.get(0).executionTimeMillis - mCurrentTimeMillis; + } + + void executeNext() { + assertFalse(mQueue.isEmpty()); + QueuedRunnable queued = mQueue.remove(0); + + mCurrentTimeMillis = queued.executionTimeMillis; + queued.runnable.run(); + } +} -- cgit v1.2.3-59-g8ed1b