Merge "Initial version of LocationTimeZoneManagerService"

This commit is contained in:
Neil Fuller
2020-09-18 17:43:50 +00:00
committed by Android (Google) Code Review
22 changed files with 3492 additions and 5 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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.
*
* <p>Providers are expected to report their uncertainty as soon as they become uncertain, as
* this enables the most flexibility for the controller to enable other providers when there are
* multiple ones. The controller is therefore responsible for deciding when to make a
* "uncertain" suggestion.
*
* <p>This method schedules an "uncertain" suggestion (if one isn't already scheduled) to be
* made later if nothing else preempts it. It can be preempted if the provider becomes certain
* (or does anything else that calls {@link #suggestImmediate(GeolocationTimeZoneSuggestion)})
* within 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);
}
}

View File

@@ -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.
*
* <p>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);
}
}

View File

@@ -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.
*
* <p>Implementation details:
*
* <p>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.
*
* <p>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.<provider name> 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.
*
* <p>Most operations are executed on the associated handler thread <em>but not all</em>, 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);
}
}
}

View File

@@ -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(" <simulated provider binder event>");
pw.println();
SimulatedBinderProviderEvent.printCommandLineOpts(pw);
pw.println();
}
}

View File

@@ -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.
*
* <p>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.
*
* <p>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<ProviderState> 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);
}
}
}

View File

@@ -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.
*
* <p>The controller interacts with the following components:
* <ul>
* <li>The surrounding service, which calls {@link #initialize(Environment, Callback)} and
* {@link #onConfigChanged()}.</li>
* <li>The {@link Environment} through which obtains information it needs.</li>
* <li>The {@link Callback} through which it makes time zone suggestions.</li>
* <li>Any {@link LocationTimeZoneProvider} instances it owns, which communicate via the
* {@link LocationTimeZoneProvider.ProviderListener#onProviderStateChange(ProviderState)}
* method.</li>
* </ul>
*
* <p>All incoming calls except for {@link
* LocationTimeZoneProviderController#dump(android.util.IndentingPrintWriter, String[])} must be
* made on the {@link Handler} thread of the {@link ThreadingDomain} passed to {@link
* #LocationTimeZoneProviderController(ThreadingDomain)}.
*
* <p>Provider / controller integration notes:
*
* <p>Providers distinguish between "unknown unknowns" ("uncertain") and "known unknowns"
* ("certain"), i.e. a provider can be uncertain and not know what the time zone is, which is
* different from the certainty that there are no time zone IDs for the current location. A provider
* can be certain about there being no time zone IDs for a location for good reason, e.g. for
* disputed areas and oceans. Distinguishing uncertainty allows the controller to try other
* providers (or give up), where as certainty means it should not.
*
* <p>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);
}
}

View File

@@ -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.
*
* <p>Threading guarantees provided / required by this interface:
* <ul>
* <li>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[])}</li>
* <li>Non-static public methods that make binder calls to remote processes (e.g.
* {@link #setRequest(LocationTimeZoneProviderRequest)}) are executed asynchronously and will
* return immediately.</li>
* <li>Callbacks received via binder are delivered via {@link Listener} are delivered on the
* {@link Handler} thread from the {@link ThreadingDomain} passed to the constructor.
* </ul>
*
* <p>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();
}
}

View File

@@ -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.
*
* <p>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 + '\''
+ '}';
}
}
}

View File

@@ -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<String> 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("<provider name> [onBind|onUnbind|locationTimeZoneEvent"
+ " <location time zone event args>]");
pw.println();
pw.println("<provider name> = " + VALID_PROVIDER_NAMES);
pw.println("<location time zone event args> ="
+ " [PERMANENT_FAILURE|UNCERTAIN|SUCCESS <time zone ids>*]");
}
/**
* Constructs a {@link SimulatedBinderProviderEvent} from the arguments of {@code shellCommand}.
*/
static SimulatedBinderProviderEvent createFromArgs(ShellCommand shellCommand) {
String providerName = shellCommand.getNextArgRequired();
if (!VALID_PROVIDER_NAMES.contains(providerName)) {
throw new IllegalArgumentException("Unknown provider name=" + providerName);
}
String injectedEvent = shellCommand.getNextArgRequired();
switch (injectedEvent) {
case "onBind": {
return new SimulatedBinderProviderEvent(
providerName, INJECTED_EVENT_TYPE_ON_BIND, null);
}
case "onUnbind": {
return new SimulatedBinderProviderEvent(
providerName, INJECTED_EVENT_TYPE_ON_UNBIND, null);
}
case "locationTimeZoneEvent": {
LocationTimeZoneEvent event = parseLocationTimeZoneEventArgs(shellCommand);
return new SimulatedBinderProviderEvent(providerName,
INJECTED_EVENT_TYPE_LOCATION_TIME_ZONE_EVENT, event);
}
default: {
throw new IllegalArgumentException("Unknown simulated event type=" + injectedEvent);
}
}
}
private static LocationTimeZoneEvent parseLocationTimeZoneEventArgs(ShellCommand shellCommand) {
LocationTimeZoneEvent.Builder eventBuilder = new LocationTimeZoneEvent.Builder()
.setElapsedRealtimeNanos(SystemClock.elapsedRealtime())
.setUserHandle(UserHandle.of(ActivityManager.getCurrentUser()));
String eventTypeString = shellCommand.getNextArgRequired();
switch (eventTypeString.toUpperCase()) {
case "PERMANENT_FAILURE": {
eventBuilder.setEventType(EVENT_TYPE_PERMANENT_FAILURE);
break;
}
case "UNCERTAIN": {
eventBuilder.setEventType(EVENT_TYPE_UNCERTAIN);
break;
}
case "SUCCESS": {
eventBuilder.setEventType(EVENT_TYPE_SUCCESS)
.setTimeZoneIds(parseTimeZoneArgs(shellCommand));
break;
}
default: {
throw new IllegalArgumentException("Error: Unknown eventType: " + eventTypeString);
}
}
return eventBuilder.build();
}
private static List<String> parseTimeZoneArgs(ShellCommand shellCommand) {
List<String> timeZoneIds = new ArrayList<>();
String timeZoneId;
while ((timeZoneId = shellCommand.getNextArg()) != null) {
timeZoneIds.add(timeZoneId);
}
return timeZoneIds;
}
@Override
public String toString() {
return "SimulatedBinderProviderEvent{"
+ "mProviderName=" + mProviderName
+ ", mEventType=" + mEventType
+ ", mLocationTimeZoneEvent=" + mLocationTimeZoneEvent
+ '}';
}
}

View File

@@ -0,0 +1,118 @@
/*
* 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.SimulatedBinderProviderEvent.INJECTED_EVENT_TYPE_LOCATION_TIME_ZONE_EVENT;
import static com.android.server.location.timezone.SimulatedBinderProviderEvent.INJECTED_EVENT_TYPE_ON_BIND;
import static com.android.server.location.timezone.SimulatedBinderProviderEvent.INJECTED_EVENT_TYPE_ON_UNBIND;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.util.IndentingPrintWriter;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.location.timezone.LocationTimeZoneProviderRequest;
import com.android.server.timezonedetector.ReferenceWithHistory;
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("mProxyLock")
@NonNull private LocationTimeZoneProviderRequest mRequest;
@NonNull private ReferenceWithHistory<String> mLastEvent = new ReferenceWithHistory<>(50);
SimulatedLocationTimeZoneProviderProxy(
@NonNull Context context, @NonNull ThreadingDomain threadingDomain) {
super(context, threadingDomain);
mRequest = LocationTimeZoneProviderRequest.EMPTY_REQUEST;
}
void simulate(@NonNull SimulatedBinderProviderEvent event) {
switch (event.getEventType()) {
case INJECTED_EVENT_TYPE_ON_BIND: {
mLastEvent.set("Simulating onProviderBound(), event=" + event);
mThreadingDomain.post(this::onBindOnHandlerThread);
break;
}
case INJECTED_EVENT_TYPE_ON_UNBIND: {
mLastEvent.set("Simulating onProviderUnbound(), event=" + event);
mThreadingDomain.post(this::onUnbindOnHandlerThread);
break;
}
case INJECTED_EVENT_TYPE_LOCATION_TIME_ZONE_EVENT: {
if (!mRequest.getReportLocationTimeZone()) {
mLastEvent.set("Test event=" + event + " is testing an invalid case:"
+ " reporting is off. mRequest=" + mRequest);
}
mLastEvent.set("Simulating LocationTimeZoneEvent, event=" + event);
handleLocationTimeZoneEvent(event.getLocationTimeZoneEvent());
break;
}
default: {
mLastEvent.set("Unknown simulated event type. event=" + event);
throw new IllegalArgumentException("Unknown simulated event type. event=" + event);
}
}
}
private void onBindOnHandlerThread() {
mThreadingDomain.assertCurrentThread();
synchronized (mSharedLock) {
mListener.onProviderBound();
}
}
private void onUnbindOnHandlerThread() {
mThreadingDomain.assertCurrentThread();
synchronized (mSharedLock) {
mListener.onProviderUnbound();
}
}
@Override
final void setRequest(@NonNull LocationTimeZoneProviderRequest 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("mRequest=" + mRequest);
ipw.println("mLastEvent=" + mLastEvent);
ipw.increaseIndent();
ipw.println("Last event history:");
mLastEvent.dump(ipw);
ipw.decreaseIndent();
}
}
}

View File

@@ -0,0 +1,120 @@
/*
* 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.internal.util.Preconditions;
/**
* 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.
*
* <p>It is <em>not</em> 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.checkArgument(Thread.currentThread() == getThread());
}
/**
* Execute the supplied runnable on the threading domain's thread.
*/
abstract void post(@NonNull Runnable runnable);
/**
* Execute the supplied runnable on the threading domain's thread with a delay.
*/
abstract void postDelayed(@NonNull Runnable runnable, long delayMillis);
abstract void postDelayed(Runnable r, Object token, 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 on the handler, i.e. calling any
* of the methods will cancel the execution of any previously queued / delayed runnable. All
* methods must be called from the {@link ThreadingDomain}'s thread.
*/
final class SingleRunnableQueue {
/**
* Runs the supplied {@link Runnable} synchronously on the threading domain's thread,
* cancelling any queued but not-yet-executed {@link Runnable} previously added by this.
* This method must be called from the threading domain's thread.
*/
void runSynchronously(Runnable r) {
cancel();
r.run();
}
/**
* 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, long delayMillis) {
cancel();
ThreadingDomain.this.postDelayed(r, this, delayMillis);
}
/**
* Cancels any queued but not-yet-executed {@link Runnable} previously added by this.
*/
public void cancel() {
assertCurrentThread();
removeQueuedRunnables(this);
}
}
}

View File

@@ -57,8 +57,12 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub
private static final String TAG = "TimeZoneDetectorService";
/** A compile time switch for enabling / disabling geolocation-based time zone detection. */
private static final boolean GEOLOCATION_TIME_ZONE_DETECTION_ENABLED = false;
/**
* A compile time constant "feature switch" for enabling / disabling location-based time zone
* detection on Android. If this is {@code false}, there should be few / little changes in
* behavior with previous releases and little overhead associated with geolocation components.
*/
public static final boolean GEOLOCATION_TIME_ZONE_DETECTION_ENABLED = false;
/**
* Handles the service lifecycle for {@link TimeZoneDetectorService} and

View File

@@ -293,6 +293,8 @@ public final class SystemServer {
"com.android.server.timedetector.TimeDetectorService$Lifecycle";
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";
private static final String ACCESSIBILITY_MANAGER_SERVICE_CLASS =
"com.android.server.accessibility.AccessibilityManagerService$Lifecycle";
private static final String ADB_SERVICE_CLASS =
@@ -1621,7 +1623,7 @@ public final class SystemServer {
try {
mSystemServiceManager.startService(TIME_DETECTOR_SERVICE_CLASS);
} catch (Throwable e) {
reportWtf("starting StartTimeDetectorService service", e);
reportWtf("starting TimeDetectorService service", e);
}
t.traceEnd();
@@ -1629,7 +1631,15 @@ public final class SystemServer {
try {
mSystemServiceManager.startService(TIME_ZONE_DETECTOR_SERVICE_CLASS);
} catch (Throwable e) {
reportWtf("starting StartTimeZoneDetectorService service", e);
reportWtf("starting TimeZoneDetectorService service", e);
}
t.traceEnd();
t.traceBegin("StartLocationTimeZoneManagerService");
try {
mSystemServiceManager.startService(LOCATION_TIME_ZONE_MANAGER_SERVICE_CLASS);
} catch (Throwable e) {
reportWtf("starting LocationTimeZoneManagerService service", e);
}
t.traceEnd();

View File

@@ -0,0 +1,528 @@
/*
* 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_SUCCESS;
import static android.location.timezone.LocationTimeZoneEvent.EVENT_TYPE_UNCERTAIN;
import static com.android.server.location.timezone.ControllerImpl.UNCERTAINTY_DELAY;
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.TestSupport.USER1_CONFIG_GEO_DETECTION_DISABLED;
import static com.android.server.location.timezone.TestSupport.USER1_CONFIG_GEO_DETECTION_ENABLED;
import static com.android.server.location.timezone.TestSupport.USER1_ID;
import static com.android.server.location.timezone.TestSupport.USER2_CONFIG_GEO_DETECTION_ENABLED;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static java.util.Arrays.asList;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.location.timezone.LocationTimeZoneEvent;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
import android.util.IndentingPrintWriter;
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.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 = 12345L;
private static final LocationTimeZoneEvent USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1 =
createLocationTimeZoneEvent(USER1_ID, EVENT_TYPE_SUCCESS, asList("Europe/London"));
private static final LocationTimeZoneEvent USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2 =
createLocationTimeZoneEvent(USER1_ID, EVENT_TYPE_SUCCESS, asList("Europe/Paris"));
private static final LocationTimeZoneEvent USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT =
createLocationTimeZoneEvent(USER1_ID, EVENT_TYPE_UNCERTAIN, null);
private TestThreadingDomain mTestThreadingDomain;
private TestCallback mTestCallback;
private TestLocationTimeZoneProvider mTestLocationTimeZoneProvider;
@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);
mTestLocationTimeZoneProvider =
new TestLocationTimeZoneProvider(mTestThreadingDomain, "primary");
}
@Test
public void initialState_enabled() {
ControllerImpl controllerImpl =
new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider);
TestEnvironment testEnvironment = new TestEnvironment(
mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED);
controllerImpl.initialize(testEnvironment, mTestCallback);
mTestLocationTimeZoneProvider.assertInitialized();
mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestThreadingDomain.assertNextQueueItemIsDelayed(UNCERTAINTY_DELAY);
mTestCallback.assertNoSuggestionMade();
}
@Test
public void initialState_disabled() {
ControllerImpl controllerImpl =
new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider);
TestEnvironment testEnvironment = new TestEnvironment(
mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_DISABLED);
controllerImpl.initialize(testEnvironment, mTestCallback);
mTestLocationTimeZoneProvider.assertInitialized();
mTestLocationTimeZoneProvider.assertIsDisabled();
mTestThreadingDomain.assertQueueEmpty();
mTestCallback.assertNoSuggestionMade();
}
@Test
public void enabled_uncertaintySuggestionSentIfNoEventReceived() {
ControllerImpl controllerImpl =
new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider);
TestEnvironment testEnvironment = new TestEnvironment(
mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED);
controllerImpl.initialize(testEnvironment, mTestCallback);
mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestCallback.assertNoSuggestionMade();
mTestThreadingDomain.assertSingleDelayedQueueItem(UNCERTAINTY_DELAY);
// Simulate time passing with no event being received.
mTestThreadingDomain.executeNext();
mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestCallback.assertUncertainSuggestionMadeAndCommit();
mTestThreadingDomain.assertQueueEmpty();
}
@Test
public void enabled_uncertaintySuggestionCancelledIfEventReceived() {
ControllerImpl controllerImpl =
new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider);
TestEnvironment testEnvironment = new TestEnvironment(
mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED);
controllerImpl.initialize(testEnvironment, mTestCallback);
mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestThreadingDomain.assertSingleDelayedQueueItem(UNCERTAINTY_DELAY);
mTestCallback.assertNoSuggestionMade();
// Simulate a location event being received by the provider. This should cause a suggestion
// to be made, and the timeout to be cleared.
mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent(
USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestThreadingDomain.assertQueueEmpty();
mTestCallback.assertSuggestionMadeAndCommit(
USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getTimeZoneIds());
}
@Test
public void enabled_repeatedCertainty() {
ControllerImpl controllerImpl =
new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider);
TestEnvironment testEnvironment = new TestEnvironment(
mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED);
controllerImpl.initialize(testEnvironment, mTestCallback);
mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestThreadingDomain.assertSingleDelayedQueueItem(UNCERTAINTY_DELAY);
mTestCallback.assertNoSuggestionMade();
// Simulate a location event being received by the provider. This should cause a suggestion
// to be made, and the timeout to be cleared.
mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent(
USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestThreadingDomain.assertQueueEmpty();
mTestCallback.assertSuggestionMadeAndCommit(
USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getTimeZoneIds());
// A second, identical event should not cause another suggestion.
mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent(
USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestThreadingDomain.assertQueueEmpty();
mTestCallback.assertNoSuggestionMade();
// And a third, different event should cause another suggestion.
mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent(
USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2);
mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestThreadingDomain.assertQueueEmpty();
mTestCallback.assertSuggestionMadeAndCommit(
USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2.getTimeZoneIds());
}
@Test
public void enabled_briefUncertaintyTriggersNoSuggestion() {
ControllerImpl controllerImpl =
new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider);
TestEnvironment testEnvironment = new TestEnvironment(
mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED);
controllerImpl.initialize(testEnvironment, mTestCallback);
mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestThreadingDomain.assertSingleDelayedQueueItem(UNCERTAINTY_DELAY);
mTestCallback.assertNoSuggestionMade();
// Simulate a location event being received by the provider. This should cause a suggestion
// to be made, and the timeout to be cleared.
mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent(
USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestThreadingDomain.assertQueueEmpty();
mTestCallback.assertSuggestionMadeAndCommit(
USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getTimeZoneIds());
// Uncertainty should cause
mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent(
USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT);
mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestThreadingDomain.assertSingleDelayedQueueItem(UNCERTAINTY_DELAY);
mTestCallback.assertNoSuggestionMade();
// And a third event should cause yet another suggestion.
mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent(
USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2);
mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestThreadingDomain.assertQueueEmpty();
mTestCallback.assertSuggestionMadeAndCommit(
USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2.getTimeZoneIds());
}
@Test
public void configChanges_enableAndDisable() {
ControllerImpl controllerImpl =
new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider);
TestEnvironment testEnvironment = new TestEnvironment(
mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_DISABLED);
controllerImpl.initialize(testEnvironment, mTestCallback);
mTestLocationTimeZoneProvider.assertIsDisabled();
mTestThreadingDomain.assertQueueEmpty();
mTestCallback.assertNoSuggestionMade();
// Now signal a config change so that geo detection is enabled.
testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestThreadingDomain.assertNextQueueItemIsDelayed(UNCERTAINTY_DELAY);
mTestCallback.assertNoSuggestionMade();
// Now signal a config change so that geo detection is disabled.
testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_DISABLED);
mTestLocationTimeZoneProvider.assertIsDisabled();
mTestThreadingDomain.assertQueueEmpty();
mTestCallback.assertNoSuggestionMade();
}
@Test
public void configChanges_disableWithPreviousSuggestion() {
ControllerImpl controllerImpl =
new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider);
TestEnvironment testEnvironment = new TestEnvironment(
mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED);
controllerImpl.initialize(testEnvironment, mTestCallback);
mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestThreadingDomain.assertSingleDelayedQueueItem(UNCERTAINTY_DELAY);
mTestCallback.assertNoSuggestionMade();
// Simulate a location event being received by the provider. This should cause a suggestion
// to be made, and the timeout to be cleared.
mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent(
USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestThreadingDomain.assertQueueEmpty();
mTestCallback.assertSuggestionMadeAndCommit(
USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getTimeZoneIds());
// Simulate the user disabling the provider.
testEnvironment.simulateConfigChange(USER1_CONFIG_GEO_DETECTION_DISABLED);
// Because there had been a previous suggestion, the controller should withdraw it
// immediately to let the downstream components know that the provider can no longer be sure
// of the time zone.
mTestLocationTimeZoneProvider.assertIsDisabled();
mTestThreadingDomain.assertQueueEmpty();
mTestCallback.assertSuggestionMadeAndCommit(null);
}
@Test
public void configChanges_userSwitch_enabledToEnabled() {
ControllerImpl controllerImpl =
new ControllerImpl(mTestThreadingDomain, mTestLocationTimeZoneProvider);
TestEnvironment testEnvironment = new TestEnvironment(
mTestThreadingDomain, controllerImpl, USER1_CONFIG_GEO_DETECTION_ENABLED);
controllerImpl.initialize(testEnvironment, mTestCallback);
// There should be a runnable scheduled to suggest uncertainty if no event is received.
mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestThreadingDomain.assertSingleDelayedQueueItem(UNCERTAINTY_DELAY);
mTestCallback.assertNoSuggestionMade();
// Have the provider suggest a time zone.
mTestLocationTimeZoneProvider.simulateLocationTimeZoneEvent(
USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
// Receiving a "success" provider event should cause a suggestion to be made synchronously,
// and also clear the scheduled uncertainty suggestion.
mTestLocationTimeZoneProvider.assertIsEnabled(USER1_CONFIG_GEO_DETECTION_ENABLED);
mTestThreadingDomain.assertQueueEmpty();
mTestCallback.assertSuggestionMadeAndCommit(
USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getTimeZoneIds());
// Simulate the user change (but geo detection still enabled).
testEnvironment.simulateConfigChange(USER2_CONFIG_GEO_DETECTION_ENABLED);
// We expect the provider to end up in PROVIDER_STATE_ENABLED, but it should have been
// disabled when the user changed.
// The controller should schedule a runnable to make a suggestion if the provider doesn't
// send a success event.
int[] expectedStateTransitions = { PROVIDER_STATE_DISABLED, PROVIDER_STATE_ENABLED };
mTestLocationTimeZoneProvider.assertStateChangesAndCommit(expectedStateTransitions);
mTestLocationTimeZoneProvider.assertConfig(USER2_CONFIG_GEO_DETECTION_ENABLED);
mTestThreadingDomain.assertSingleDelayedQueueItem(UNCERTAINTY_DELAY);
mTestCallback.assertNoSuggestionMade();
// Simulate no event being received, and time passing.
mTestThreadingDomain.executeNext();
mTestLocationTimeZoneProvider.assertIsEnabled(USER2_CONFIG_GEO_DETECTION_ENABLED);
mTestThreadingDomain.assertQueueEmpty();
mTestCallback.assertUncertainSuggestionMadeAndCommit();
}
private static LocationTimeZoneEvent createLocationTimeZoneEvent(@UserIdInt int userId,
int eventType, @Nullable List<String> timeZoneIds) {
LocationTimeZoneEvent.Builder builder = new LocationTimeZoneEvent.Builder()
.setElapsedRealtimeNanos(ARBITRARY_TIME)
.setUserHandle(UserHandle.of(userId))
.setEventType(eventType);
if (timeZoneIds != null) {
builder.setTimeZoneIds(timeZoneIds);
}
return builder.build();
}
private static class TestEnvironment extends LocationTimeZoneProviderController.Environment {
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
ConfigurationInternal getCurrentUserConfigurationInternal() {
return mConfigurationInternal;
}
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<GeolocationTimeZoneSuggestion> mLatestSuggestion = new TestState<>();
TestCallback(ThreadingDomain threadingDomain) {
super(threadingDomain);
}
@Override
void suggest(GeolocationTimeZoneSuggestion suggestion) {
mLatestSuggestion.set(suggestion);
}
void assertSuggestionMadeAndCommit(@Nullable List<String> 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<ProviderState> mTestProviderState = new TestState<>();
private boolean mInitialized;
/**
* Creates the instance.
*/
TestLocationTimeZoneProvider(ThreadingDomain threadingDomain, String providerName) {
super(threadingDomain, providerName);
}
@Override
void onInitialize() {
mInitialized = true;
}
@Override
void onSetCurrentState(ProviderState newState) {
mTestProviderState.set(newState);
}
@Override
void onEnable() {
// Nothing needed for tests.
}
@Override
void onDisable() {
// Nothing needed for tests.
}
@Override
void logWarn(String msg) {
System.out.println(msg);
}
@Override
public void dump(IndentingPrintWriter pw, String[] args) {
// Nothing needed for tests.
}
/** Asserts that {@link #initialize(ProviderListener)} has been called. */
void assertInitialized() {
assertTrue(mInitialized);
}
void assertIsDisabled() {
// Disabled providers don't hold config.
assertConfig(null);
assertIsEnabledAndCommit(false);
}
/**
* Asserts the provider's config matches the expected, and the current state is set
* accordinly. Commits the latest changes to the state.
*/
void assertIsEnabled(@NonNull ConfigurationInternal expectedConfig) {
assertConfig(expectedConfig);
boolean expectIsEnabled = expectedConfig.getAutoDetectionEnabledBehavior();
assertIsEnabledAndCommit(expectIsEnabled);
}
private void assertIsEnabledAndCommit(boolean enabled) {
ProviderState currentState = mCurrentState.get();
if (enabled) {
assertEquals(PROVIDER_STATE_ENABLED, currentState.stateEnum);
} else {
assertEquals(PROVIDER_STATE_DISABLED, currentState.stateEnum);
}
mTestProviderState.commitLatest();
}
void assertConfig(@NonNull ConfigurationInternal expectedConfig) {
ProviderState currentState = mCurrentState.get();
assertEquals(expectedConfig, currentState.currentUserConfiguration);
}
void simulateLocationTimeZoneEvent(@NonNull LocationTimeZoneEvent event) {
handleLocationTimeZoneEvent(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<ProviderState> 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();
}
}
}

View File

@@ -0,0 +1,208 @@
/*
* 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.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/** 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 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(ranOnExpectedThread.get());
}
@Test
public void postDelayed() throws Exception {
ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler);
long beforeExecutionNanos = System.nanoTime();
AtomicBoolean ranOnExpectedThread = new AtomicBoolean(false);
LatchedRunnable testLogic = new LatchedRunnable(() -> {
ranOnExpectedThread.set(Thread.currentThread() == mTestHandler.getLooper().getThread());
});
domain.postDelayed(testLogic, 5000);
testLogic.assertCompletesWithin(60, TimeUnit.SECONDS);
assertTrue(ranOnExpectedThread.get());
long afterExecutionNanos = System.nanoTime();
assertTrue(afterExecutionNanos - beforeExecutionNanos >= TimeUnit.SECONDS.toNanos(5));
}
@Test
public void singleRunnableHandler_runSynchronously() throws Exception {
ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler);
SingleRunnableQueue singleRunnableQueue = domain.createSingleRunnableQueue();
AtomicBoolean testPassed = new AtomicBoolean(false);
// Calls to SingleRunnableQueue must be made on the handler thread it is associated with,
// so this uses runWithScissors() to block until the lambda has completed.
mTestHandler.runWithScissors(() -> {
Thread testThread = Thread.currentThread();
CountDownLatch latch = new CountDownLatch(1);
singleRunnableQueue.runSynchronously(() -> {
assertSame(Thread.currentThread(), testThread);
latch.countDown();
});
assertTrue(awaitWithRuntimeException(latch, 60, TimeUnit.SECONDS));
testPassed.set(true);
}, TimeUnit.SECONDS.toMillis(60));
assertTrue(testPassed.get());
}
@Test
public void singleRunnableHandler_runDelayed() throws Exception {
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));
}
}
}

View File

@@ -0,0 +1,134 @@
/*
* 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_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 static com.android.server.location.timezone.TestSupport.USER1_CONFIG_GEO_DETECTION_ENABLED;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import android.platform.test.annotations.Presubmit;
import android.util.IndentingPrintWriter;
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;
/**
* Tests for {@link NullLocationTimeZoneProvider} and, indirectly, the class it extends
* {@link LocationTimeZoneProvider}.
*/
@Presubmit
public class NullLocationTimeZoneProviderTest {
private TestThreadingDomain mTestThreadingDomain;
private TestController mTestController;
@Before
public void setUp() {
mTestThreadingDomain = new TestThreadingDomain();
mTestController = new TestController(mTestThreadingDomain);
}
@Test
public void initialization() {
String providerName = "primary";
NullLocationTimeZoneProvider provider =
new NullLocationTimeZoneProvider(mTestThreadingDomain, providerName);
provider.initialize(providerState -> mTestController.onProviderStateChange(providerState));
ProviderState currentState = provider.getCurrentState();
assertEquals(PROVIDER_STATE_DISABLED, currentState.stateEnum);
assertNull(currentState.currentUserConfiguration);
assertSame(provider, currentState.provider);
mTestThreadingDomain.assertQueueEmpty();
}
@Test
public void enableSchedulesPermFailure() {
String providerName = "primary";
NullLocationTimeZoneProvider provider =
new NullLocationTimeZoneProvider(mTestThreadingDomain, providerName);
provider.initialize(providerState -> mTestController.onProviderStateChange(providerState));
ConfigurationInternal config = USER1_CONFIG_GEO_DETECTION_ENABLED;
provider.enable(config);
// The StubbedProvider should enters enabled state, but immediately schedule a runnable to
// switch to perm failure.
ProviderState currentState = provider.getCurrentState();
assertSame(provider, currentState.provider);
assertEquals(PROVIDER_STATE_ENABLED, currentState.stateEnum);
assertEquals(config, currentState.currentUserConfiguration);
mTestThreadingDomain.assertSingleImmediateQueueItem();
// Entering enabled() does not trigger an onProviderStateChanged() as it is requested by the
// controller.
mTestController.assertProviderChangeNotTriggered();
// Check the queued runnable causes the provider to go into perm failed state.
mTestThreadingDomain.executeNext();
// Entering perm failed triggers an onProviderStateChanged() as it is asynchronously
// triggered.
mTestController.assertProviderChangeTriggered(PROVIDER_STATE_PERM_FAILED);
}
/** A test stand-in for the {@link LocationTimeZoneProviderController}. */
private static class TestController extends LocationTimeZoneProviderController {
private TestState<ProviderState> mProviderState = new TestState<>();
TestController(ThreadingDomain threadingDomain) {
super(threadingDomain);
}
@Override
void initialize(Environment environment, Callback callback) {
// Not needed for provider testing.
}
@Override
void onConfigChanged() {
// Not needed for provider testing.
}
void onProviderStateChange(ProviderState providerState) {
this.mProviderState.set(providerState);
}
@Override
public void dump(IndentingPrintWriter pw, String[] args) {
// Not needed for provider testing.
}
void assertProviderChangeTriggered(int expectedStateEnum) {
assertEquals(expectedStateEnum, mProviderState.getLatest().stateEnum);
mProviderState.commitLatest();
}
public void assertProviderChangeNotTriggered() {
mProviderState.assertHasNotBeenSet();
}
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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)
.setAutoDetectionEnabled(true)
.setLocationEnabled(true)
.setGeoDetectionEnabled(geoDetectionEnabled)
.build();
}
}

View File

@@ -0,0 +1,128 @@
/*
* 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.assertTrue;
import android.annotation.NonNull;
import android.annotation.Nullable;
import java.time.Duration;
import java.util.LinkedList;
import java.util.Objects;
/**
* 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 {
@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 LinkedList<QueuedRunnable> mQueue = new LinkedList<>();
TestThreadingDomain() {
// Pick an arbitrary time.
mCurrentTimeMillis = 123456L;
}
@Override
Thread getThread() {
return Thread.currentThread();
}
@Override
void post(Runnable r) {
mQueue.add(new QueuedRunnable(r, null, mCurrentTimeMillis));
}
@Override
void postDelayed(Runnable r, long delayMillis) {
mQueue.add(new QueuedRunnable(r, null, mCurrentTimeMillis + delayMillis));
}
@Override
void postDelayed(Runnable r, Object token, long delayMillis) {
mQueue.add(new QueuedRunnable(r, token, mCurrentTimeMillis + delayMillis));
}
@Override
void removeQueuedRunnables(Object token) {
mQueue.removeIf(runnable -> runnable.token != null && runnable.token.equals(token));
}
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);
}
void assertNextQueueItemIsDelayed(Duration expectedDelay) {
assertTrue(getNextQueueItemDelayMillis() == expectedDelay.toMillis());
}
void assertQueueEmpty() {
assertTrue(mQueue.isEmpty());
}
long getNextQueueItemDelayMillis() {
assertQueueLength(1);
return mQueue.getFirst().executionTimeMillis - mCurrentTimeMillis;
}
void executeNext() {
assertQueueLength(1);
QueuedRunnable queued = mQueue.removeFirst();
mCurrentTimeMillis = queued.executionTimeMillis;
queued.runnable.run();
}
}

View File

@@ -76,7 +76,7 @@ public class TestState<T> {
/** Asserts the number of times {@link #set} has been called. */
public void assertChangeCount(int expectedCount) {
assertEquals(expectedCount, mValues.size());
assertEquals(expectedCount, getChangeCount());
}
/**
@@ -89,4 +89,25 @@ public class TestState<T> {
}
return mInitialValue;
}
/** Returns the number of times {@link #set} has been called. */
public int getChangeCount() {
return mValues.size();
}
/**
* Returns an historic value of the state. Values for {@code age} can be from {@code 0}, the
* latest value, through {@code getChangeCount() - 1}, which returns the oldest change, to
* {@code getChangeCount()}, which returns the initial value. Values outside of this range will
* cause {@link IndexOutOfBoundsException} to be thrown.
*/
public T getPrevious(int age) {
int size = mValues.size();
if (age < size) {
return mValues.get(size - 1 - age);
} else if (age == size) {
return mInitialValue;
}
throw new IndexOutOfBoundsException("age=" + age + " is too big.");
}
}