diff --git a/services/core/java/com/android/server/location/timezone/BinderLocationTimeZoneProvider.java b/services/core/java/com/android/server/location/timezone/BinderLocationTimeZoneProvider.java
new file mode 100644
index 0000000000000..92dabe337d87c
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/BinderLocationTimeZoneProvider.java
@@ -0,0 +1,214 @@
+/*
+ * 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_DISABLED;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.location.timezone.LocationTimeZoneEvent;
+import android.util.IndentingPrintWriter;
+import android.util.Slog;
+
+import com.android.internal.location.timezone.LocationTimeZoneProviderRequest;
+
+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.setListener(new LocationTimeZoneProviderProxy.Listener() {
+ @Override
+ public void onReportLocationTimeZoneEvent(
+ @NonNull LocationTimeZoneEvent locationTimeZoneEvent) {
+ handleLocationTimeZoneEvent(locationTimeZoneEvent);
+ }
+
+ @Override
+ public void onProviderBound() {
+ handleOnProviderBound();
+ }
+
+ @Override
+ public void onProviderUnbound() {
+ handleProviderLost("onProviderUnbound()");
+ }
+ });
+ }
+
+ private void handleProviderLost(String reason) {
+ mThreadingDomain.assertCurrentThread();
+
+ synchronized (mSharedLock) {
+ ProviderState currentState = mCurrentState.get();
+ switch (currentState.stateEnum) {
+ case PROVIDER_STATE_ENABLED: {
+ // Losing a remote provider is treated as becoming uncertain.
+ String msg = "handleProviderLost reason=" + reason
+ + ", mProviderName=" + mProviderName
+ + ", currentState=" + currentState;
+ debugLog(msg);
+ // This is an unusual PROVIDER_STATE_ENABLED state because event == null
+ ProviderState newState = currentState.newState(
+ PROVIDER_STATE_ENABLED, null, currentState.currentUserConfiguration,
+ msg);
+ setCurrentState(newState, true);
+ break;
+ }
+ case PROVIDER_STATE_DISABLED: {
+ debugLog("handleProviderLost reason=" + reason
+ + ", mProviderName=" + mProviderName
+ + ", currentState=" + currentState
+ + ": No state change required, provider is disabled.");
+ break;
+ }
+ case PROVIDER_STATE_PERM_FAILED: {
+ debugLog("handleProviderLost reason=" + reason
+ + ", mProviderName=" + mProviderName
+ + ", currentState=" + currentState
+ + ": No state change required, provider is perm failed.");
+ 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_ENABLED: {
+ debugLog("handleOnProviderBound mProviderName=" + mProviderName
+ + ", currentState=" + currentState + ": Provider is enabled.");
+ break;
+ }
+ case PROVIDER_STATE_DISABLED: {
+ debugLog("handleOnProviderBound mProviderName=" + mProviderName
+ + ", currentState=" + currentState + ": Provider is disabled.");
+ break;
+ }
+ case PROVIDER_STATE_PERM_FAILED: {
+ debugLog("handleOnProviderBound"
+ + ", mProviderName=" + mProviderName
+ + ", currentState=" + currentState
+ + ": No state change required, provider is perm failed.");
+ break;
+ }
+ default: {
+ throw new IllegalStateException("Unknown currentState=" + currentState);
+ }
+ }
+ }
+ }
+
+ @Override
+ void onEnable() {
+ // 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.
+ // TODO(b/152744911): Decide whether to send a timeout so the provider knows how long
+ // it has to generate the first event before it could be bypassed.
+ LocationTimeZoneProviderRequest request =
+ new LocationTimeZoneProviderRequest.Builder()
+ .setReportLocationTimeZone(true)
+ .build();
+ mProxy.setRequest(request);
+ }
+
+ @Override
+ void onDisable() {
+ LocationTimeZoneProviderRequest request =
+ new LocationTimeZoneProviderRequest.Builder()
+ .setReportLocationTimeZone(false)
+ .build();
+ mProxy.setRequest(request);
+ }
+
+ @Override
+ void logWarn(String msg) {
+ Slog.w(TAG, msg);
+ }
+
+ @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
+ + '}';
+ }
+ }
+
+ /**
+ * Passes the supplied simulation / testing event to the current proxy iff the proxy is a
+ * {@link SimulatedLocationTimeZoneProviderProxy}. If not, the event is logged but discarded.
+ */
+ void simulateBinderProviderEvent(SimulatedBinderProviderEvent event) {
+ if (!(mProxy instanceof SimulatedLocationTimeZoneProviderProxy)) {
+ Slog.w(TAG, mProxy + " is not a " + SimulatedLocationTimeZoneProviderProxy.class
+ + ", event=" + event);
+ return;
+ }
+ ((SimulatedLocationTimeZoneProviderProxy) mProxy).simulate(event);
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/ControllerCallbackImpl.java b/services/core/java/com/android/server/location/timezone/ControllerCallbackImpl.java
new file mode 100644
index 0000000000000..cd9aa2fd6a5b7
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/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.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
new file mode 100644
index 0000000000000..2e2481c30f222
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/ControllerEnvironmentImpl.java
@@ -0,0 +1,51 @@
+/*
+ * 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.ConfigurationInternal;
+import com.android.server.timezonedetector.TimeZoneDetectorInternal;
+
+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 {
+
+ @NonNull private final TimeZoneDetectorInternal mTimeZoneDetectorInternal;
+ @NonNull private final LocationTimeZoneProviderController mController;
+
+ ControllerEnvironmentImpl(@NonNull ThreadingDomain threadingDomain,
+ @NonNull LocationTimeZoneProviderController controller) {
+ super(threadingDomain);
+ mController = Objects.requireNonNull(controller);
+ mTimeZoneDetectorInternal = LocalServices.getService(TimeZoneDetectorInternal.class);
+
+ // Listen for configuration changes.
+ mTimeZoneDetectorInternal.addConfigurationListener(
+ () -> mThreadingDomain.post(mController::onConfigChanged));
+ }
+
+ @Override
+ ConfigurationInternal getCurrentUserConfigurationInternal() {
+ return mTimeZoneDetectorInternal.getCurrentUserConfigurationInternal();
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/ControllerImpl.java b/services/core/java/com/android/server/location/timezone/ControllerImpl.java
new file mode 100644
index 0000000000000..e31cfc4e8b408
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/ControllerImpl.java
@@ -0,0 +1,420 @@
+/*
+ * 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.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_PERMANENT_FAILURE;
+import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_SUCCESS;
+import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_UNCERTAIN;
+
+import static com.android.server.location.timezone.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_DISABLED;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.location.timezone.LocationTimeZoneEvent;
+import android.util.IndentingPrintWriter;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+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.Objects;
+
+/**
+ * A real implementation of {@link LocationTimeZoneProviderController} that supports a single
+ * {@link LocationTimeZoneProvider}.
+ *
+ * TODO(b/152744911): This implementation currently only supports a single ("primary") provider.
+ * Support for a secondary provider will be added in a later commit.
+ */
+class ControllerImpl extends LocationTimeZoneProviderController {
+
+ @VisibleForTesting
+ static final Duration UNCERTAINTY_DELAY = Duration.ofMinutes(5);
+
+ @NonNull private final LocationTimeZoneProvider mProvider;
+ @NonNull private final SingleRunnableQueue mDelayedSuggestionQueue;
+
+ @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;
+
+ /**
+ * Contains any currently pending suggestion on {@link #mDelayedSuggestionQueue}, if there is
+ * one.
+ */
+ @GuardedBy("mSharedLock")
+ @Nullable
+ private GeolocationTimeZoneSuggestion mPendingSuggestion;
+
+ /** Contains the last suggestion actually made, if there is one. */
+ @GuardedBy("mSharedLock")
+ @Nullable
+ private GeolocationTimeZoneSuggestion mLastSuggestion;
+
+ ControllerImpl(@NonNull ThreadingDomain threadingDomain,
+ @NonNull LocationTimeZoneProvider provider) {
+ super(threadingDomain);
+ mDelayedSuggestionQueue = threadingDomain.createSingleRunnableQueue();
+ mProvider = Objects.requireNonNull(provider);
+ }
+
+ @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();
+
+ mProvider.initialize(ControllerImpl.this::onProviderStateChange);
+ enableOrDisableProvider(mCurrentUserConfiguration);
+ }
+ }
+
+ @Override
+ void onConfigChanged() {
+ mThreadingDomain.assertCurrentThread();
+
+ synchronized (mSharedLock) {
+ debugLog("onEnvironmentConfigChanged()");
+
+ ConfigurationInternal oldConfig = mCurrentUserConfiguration;
+ ConfigurationInternal newConfig = mEnvironment.getCurrentUserConfigurationInternal();
+ mCurrentUserConfiguration = newConfig;
+
+ if (!newConfig.equals(oldConfig)) {
+ if (newConfig.getUserId() != oldConfig.getUserId()) {
+ // If the user changed, disable the provider if needed. It may be re-enabled for
+ // the new user below if their settings allow.
+ debugLog("User changed. old=" + oldConfig.getUserId()
+ + ", new=" + newConfig.getUserId());
+ debugLog("Disabling LocationTimeZoneProviders as needed");
+ if (mProvider.getCurrentState().stateEnum == PROVIDER_STATE_ENABLED) {
+ mProvider.disable();
+ }
+ }
+
+ enableOrDisableProvider(newConfig);
+ }
+ }
+ }
+
+ @GuardedBy("mSharedLock")
+ private void enableOrDisableProvider(@NonNull ConfigurationInternal configuration) {
+ ProviderState providerState = mProvider.getCurrentState();
+ boolean geoDetectionEnabled = configuration.getGeoDetectionEnabledBehavior();
+ boolean providerWasEnabled = providerState.stateEnum == PROVIDER_STATE_ENABLED;
+ if (geoDetectionEnabled) {
+ switch (providerState.stateEnum) {
+ case PROVIDER_STATE_DISABLED: {
+ debugLog("Enabling " + mProvider);
+ mProvider.enable(configuration);
+ break;
+ }
+ case PROVIDER_STATE_ENABLED: {
+ debugLog("No need to enable " + mProvider + ": already enabled");
+ break;
+ }
+ case PROVIDER_STATE_PERM_FAILED: {
+ debugLog("Unable to enable " + mProvider + ": it is perm failed");
+ break;
+ }
+ default:
+ warnLog("Unknown provider state: " + mProvider);
+ break;
+ }
+ } else {
+ switch (providerState.stateEnum) {
+ case PROVIDER_STATE_DISABLED: {
+ debugLog("No need to disable " + mProvider + ": already enabled");
+ break;
+ }
+ case PROVIDER_STATE_ENABLED: {
+ debugLog("Disabling " + mProvider);
+ mProvider.disable();
+ break;
+ }
+ case PROVIDER_STATE_PERM_FAILED: {
+ debugLog("Unable to disable " + mProvider + ": it is perm failed");
+ break;
+ }
+ default: {
+ warnLog("Unknown provider state: " + mProvider);
+ break;
+ }
+ }
+ }
+
+ boolean isProviderEnabled =
+ mProvider.getCurrentState().stateEnum == PROVIDER_STATE_ENABLED;
+
+ if (isProviderEnabled) {
+ if (!providerWasEnabled) {
+ // When a provider has first been enabled, we allow it some time for it to
+ // initialize.
+ // This sets up an empty suggestion to trigger if no explicit "certain" or
+ // "uncertain" suggestion preempts it within UNCERTAINTY_DELAY. If, for some reason,
+ // the provider does provide any events then this scheduled suggestion will ensure
+ // the controller makes at least an uncertain suggestion.
+ suggestDelayed(createEmptySuggestion(
+ "No event received in delay=" + UNCERTAINTY_DELAY), UNCERTAINTY_DELAY);
+ }
+ } else {
+ // Clear any queued suggestions.
+ clearDelayedSuggestion();
+
+ // If the provider is now not enabled, and a previous "certain" suggestion has been
+ // made, then a new "uncertain" suggestion must be made to indicate the provider no
+ // longer has an opinion and will not be sending updates.
+ if (mLastSuggestion != null && mLastSuggestion.getZoneIds() != null) {
+ suggestImmediate(createEmptySuggestion(""));
+ }
+ }
+ }
+
+ void onProviderStateChange(@NonNull ProviderState providerState) {
+ mThreadingDomain.assertCurrentThread();
+ assertProviderKnown(providerState.provider);
+
+ synchronized (mSharedLock) {
+ switch (providerState.stateEnum) {
+ case PROVIDER_STATE_DISABLED: {
+ // This should never happen: entering disabled does not trigger an event.
+ warnLog("onProviderStateChange: Unexpected state change for disabled provider,"
+ + " providerState=" + providerState);
+ break;
+ }
+ case PROVIDER_STATE_ENABLED: {
+ // Entering enabled does not trigger an event, so this only happens if an event
+ // is received while the provider is enabled.
+ debugLog("onProviderStateChange: Received notification of an event while"
+ + " enabled, providerState=" + providerState);
+ providerEnabledProcessEvent(providerState);
+ break;
+ }
+ case PROVIDER_STATE_PERM_FAILED: {
+ debugLog("Received notification of permanent failure for"
+ + " provider=" + providerState);
+ GeolocationTimeZoneSuggestion suggestion = createEmptySuggestion(
+ "provider=" + providerState.provider
+ + " permanently failed: " + providerState);
+ suggestImmediate(suggestion);
+ break;
+ }
+ default: {
+ warnLog("onProviderStateChange: Unexpected providerState=" + providerState);
+ }
+ }
+ }
+ }
+
+ private void assertProviderKnown(LocationTimeZoneProvider provider) {
+ if (provider != mProvider) {
+ throw new IllegalArgumentException("Unknown provider: " + provider);
+ }
+ }
+
+ /**
+ * Called when a provider has changed state but just moved from a PROVIDER_STATE_ENABLED state
+ * to another PROVIDER_STATE_ENABLED state, usually as a result of a new {@link
+ * LocationTimeZoneEvent} being received. There are some cases where event can be null.
+ */
+ private void providerEnabledProcessEvent(@NonNull ProviderState providerState) {
+ LocationTimeZoneEvent event = providerState.event;
+ if (event == null) {
+ // Implicit uncertainty, i.e. where the provider is enabled, but a problem has been
+ // detected without having received an event. For example, if the process has detected
+ // the loss of a binder-based provider. This is treated like explicit uncertainty, i.e.
+ // where the provider has explicitly told this process it is uncertain.
+ scheduleUncertainSuggestionIfNeeded(null);
+ return;
+ }
+
+ // Consistency check for user. This may be possible as there are various races around
+ // current user switches.
+ if (!Objects.equals(event.getUserHandle(), mCurrentUserConfiguration.getUserHandle())) {
+ warnLog("Using event=" + event + " from a different user="
+ + mCurrentUserConfiguration);
+ }
+
+ if (!mCurrentUserConfiguration.getGeoDetectionEnabledBehavior()) {
+ // This should not happen: the provider should not be in an enabled state if the user
+ // does not have geodetection enabled.
+ warnLog("Provider=" + providerState + " is enabled, but currentUserConfiguration="
+ + mCurrentUserConfiguration + " suggests it shouldn't be.");
+ }
+
+ switch (event.getEventType()) {
+ case EVENT_TYPE_PERMANENT_FAILURE: {
+ // This shouldn't happen. Providers cannot be enabled and have this event.
+ warnLog("Provider=" + providerState
+ + " is enabled, but event suggests it shouldn't be");
+ break;
+ }
+ case EVENT_TYPE_UNCERTAIN: {
+ scheduleUncertainSuggestionIfNeeded(event);
+ break;
+ }
+ case EVENT_TYPE_SUCCESS: {
+ GeolocationTimeZoneSuggestion suggestion =
+ new GeolocationTimeZoneSuggestion(event.getTimeZoneIds());
+ suggestion.addDebugInfo("Event received provider=" + mProvider.getName()
+ + ", event=" + event);
+ // Rely on the receiver to dedupe events. It is better to over-communicate.
+ suggestImmediate(suggestion);
+ break;
+ }
+ default: {
+ warnLog("Unknown eventType=" + event.getEventType());
+ break;
+ }
+ }
+ }
+
+ /**
+ * Indicates a provider has become uncertain with the event (if any) received that indicates
+ * that.
+ *
+ *
Providers are expected to report their uncertainty as soon as they become uncertain, as
+ * this enables the most flexibility for the controller to enable other providers when there are
+ * multiple ones. The controller is therefore responsible for deciding when to make a
+ * "uncertain" suggestion.
+ *
+ *
This method schedules an "uncertain" suggestion (if one isn't already scheduled) to be
+ * made later if nothing else preempts it. It can be preempted if the provider becomes certain
+ * (or does anything else that calls {@link #suggestImmediate(GeolocationTimeZoneSuggestion)})
+ * within UNCERTAINTY_DELAY. Preemption causes the scheduled "uncertain" event to be cancelled.
+ * If the provider repeatedly sends uncertainty events within UNCERTAINTY_DELAY, those events
+ * are effectively ignored (i.e. the timer is not reset each time).
+ */
+ private void scheduleUncertainSuggestionIfNeeded(@Nullable LocationTimeZoneEvent event) {
+ if (mPendingSuggestion == null || mPendingSuggestion.getZoneIds() != null) {
+ GeolocationTimeZoneSuggestion suggestion = createEmptySuggestion(
+ "provider=" + mProvider + " became uncertain, event=" + event);
+ suggestDelayed(suggestion, UNCERTAINTY_DELAY);
+ }
+ }
+
+ @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("mPendingSuggestion=" + mPendingSuggestion);
+ ipw.println("mLastSuggestion=" + mLastSuggestion);
+
+ ipw.println("Provider:");
+ ipw.increaseIndent(); // level 2
+ mProvider.dump(ipw, args);
+ ipw.decreaseIndent(); // level 2
+
+ ipw.decreaseIndent(); // level 1
+ }
+ }
+
+ /** Sends an immediate suggestion, cancelling any pending suggestion. */
+ @GuardedBy("mSharedLock")
+ private void suggestImmediate(@NonNull GeolocationTimeZoneSuggestion suggestion) {
+ debugLog("suggestImmediate: Executing suggestion=" + suggestion);
+ mDelayedSuggestionQueue.runSynchronously(() -> mCallback.suggest(suggestion));
+ mPendingSuggestion = null;
+ mLastSuggestion = suggestion;
+ }
+
+ /** Clears any pending suggestion. */
+ @GuardedBy("mSharedLock")
+ private void clearDelayedSuggestion() {
+ mDelayedSuggestionQueue.cancel();
+ mPendingSuggestion = null;
+ }
+
+
+ /**
+ * Schedules a delayed suggestion. There can only be one delayed suggestion at a time.
+ * If there is a pending scheduled suggestion equal to the one passed, it will not be replaced.
+ * Replacing a previous delayed suggestion has the effect of cancelling the timeout associated
+ * with that previous suggestion.
+ */
+ @GuardedBy("mSharedLock")
+ private void suggestDelayed(@NonNull GeolocationTimeZoneSuggestion suggestion,
+ @NonNull Duration delay) {
+ Objects.requireNonNull(suggestion);
+ Objects.requireNonNull(delay);
+
+ if (Objects.equals(mPendingSuggestion, suggestion)) {
+ // Do not reset the timer.
+ debugLog("suggestDelayed: Suggestion=" + suggestion + " is equal to existing."
+ + " Not scheduled.");
+ return;
+ }
+
+ debugLog("suggestDelayed: Scheduling suggestion=" + suggestion);
+ mPendingSuggestion = suggestion;
+
+ mDelayedSuggestionQueue.runDelayed(() -> {
+ debugLog("suggestDelayed: Executing suggestion=" + suggestion);
+ mCallback.suggest(suggestion);
+ mPendingSuggestion = null;
+ mLastSuggestion = suggestion;
+ }, delay.toMillis());
+ }
+
+ private static GeolocationTimeZoneSuggestion createEmptySuggestion(String reason) {
+ GeolocationTimeZoneSuggestion suggestion = new GeolocationTimeZoneSuggestion(null);
+ suggestion.addDebugInfo(reason);
+ return suggestion;
+ }
+
+ /**
+ * Asynchronously passes a {@link SimulatedBinderProviderEvent] to the appropriate provider.
+ * If the provider name does not match a known provider, then the event is logged and discarded.
+ */
+ void simulateBinderProviderEvent(SimulatedBinderProviderEvent event) {
+ if (!Objects.equals(mProvider.getName(), event.getProviderName())) {
+ warnLog("Unable to process simulated binder provider event,"
+ + " unknown providerName in event=" + event);
+ return;
+ }
+ if (!(mProvider instanceof BinderLocationTimeZoneProvider)) {
+ warnLog("Unable to process simulated binder provider event,"
+ + " provider is not a " + BinderLocationTimeZoneProvider.class
+ + ", event=" + event);
+ return;
+ }
+ ((BinderLocationTimeZoneProvider) mProvider).simulateBinderProviderEvent(event);
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/HandlerThreadingDomain.java b/services/core/java/com/android/server/location/timezone/HandlerThreadingDomain.java
new file mode 100644
index 0000000000000..17e719ed2cb09
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/HandlerThreadingDomain.java
@@ -0,0 +1,73 @@
+/*
+ * 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.os.Handler;
+
+import java.util.Objects;
+
+/**
+ * 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
+ void postDelayed(@NonNull Runnable r, long delayMillis) {
+ getHandler().postDelayed(r, delayMillis);
+ }
+
+ @Override
+ void postDelayed(Runnable r, Object token, 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
new file mode 100644
index 0000000000000..238f999ff8a67
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerService.java
@@ -0,0 +1,269 @@
+/*
+ * 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.Binder;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
+import android.os.SystemProperties;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.DumpUtils;
+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.util.Objects;
+
+/**
+ * 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
+ * offer {@link android.location.timezone.LocationTimeZoneEvent}s.
+ *
+ * TODO(b/152744911): This implementation currently only supports a primary provider. Support for a
+ * secondary provider must be added in a later commit.
+ *
+ *
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() {
+ if (TimeZoneDetectorService.GEOLOCATION_TIME_ZONE_DETECTION_ENABLED) {
+ Context context = getContext();
+ 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 compile-time disabled");
+ }
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ if (TimeZoneDetectorService.GEOLOCATION_TIME_ZONE_DETECTION_ENABLED) {
+ 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";
+
+ static final String PRIMARY_PROVIDER_NAME = "primary";
+
+ private static final String SIMULATION_MODE_SYSTEM_PROPERTY_PREFIX =
+ "persist.sys.location_tz_simulation_mode.";
+
+ private static final String ATTRIBUTION_TAG = "LocationTimeZoneService";
+
+ private static final String PRIMARY_LOCATION_TIME_ZONE_SERVICE_ACTION =
+ "com.android.location.timezone.service.v1.PrimaryLocationTimeZoneProvider";
+
+
+ @NonNull private final Context mContext;
+
+ /**
+ * The {@link ThreadingDomain} used to supply the {@link android.os.Handler} and 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;
+
+ /** The shared lock from {@link #mThreadingDomain}. */
+ @NonNull private final Object mSharedLock;
+
+ // Lazily initialized. Non-null and effectively final after onSystemThirdPartyAppsCanStart().
+ @GuardedBy("mSharedLock")
+ private ControllerImpl mLocationTimeZoneDetectorController;
+
+ LocationTimeZoneManagerService(Context context) {
+ mContext = context.createAttributionContext(ATTRIBUTION_TAG);
+ mThreadingDomain = new HandlerThreadingDomain(FgThread.getHandler());
+ 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.
+ synchronized (mSharedLock) {
+ LocationTimeZoneProvider primary = createPrimaryProvider();
+ mLocationTimeZoneDetectorController = new ControllerImpl(mThreadingDomain, primary);
+ ControllerCallbackImpl callback = new ControllerCallbackImpl(mThreadingDomain);
+ ControllerEnvironmentImpl environment = new ControllerEnvironmentImpl(
+ mThreadingDomain, mLocationTimeZoneDetectorController);
+
+ // Initialize the controller on the mThreadingDomain thread: this ensures that the
+ // ThreadingDomain requirements for the controller / environment methods are honored.
+ mThreadingDomain.post(() ->
+ mLocationTimeZoneDetectorController.initialize(environment, callback));
+ }
+ }
+
+ private LocationTimeZoneProvider createPrimaryProvider() {
+ LocationTimeZoneProviderProxy proxy;
+ if (isInSimulationMode(PRIMARY_PROVIDER_NAME)) {
+ proxy = new SimulatedLocationTimeZoneProviderProxy(mContext, mThreadingDomain);
+ } else {
+ // TODO Uncomment this code in a later commit.
+ throw new UnsupportedOperationException("Not implemented");
+ /*
+ proxy = RealLocationTimeZoneProviderProxy.createAndRegister(
+ mContext,
+ mThreadingDomain,
+ PRIMARY_LOCATION_TIME_ZONE_SERVICE_ACTION,
+ com.android.internal.R.bool.config_enablePrimaryLocationTimeZoneOverlay,
+ com.android.internal.R.string.config_primaryLocationTimeZoneProviderPackageName
+ );
+ */
+ }
+ return createLocationTimeZoneProvider(PRIMARY_PROVIDER_NAME, proxy);
+ }
+
+ private boolean isInSimulationMode(String providerName) {
+ return SystemProperties.getBoolean(
+ SIMULATION_MODE_SYSTEM_PROPERTY_PREFIX + providerName, false);
+ }
+
+ private LocationTimeZoneProvider createLocationTimeZoneProvider(
+ @NonNull String providerName, @NonNull LocationTimeZoneProviderProxy proxy) {
+ LocationTimeZoneProvider provider;
+ if (proxy != null) {
+ debugLog("LocationTimeZoneProvider found for providerName=" + providerName);
+ provider = new BinderLocationTimeZoneProvider(mThreadingDomain,
+ providerName, proxy);
+ } else {
+ debugLog("No LocationTimeZoneProvider found for providerName=" + providerName
+ + ": stubbing");
+ provider = new NullLocationTimeZoneProvider(mThreadingDomain, providerName);
+ }
+ return provider;
+ }
+
+ @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);
+ }
+
+ /**
+ * Asynchronously passes a {@link SimulatedBinderProviderEvent] to the appropriate provider.
+ * The device must be in simulation mode, otherwise an {@link IllegalStateException} will be
+ * thrown.
+ */
+ void simulateBinderProviderEvent(SimulatedBinderProviderEvent event)
+ throws IllegalStateException {
+ if (!isInSimulationMode(event.getProviderName())) {
+ throw new IllegalStateException("Use \"setprop "
+ + SIMULATION_MODE_SYSTEM_PROPERTY_PREFIX + event.getProviderName()
+ + " 1\" and reboot before injecting simulated binder events.");
+ }
+ mThreadingDomain.post(
+ () -> mLocationTimeZoneDetectorController.simulateBinderProviderEvent(event));
+ }
+
+ @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("{Uninitialized}");
+ } 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) {
+ if (Log.isLoggable(TAG, Log.WARN)) {
+ Slog.w(TAG, msg);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerShellCommand.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerShellCommand.java
new file mode 100644
index 0000000000000..7c3b891743cc8
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/LocationTimeZoneManagerShellCommand.java
@@ -0,0 +1,80 @@
+/*
+ * 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.os.ShellCommand;
+
+import java.io.PrintWriter;
+
+/** Implements the shell command interface for {@link LocationTimeZoneManagerService}. */
+class LocationTimeZoneManagerShellCommand extends ShellCommand {
+
+ private final LocationTimeZoneManagerService mService;
+
+ LocationTimeZoneManagerShellCommand(LocationTimeZoneManagerService service) {
+ mService = service;
+ }
+
+ @Override
+ public int onCommand(String cmd) {
+ if (cmd == null) {
+ return handleDefaultCommands(cmd);
+ }
+
+ switch (cmd) {
+ case "simulate_binder": {
+ return runSimulateBinderEvent();
+ }
+ default: {
+ return handleDefaultCommands(cmd);
+ }
+ }
+ }
+
+ private int runSimulateBinderEvent() {
+ PrintWriter outPrintWriter = getOutPrintWriter();
+
+ SimulatedBinderProviderEvent simulatedProviderBinderEvent;
+ try {
+ simulatedProviderBinderEvent = SimulatedBinderProviderEvent.createFromArgs(this);
+ } catch (IllegalArgumentException e) {
+ outPrintWriter.println("Error: " + e.getMessage());
+ return 1;
+ }
+
+ outPrintWriter.println("Injecting: " + simulatedProviderBinderEvent);
+ try {
+ mService.simulateBinderProviderEvent(simulatedProviderBinderEvent);
+ } catch (IllegalStateException e) {
+ outPrintWriter.println("Error: " + e.getMessage());
+ return 2;
+ }
+ return 0;
+ }
+
+ @Override
+ public void onHelp() {
+ final PrintWriter pw = getOutPrintWriter();
+ pw.println("Location Time Zone Manager (location_time_zone_manager) commands:");
+ pw.println(" help");
+ pw.println(" Print this help text.");
+ pw.println(" simulate_binder");
+ pw.println(" ");
+ pw.println();
+ SimulatedBinderProviderEvent.printCommandLineOpts(pw);
+ pw.println();
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProvider.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProvider.java
new file mode 100644
index 0000000000000..3743779b20c91
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProvider.java
@@ -0,0 +1,511 @@
+/*
+ * 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.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_PERMANENT_FAILURE;
+import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_SUCCESS;
+import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_UNCERTAIN;
+
+import static com.android.server.location.timezone.LocationTimeZoneManagerService.debugLog;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DISABLED;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_ENABLED;
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.location.timezone.LocationTimeZoneEvent;
+import android.os.Handler;
+import android.os.SystemClock;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.timezonedetector.ConfigurationInternal;
+import com.android.server.timezonedetector.Dumpable;
+import com.android.server.timezonedetector.ReferenceWithHistory;
+
+import java.util.Objects;
+
+/**
+ * A facade used by the {@link LocationTimeZoneProviderController} to interact with a location time
+ * zone provider. The provider could have a binder implementation with logic running in another
+ * process, or could be a stubbed instance when no real provider is registered.
+ *
+ *
The provider is supplied with a {@link ProviderListener} via {@link
+ * #initialize(ProviderListener)}. This enables it to communicates 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.
+ *
+ *
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({ PROVIDER_STATE_UNKNOWN, PROVIDER_STATE_ENABLED, PROVIDER_STATE_DISABLED,
+ PROVIDER_STATE_PERM_FAILED })
+ @interface ProviderStateEnum {}
+
+ /**
+ * Uninitialized value. Must not be used afte {@link LocationTimeZoneProvider#initialize}.
+ */
+ static final int PROVIDER_STATE_UNKNOWN = 0;
+
+ /**
+ * The provider is currently enabled.
+ */
+ static final int PROVIDER_STATE_ENABLED = 1;
+
+ /**
+ * The provider is currently disabled.
+ * This is the state after {@link #initialize} is called.
+ */
+ static final int PROVIDER_STATE_DISABLED = 2;
+
+ /**
+ * The provider has failed and cannot be re-enabled.
+ *
+ * Providers may enter this state after a provider is enabled.
+ */
+ static final int PROVIDER_STATE_PERM_FAILED = 3;
+
+ /** 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 LocationTimeZoneEvent} received. Only populated when {@link #stateEnum}
+ * is {@link #PROVIDER_STATE_ENABLED}, but it can be {@code null} then too if no event has
+ * yet been received.
+ */
+ @Nullable public final LocationTimeZoneEvent event;
+
+ /**
+ * The user configuration associated with the current state. Only and always present when
+ * {@link #stateEnum} is {@link #PROVIDER_STATE_ENABLED}.
+ */
+ @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.
+ */
+ 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 LocationTimeZoneEvent 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 LocationTimeZoneEvent event,
+ @Nullable ConfigurationInternal currentUserConfig,
+ @Nullable String debugInfo) {
+
+ // Check valid "from" transitions.
+ switch (this.stateEnum) {
+ case PROVIDER_STATE_UNKNOWN: {
+ if (newStateEnum != PROVIDER_STATE_DISABLED) {
+ throw new IllegalArgumentException(
+ "Must transition from " + prettyPrintStateEnum(
+ PROVIDER_STATE_UNKNOWN)
+ + " to " + prettyPrintStateEnum(PROVIDER_STATE_DISABLED));
+ }
+ break;
+ }
+ case PROVIDER_STATE_DISABLED:
+ case PROVIDER_STATE_ENABLED: {
+ // These can go to each other or PROVIDER_STATE_PERM_FAILED.
+ break;
+ }
+ case PROVIDER_STATE_PERM_FAILED: {
+ throw new IllegalArgumentException("Illegal transition out of "
+ + prettyPrintStateEnum(PROVIDER_STATE_UNKNOWN));
+ }
+ 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_DISABLED: {
+ if (event != null || currentUserConfig != null) {
+ throw new IllegalArgumentException(
+ "Disabled state: event and currentUserConfig must be null"
+ + ", event=" + event
+ + ", currentUserConfig=" + currentUserConfig);
+ }
+ break;
+ }
+ case PROVIDER_STATE_ENABLED: {
+ if (currentUserConfig == null) {
+ throw new IllegalArgumentException(
+ "Enabled state: currentUserConfig must not be null");
+ }
+ break;
+ }
+ case PROVIDER_STATE_PERM_FAILED: {
+ if (event != null || currentUserConfig != null) {
+ throw new IllegalArgumentException(
+ "Perf failed state: event and currentUserConfig must be null"
+ + ", event=" + event
+ + ", currentUserConfig=" + currentUserConfig);
+ }
+ break;
+ }
+ default: {
+ throw new IllegalArgumentException("Unknown newStateEnum=" + newStateEnum);
+ }
+ }
+ return new ProviderState(provider, newStateEnum, event, currentUserConfig, debugInfo);
+ }
+
+ @Override
+ public String toString() {
+ return "State{"
+ + "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_DISABLED:
+ return "Disabled (" + PROVIDER_STATE_DISABLED + ")";
+ case PROVIDER_STATE_ENABLED:
+ return "Enabled (" + PROVIDER_STATE_ENABLED + ")";
+ case PROVIDER_STATE_PERM_FAILED:
+ return "Perm failure (" + PROVIDER_STATE_PERM_FAILED + ")";
+ case PROVIDER_STATE_UNKNOWN:
+ default:
+ return "Unknown (" + state + ")";
+ }
+ }
+ }
+
+ @NonNull final ThreadingDomain mThreadingDomain;
+ @NonNull final Object mSharedLock;
+ @NonNull final String mProviderName;
+
+ /**
+ * The current state (with history for debugging).
+ */
+ @GuardedBy("mSharedLock")
+ final ReferenceWithHistory mCurrentState =
+ new ReferenceWithHistory<>(10);
+
+ // 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);
+ mSharedLock = threadingDomain.getLockObject();
+ mProviderName = Objects.requireNonNull(providerName);
+ }
+
+ /**
+ * 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);
+ ProviderState newState = currentState.newState(
+ PROVIDER_STATE_DISABLED, null, null, "initialize() called");
+ setCurrentState(newState, false);
+
+ onInitialize();
+ }
+ }
+
+ /**
+ * Implemented by subclasses to do work during {@link #initialize}.
+ */
+ abstract void onInitialize();
+
+ /**
+ * 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 (notifyChanges) {
+ if (!Objects.equals(newState, oldState)) {
+ 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;
+ }
+
+ /**
+ * Enables the provider. It is an error to call this method except when the {@link
+ * #getCurrentState()} is at {@link ProviderState#PROVIDER_STATE_DISABLED}. This method must be
+ * called using the handler thread from the {@link ThreadingDomain}.
+ */
+ final void enable(@NonNull ConfigurationInternal currentUserConfiguration) {
+ mThreadingDomain.assertCurrentThread();
+
+ synchronized (mSharedLock) {
+ assertCurrentState(PROVIDER_STATE_DISABLED);
+
+ ProviderState currentState = getCurrentState();
+ ProviderState newState = currentState.newState(
+ PROVIDER_STATE_ENABLED, null, currentUserConfiguration, "enable() called");
+ setCurrentState(newState, false);
+ onEnable();
+ }
+ }
+
+ /**
+ * Implemented by subclasses to do work during {@link #enable}.
+ */
+ abstract void onEnable();
+
+ /**
+ * Disables the provider. It is an error* to call this method except when the {@link
+ * #getCurrentState()} is at {@link ProviderState#PROVIDER_STATE_ENABLED}. This method must be
+ * called using the handler thread from the {@link ThreadingDomain}.
+ */
+ final void disable() {
+ mThreadingDomain.assertCurrentThread();
+
+ synchronized (mSharedLock) {
+ assertCurrentState(PROVIDER_STATE_ENABLED);
+
+ ProviderState currentState = getCurrentState();
+ ProviderState newState =
+ currentState.newState(PROVIDER_STATE_DISABLED, null, null, "disable() called");
+ setCurrentState(newState, false);
+
+ onDisable();
+ }
+ }
+
+ /**
+ * Implemented by subclasses to do work during {@link #disable}.
+ */
+ abstract void onDisable();
+
+ /** For subclasses to invoke when a {@link LocationTimeZoneEvent} has been received. */
+ final void handleLocationTimeZoneEvent(
+ @NonNull LocationTimeZoneEvent locationTimeZoneEvent) {
+ mThreadingDomain.assertCurrentThread();
+ Objects.requireNonNull(locationTimeZoneEvent);
+
+ synchronized (mSharedLock) {
+ debugLog("handleLocationTimeZoneEvent: mProviderName=" + mProviderName
+ + ", locationTimeZoneEvent=" + locationTimeZoneEvent);
+
+ ProviderState currentState = getCurrentState();
+ int eventType = locationTimeZoneEvent.getEventType();
+ switch (currentState.stateEnum) {
+ case PROVIDER_STATE_PERM_FAILED: {
+ // After entering perm failed, there is nothing to do. The remote peer is
+ // supposed to stop sending events after it has reported perm failure.
+ logWarn("handleLocationTimeZoneEvent: Event=" + locationTimeZoneEvent
+ + " received for provider=" + this + " when in failed state");
+ return;
+ }
+ case PROVIDER_STATE_DISABLED: {
+ switch (eventType) {
+ case EVENT_TYPE_PERMANENT_FAILURE: {
+ String msg = "handleLocationTimeZoneEvent:"
+ + " Failure event=" + locationTimeZoneEvent
+ + " received for disabled provider=" + this
+ + ", entering permanently failed state";
+ logWarn(msg);
+ ProviderState newState = currentState.newState(
+ PROVIDER_STATE_PERM_FAILED, null, null, msg);
+ setCurrentState(newState, true);
+ return;
+ }
+ case EVENT_TYPE_SUCCESS:
+ case EVENT_TYPE_UNCERTAIN: {
+ // Any geolocation-related events received for a disabled provider are
+ // ignored: they should not happen.
+ logWarn("handleLocationTimeZoneEvent:"
+ + " event=" + locationTimeZoneEvent
+ + " received for disabled provider=" + this
+ + ", ignoring");
+
+ return;
+ }
+ default: {
+ throw new IllegalStateException(
+ "Unknown eventType=" + locationTimeZoneEvent);
+ }
+ }
+ }
+ case PROVIDER_STATE_ENABLED: {
+ switch (eventType) {
+ case EVENT_TYPE_PERMANENT_FAILURE: {
+ String msg = "handleLocationTimeZoneEvent:"
+ + " Failure event=" + locationTimeZoneEvent
+ + " received for provider=" + this
+ + ", entering permanently failed state";
+ logWarn(msg);
+ ProviderState newState = currentState.newState(
+ PROVIDER_STATE_PERM_FAILED, null, null, msg);
+ setCurrentState(newState, true);
+ return;
+ }
+ case EVENT_TYPE_UNCERTAIN:
+ case EVENT_TYPE_SUCCESS: {
+ ProviderState newState = currentState.newState(PROVIDER_STATE_ENABLED,
+ locationTimeZoneEvent, currentState.currentUserConfiguration,
+ "handleLocationTimeZoneEvent() when enabled");
+ setCurrentState(newState, true);
+ return;
+ }
+ default: {
+ throw new IllegalStateException(
+ "Unknown eventType=" + locationTimeZoneEvent);
+ }
+ }
+ }
+ default: {
+ throw new IllegalStateException("Unknown providerType=" + currentState);
+ }
+ }
+ }
+ }
+
+ /**
+ * Implemented by subclasses.
+ */
+ abstract void logWarn(String msg);
+
+ private void assertCurrentState(@ProviderState.ProviderStateEnum int requiredState) {
+ ProviderState currentState = getCurrentState();
+ if (currentState.stateEnum != requiredState) {
+ throw new IllegalStateException(
+ "Required stateEnum=" + requiredState + ", but was " + currentState);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderController.java b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderController.java
new file mode 100644
index 0000000000000..2f75c43594df6
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderController.java
@@ -0,0 +1,124 @@
+/*
+ * 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.os.Handler;
+
+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.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:
+ *
+ *
The surrounding service, which calls {@link #initialize(Environment, Callback)} and
+ * {@link #onConfigChanged()}.
+ *
The {@link Environment} through which obtains information it needs.
+ *
The {@link Callback} through which it makes time zone suggestions.
+ *
Any {@link LocationTimeZoneProvider} instances it owns, which communicate via the
+ * {@link LocationTimeZoneProvider.ProviderListener#onProviderStateChange(ProviderState)}
+ * method.
+ *
+ *
+ *
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 disable 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();
+
+ /**
+ * 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();
+ }
+
+ /** Returns the {@link ConfigurationInternal} for the current user of the device. */
+ abstract ConfigurationInternal getCurrentUserConfigurationInternal();
+ }
+
+ /**
+ * 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
new file mode 100644
index 0000000000000..3d889ae1856ad
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/LocationTimeZoneProviderProxy.java
@@ -0,0 +1,119 @@
+/*
+ * 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.location.timezone.LocationTimeZoneEvent;
+import android.os.Handler;
+import android.util.IndentingPrintWriter;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.location.timezone.LocationTimeZoneProviderRequest;
+import com.android.server.timezonedetector.Dumpable;
+
+import java.util.Objects;
+
+/**
+ * System server-side proxy for ILocationTimeZoneProvider implementations, i.e. this provides the
+ * system server object used to communicate with a remote LocationTimeZoneProvider over Binder,
+ * which could be running in a different process. As LocationTimeZoneProviders are bound / unbound
+ * this proxy will rebind to the "best" available remote process.
+ *
+ *
Threading guarantees provided / required by this interface:
+ *
+ *
All public methods defined by this class must be invoked using the {@link Handler} thread
+ * from the {@link ThreadingDomain} passed to the constructor, excluding
+ * {@link #dump(IndentingPrintWriter, String[])}
+ *
Non-static public methods that make binder calls to remote processes (e.g.
+ * {@link #setRequest(LocationTimeZoneProviderRequest)}) are executed asynchronously and will
+ * return immediately.
+ *
Callbacks received via binder are delivered via {@link Listener} are delivered on the
+ * {@link Handler} thread from the {@link ThreadingDomain} passed to the constructor.
+ *
+ *
+ *
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();
+ }
+
+ /**
+ * Sets the listener. The listener can expect to receive all events after this point.
+ */
+ void setListener(@NonNull Listener listener) {
+ Objects.requireNonNull(listener);
+ synchronized (mSharedLock) {
+ if (mListener != null) {
+ throw new IllegalStateException("listener already set");
+ }
+ this.mListener = listener;
+ }
+ }
+
+ /**
+ * Sets a new request for the provider.
+ */
+ abstract void setRequest(@NonNull LocationTimeZoneProviderRequest request);
+
+ /**
+ * Handles a {@link LocationTimeZoneEvent} from a remote process.
+ */
+ final void handleLocationTimeZoneEvent(
+ @NonNull LocationTimeZoneEvent locationTimeZoneEvent) {
+ // These calls are invoked on a binder thread. Move to the mThreadingDomain thread as
+ // required by the guarantees for this class.
+ mThreadingDomain.post(() -> mListener.onReportLocationTimeZoneEvent(locationTimeZoneEvent));
+ }
+
+ /**
+ * Interface for listening to location time zone providers. See {@link
+ * LocationTimeZoneProviderProxy} for threading guarantees.
+ */
+ interface Listener {
+
+ /**
+ * Called when a provider receives a {@link LocationTimeZoneEvent}.
+ */
+ void onReportLocationTimeZoneEvent(@NonNull LocationTimeZoneEvent locationTimeZoneEvent);
+
+ /**
+ * 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/NullLocationTimeZoneProvider.java b/services/core/java/com/android/server/location/timezone/NullLocationTimeZoneProvider.java
new file mode 100644
index 0000000000000..79e2b975089d1
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/NullLocationTimeZoneProvider.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.location.timezone;
+
+import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.IndentingPrintWriter;
+import android.util.Slog;
+
+/**
+ * A {@link LocationTimeZoneProvider} that provides minimal responses needed for the {@link
+ * LocationTimeZoneProviderController} to operate correctly when there is no "real" provider
+ * configured. 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 LocationTimeZoneProvider} implementation will be
+ * defaulted to a {@link NullLocationTimeZoneProvider}. The {@link NullLocationTimeZoneProvider}
+ * enters a {@link ProviderState#PROVIDER_STATE_PERM_FAILED} state immediately after being enabled
+ * for the first time and sends the appropriate event, which ensures the {@link
+ * LocationTimeZoneProviderController} won't expect any further {@link
+ * android.location.timezone.LocationTimeZoneEvent}s to come from it, and won't attempt to use it
+ * again.
+ */
+class NullLocationTimeZoneProvider extends LocationTimeZoneProvider {
+
+ private static final String TAG = "NullLocationTimeZoneProvider";
+
+ /** Creates the instance. */
+ NullLocationTimeZoneProvider(@NonNull ThreadingDomain threadingDomain,
+ @NonNull String providerName) {
+ super(threadingDomain, providerName);
+ }
+
+ @Override
+ void onInitialize() {
+ // No-op
+ }
+
+ @Override
+ void onEnable() {
+ // Report a failure (asynchronously using the mThreadingDomain thread to avoid recursion).
+ mThreadingDomain.post(()-> {
+ // Enter the perm-failed state.
+ ProviderState currentState = mCurrentState.get();
+ ProviderState failedState = currentState.newState(
+ PROVIDER_STATE_PERM_FAILED, null, null, "Stubbed provider");
+ setCurrentState(failedState, true);
+ });
+ }
+
+ @Override
+ void onDisable() {
+ // Ignored - NullLocationTimeZoneProvider is always permanently failed.
+ }
+
+ @Override
+ void logWarn(String msg) {
+ Slog.w(TAG, msg);
+ }
+
+ @Override
+ public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) {
+ synchronized (mSharedLock) {
+ ipw.println("{Stubbed LocationTimeZoneProvider}");
+ ipw.println("mProviderName=" + mProviderName);
+ ipw.println("mCurrentState=" + mCurrentState);
+ }
+ }
+
+ @Override
+ public String toString() {
+ synchronized (mSharedLock) {
+ return "NullLocationTimeZoneProvider{"
+ + "mProviderName='" + mProviderName + '\''
+ + "mCurrentState='" + mCurrentState + '\''
+ + '}';
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/location/timezone/SimulatedBinderProviderEvent.java b/services/core/java/com/android/server/location/timezone/SimulatedBinderProviderEvent.java
new file mode 100644
index 0000000000000..ef2e349fceaab
--- /dev/null
+++ b/services/core/java/com/android/server/location/timezone/SimulatedBinderProviderEvent.java
@@ -0,0 +1,162 @@
+/*
+ * 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.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_PERMANENT_FAILURE;
+import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_SUCCESS;
+import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_UNCERTAIN;
+
+import static com.android.server.location.timezone.LocationTimeZoneManagerService.PRIMARY_PROVIDER_NAME;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.location.timezone.LocationTimeZoneEvent;
+import android.os.ShellCommand;
+import android.os.SystemClock;
+import android.os.UserHandle;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * An event used for simulating real binder proxy behavior using a {@link
+ * SimulatedLocationTimeZoneProviderProxy}.
+ */
+final class SimulatedBinderProviderEvent {
+
+ private static final List VALID_PROVIDER_NAMES = Arrays.asList(PRIMARY_PROVIDER_NAME);
+
+ static final int INJECTED_EVENT_TYPE_ON_BIND = 1;
+ static final int INJECTED_EVENT_TYPE_ON_UNBIND = 2;
+ static final int INJECTED_EVENT_TYPE_LOCATION_TIME_ZONE_EVENT = 3;
+
+
+ @NonNull private final String mProviderName;
+ private final int mEventType;
+ @Nullable private final LocationTimeZoneEvent mLocationTimeZoneEvent;
+
+ private SimulatedBinderProviderEvent(@NonNull String providerName, int eventType,
+ @Nullable LocationTimeZoneEvent locationTimeZoneEvent) {
+ this.mProviderName = Objects.requireNonNull(providerName);
+ this.mEventType = eventType;
+ this.mLocationTimeZoneEvent = locationTimeZoneEvent;
+ }
+
+ @NonNull
+ String getProviderName() {
+ return mProviderName;
+ }
+
+ @Nullable
+ LocationTimeZoneEvent getLocationTimeZoneEvent() {
+ return mLocationTimeZoneEvent;
+ }
+
+ int getEventType() {
+ return mEventType;
+ }
+
+ /** Prints the command line options that {@link #createFromArgs(ShellCommand)} understands. */
+ static void printCommandLineOpts(PrintWriter pw) {
+ pw.println("Simulated provider binder event:");
+ pw.println();
+ pw.println(" [onBind|onUnbind|locationTimeZoneEvent"
+ + " ]");
+ pw.println();
+ pw.println(" = " + VALID_PROVIDER_NAMES);
+ pw.println(" ="
+ + " [PERMANENT_FAILURE|UNCERTAIN|SUCCESS