Merge "Initial version of LocationTimeZoneManagerService"
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 + '\''
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user