Merge "Add start / stop to location_time_zone_manager"

This commit is contained in:
Neil Fuller
2021-01-07 10:25:27 +00:00
committed by Android (Google) Code Review
22 changed files with 804 additions and 321 deletions

View File

@@ -17,6 +17,7 @@
package com.android.server.location.timezone;
import static com.android.server.location.timezone.LocationTimeZoneManagerService.debugLog;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_CERTAIN;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING;
@@ -71,6 +72,11 @@ class BinderLocationTimeZoneProvider extends LocationTimeZoneProvider {
});
}
@Override
void onDestroy() {
mProxy.destroy();
}
private void handleProviderLost(String reason) {
mThreadingDomain.assertCurrentThread();
@@ -100,11 +106,12 @@ class BinderLocationTimeZoneProvider extends LocationTimeZoneProvider {
+ ": No state change required, provider is stopped.");
break;
}
case PROVIDER_STATE_PERM_FAILED: {
case PROVIDER_STATE_PERM_FAILED:
case PROVIDER_STATE_DESTROYED: {
debugLog("handleProviderLost reason=" + reason
+ ", mProviderName=" + mProviderName
+ ", currentState=" + currentState
+ ": No state change required, provider is perm failed.");
+ ": No state change required, provider is terminated.");
break;
}
default: {
@@ -132,11 +139,12 @@ class BinderLocationTimeZoneProvider extends LocationTimeZoneProvider {
+ ", currentState=" + currentState + ": Provider is stopped.");
break;
}
case PROVIDER_STATE_PERM_FAILED: {
case PROVIDER_STATE_PERM_FAILED:
case PROVIDER_STATE_DESTROYED: {
debugLog("handleOnProviderBound"
+ ", mProviderName=" + mProviderName
+ ", currentState=" + currentState
+ ": No state change required, provider is perm failed.");
+ ": No state change required, provider is terminated.");
break;
}
default: {

View File

@@ -19,6 +19,7 @@ package com.android.server.location.timezone;
import android.annotation.NonNull;
import com.android.server.LocalServices;
import com.android.server.timezonedetector.ConfigurationChangeListener;
import com.android.server.timezonedetector.ConfigurationInternal;
import com.android.server.timezonedetector.TimeZoneDetectorInternal;
@@ -37,6 +38,7 @@ class ControllerEnvironmentImpl extends LocationTimeZoneProviderController.Envir
@NonNull private final TimeZoneDetectorInternal mTimeZoneDetectorInternal;
@NonNull private final LocationTimeZoneProviderController mController;
@NonNull private final ConfigurationChangeListener mConfigurationChangeListener;
ControllerEnvironmentImpl(@NonNull ThreadingDomain threadingDomain,
@NonNull LocationTimeZoneProviderController controller) {
@@ -45,8 +47,14 @@ class ControllerEnvironmentImpl extends LocationTimeZoneProviderController.Envir
mTimeZoneDetectorInternal = LocalServices.getService(TimeZoneDetectorInternal.class);
// Listen for configuration changes.
mTimeZoneDetectorInternal.addConfigurationListener(
() -> mThreadingDomain.post(mController::onConfigChanged));
mConfigurationChangeListener = () -> mThreadingDomain.post(mController::onConfigChanged);
mTimeZoneDetectorInternal.addConfigurationListener(mConfigurationChangeListener);
}
@Override
void destroy() {
mTimeZoneDetectorInternal.removeConfigurationListener(mConfigurationChangeListener);
}
@Override

View File

@@ -19,6 +19,7 @@ package com.android.server.location.timezone;
import static com.android.server.location.timezone.LocationTimeZoneManagerService.debugLog;
import static com.android.server.location.timezone.LocationTimeZoneManagerService.warnLog;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_CERTAIN;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING;
@@ -151,6 +152,23 @@ class ControllerImpl extends LocationTimeZoneProviderController {
return mUncertaintyTimeoutQueue.getQueuedDelayMillis();
}
@Override
void destroy() {
mThreadingDomain.assertCurrentThread();
synchronized (mSharedLock) {
stopProviders();
mPrimaryProvider.destroy();
mSecondaryProvider.destroy();
// If the controller has made a "certain" suggestion, it should make an uncertain
// suggestion to cancel it.
if (mLastSuggestion != null && mLastSuggestion.getZoneIds() != null) {
makeSuggestion(createUncertainSuggestion("Controller is destroyed"));
}
}
}
@GuardedBy("mSharedLock")
private void stopProviders() {
stopProviderIfStarted(mPrimaryProvider);
@@ -182,8 +200,9 @@ class ControllerImpl extends LocationTimeZoneProviderController {
provider.stopUpdates();
break;
}
case PROVIDER_STATE_PERM_FAILED: {
debugLog("Unable to stop " + provider + ": it is perm failed");
case PROVIDER_STATE_PERM_FAILED:
case PROVIDER_STATE_DESTROYED: {
debugLog("Unable to stop " + provider + ": it is terminated.");
break;
}
default: {
@@ -285,8 +304,9 @@ class ControllerImpl extends LocationTimeZoneProviderController {
debugLog("No need to start " + provider + ": already started");
break;
}
case PROVIDER_STATE_PERM_FAILED: {
debugLog("Unable to start " + provider + ": it is perm failed");
case PROVIDER_STATE_PERM_FAILED:
case PROVIDER_STATE_DESTROYED: {
debugLog("Unable to start " + provider + ": it is terminated");
break;
}
default: {
@@ -303,17 +323,20 @@ class ControllerImpl extends LocationTimeZoneProviderController {
synchronized (mSharedLock) {
switch (providerState.stateEnum) {
case PROVIDER_STATE_STOPPED: {
// This should never happen: entering stopped does not trigger a state change.
warnLog("onProviderStateChange: Unexpected state change for stopped provider,"
case PROVIDER_STATE_STARTED_INITIALIZING:
case PROVIDER_STATE_STOPPED:
case PROVIDER_STATE_DESTROYED: {
// This should never happen: entering initializing, stopped or destroyed are
// triggered by the controller so and should not trigger a state change
// callback.
warnLog("onProviderStateChange: Unexpected state change for provider,"
+ " provider=" + provider);
break;
}
case PROVIDER_STATE_STARTED_INITIALIZING:
case PROVIDER_STATE_STARTED_CERTAIN:
case PROVIDER_STATE_STARTED_UNCERTAIN: {
// Entering started does not trigger a state change, so this only happens if an
// event is received while the provider is started.
// These are valid and only happen if an event is received while the provider is
// started.
debugLog("onProviderStateChange: Received notification of a state change while"
+ " started, provider=" + provider);
handleProviderStartedStateChange(providerState);
@@ -349,17 +372,18 @@ class ControllerImpl extends LocationTimeZoneProviderController {
// If a provider has failed, the other may need to be started.
if (failedProvider == mPrimaryProvider) {
if (secondaryCurrentState.stateEnum != PROVIDER_STATE_PERM_FAILED) {
// The primary must have failed. Try to start the secondary. This does nothing if
// the provider is already started, and will leave the provider in
// {started initializing} if the provider is stopped.
if (!secondaryCurrentState.isTerminated()) {
// Try to start the secondary. This does nothing if the provider is already
// started, and will leave the provider in {started initializing} if the provider is
// stopped.
tryStartProvider(mSecondaryProvider, mCurrentUserConfiguration);
}
} else if (failedProvider == mSecondaryProvider) {
// No-op: The secondary will only be active if the primary is uncertain or is failed.
// So, there the primary should not need to be started when the secondary fails.
// No-op: The secondary will only be active if the primary is uncertain or is
// terminated. So, there the primary should not need to be started when the secondary
// fails.
if (primaryCurrentState.stateEnum != PROVIDER_STATE_STARTED_UNCERTAIN
&& primaryCurrentState.stateEnum != PROVIDER_STATE_PERM_FAILED) {
&& !primaryCurrentState.isTerminated()) {
warnLog("Secondary provider unexpected reported a failure:"
+ " failed provider=" + failedProvider.getName()
+ ", primary provider=" + mPrimaryProvider
@@ -367,19 +391,18 @@ class ControllerImpl extends LocationTimeZoneProviderController {
}
}
// If both providers are now failed, the controller needs to tell the next component in the
// time zone detection process.
if (primaryCurrentState.stateEnum == PROVIDER_STATE_PERM_FAILED
&& secondaryCurrentState.stateEnum == PROVIDER_STATE_PERM_FAILED) {
// If both providers are now terminated, the controller needs to tell the next component in
// the time zone detection process.
if (primaryCurrentState.isTerminated() && secondaryCurrentState.isTerminated()) {
// If both providers are newly failed then the controller is uncertain by definition
// If both providers are newly terminated then the controller is uncertain by definition
// and it will never recover so it can send a suggestion immediately.
cancelUncertaintyTimeout();
// If both providers are now failed, then a suggestion must be sent informing the time
// zone detector that there are no further updates coming in future.
// If both providers are now terminated, then a suggestion must be sent informing the
// time zone detector that there are no further updates coming in future.
GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion(
"Both providers are permanently failed:"
"Both providers are terminated:"
+ " primary=" + primaryCurrentState.provider
+ ", secondary=" + secondaryCurrentState.provider);
makeSuggestion(suggestion);

View File

@@ -21,6 +21,10 @@ import android.annotation.NonNull;
import android.os.Handler;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* The real implementation of {@link ThreadingDomain} that uses a {@link Handler}.
@@ -57,6 +61,38 @@ final class HandlerThreadingDomain extends ThreadingDomain {
getHandler().post(r);
}
@Override
<V> V postAndWait(@NonNull Callable<V> callable, @DurationMillisLong long durationMillis)
throws Exception {
// Calling this on this domain's thread would lead to deadlock.
assertNotCurrentThread();
AtomicReference<V> resultReference = new AtomicReference<>();
AtomicReference<Exception> exceptionReference = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
post(() -> {
try {
resultReference.set(callable.call());
} catch (Exception e) {
exceptionReference.set(e);
} finally {
latch.countDown();
}
});
try {
if (!latch.await(durationMillis, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Timed out");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (exceptionReference.get() != null) {
throw exceptionReference.get();
}
return resultReference.get();
}
@Override
void postDelayed(@NonNull Runnable r, @DurationMillisLong long delayMillis) {
getHandler().postDelayed(r, delayMillis);

View File

@@ -153,10 +153,14 @@ public class LocationTimeZoneManagerService extends Binder {
/** The shared lock from {@link #mThreadingDomain}. */
@NonNull private final Object mSharedLock;
// Lazily initialized. Non-null and effectively final after onSystemThirdPartyAppsCanStart().
// Lazily initialized. Can be null if the service has been stopped.
@GuardedBy("mSharedLock")
private ControllerImpl mLocationTimeZoneDetectorController;
// Lazily initialized. Can be null if the service has been stopped.
@GuardedBy("mSharedLock")
private ControllerEnvironmentImpl mEnvironment;
LocationTimeZoneManagerService(Context context) {
mContext = context.createAttributionContext(ATTRIBUTION_TAG);
mHandler = FgThread.getHandler();
@@ -178,31 +182,59 @@ public class LocationTimeZoneManagerService extends Binder {
}
void onSystemThirdPartyAppsCanStart() {
// Called on an arbitrary thread during initialization.
synchronized (mSharedLock) {
LocationTimeZoneProvider primary = createPrimaryProvider();
LocationTimeZoneProvider secondary = createSecondaryProvider();
mLocationTimeZoneDetectorController =
new ControllerImpl(mThreadingDomain, primary, secondary);
ControllerCallbackImpl callback = new ControllerCallbackImpl(mThreadingDomain);
ControllerEnvironmentImpl environment = new ControllerEnvironmentImpl(
mThreadingDomain, mLocationTimeZoneDetectorController);
// Called on an arbitrary thread during initialization. We do not want to wait for
// completion as it would delay boot.
final boolean waitForCompletion = false;
startInternal(waitForCompletion);
}
// 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));
/**
* Starts the service during server initialization or during tests after a call to
* {@link #stop()}.
*/
void start() {
enforceManageTimeZoneDetectorPermission();
final boolean waitForCompletion = true;
startInternal(waitForCompletion);
}
/**
* Starts the service during server initialization or during tests after a call to
* {@link #stop()}.
*
* <p>To avoid tests needing to sleep, when {@code waitForCompletion} is {@code true}, this
* method will not return until all the system server components have started.
*/
private void startInternal(boolean waitForCompletion) {
Runnable runnable = () -> {
synchronized (mSharedLock) {
if (mLocationTimeZoneDetectorController == null) {
LocationTimeZoneProvider primary = createPrimaryProvider();
LocationTimeZoneProvider secondary = createSecondaryProvider();
mLocationTimeZoneDetectorController =
new ControllerImpl(mThreadingDomain, primary, secondary);
ControllerCallbackImpl callback = new ControllerCallbackImpl(
mThreadingDomain);
mEnvironment = new ControllerEnvironmentImpl(
mThreadingDomain, mLocationTimeZoneDetectorController);
mLocationTimeZoneDetectorController.initialize(mEnvironment, callback);
}
}
};
if (waitForCompletion) {
mThreadingDomain.postAndWait(runnable, BLOCKING_OP_WAIT_DURATION_MILLIS);
} else {
mThreadingDomain.post(runnable);
}
}
private LocationTimeZoneProvider createPrimaryProvider() {
if (isDisabled(PRIMARY_PROVIDER_NAME)) {
return new NullLocationTimeZoneProvider(mThreadingDomain, PRIMARY_PROVIDER_NAME);
}
LocationTimeZoneProviderProxy proxy;
if (isInSimulationMode(PRIMARY_PROVIDER_NAME)) {
proxy = new SimulatedLocationTimeZoneProviderProxy(mContext, mThreadingDomain);
} else if (isDisabled(PRIMARY_PROVIDER_NAME)) {
proxy = new NullLocationTimeZoneProviderProxy(mContext, mThreadingDomain);
} else {
proxy = new RealLocationTimeZoneProviderProxy(
mContext,
@@ -217,13 +249,11 @@ public class LocationTimeZoneManagerService extends Binder {
}
private LocationTimeZoneProvider createSecondaryProvider() {
if (isDisabled(SECONDARY_PROVIDER_NAME)) {
return new NullLocationTimeZoneProvider(mThreadingDomain, SECONDARY_PROVIDER_NAME);
}
LocationTimeZoneProviderProxy proxy;
if (isInSimulationMode(SECONDARY_PROVIDER_NAME)) {
proxy = new SimulatedLocationTimeZoneProviderProxy(mContext, mThreadingDomain);
} else if (isDisabled(SECONDARY_PROVIDER_NAME)) {
proxy = new NullLocationTimeZoneProviderProxy(mContext, mThreadingDomain);
} else {
proxy = new RealLocationTimeZoneProviderProxy(
mContext,
@@ -274,6 +304,25 @@ public class LocationTimeZoneManagerService extends Binder {
return Objects.equals(systemPropertyProviderMode, mode);
}
/**
* Stops the service for tests. To avoid tests needing to sleep, this method will not return
* until all the system server components have stopped.
*/
void stop() {
enforceManageTimeZoneDetectorPermission();
mThreadingDomain.postAndWait(() -> {
synchronized (mSharedLock) {
if (mLocationTimeZoneDetectorController != null) {
mLocationTimeZoneDetectorController.destroy();
mLocationTimeZoneDetectorController = null;
mEnvironment.destroy();
mEnvironment = null;
}
}
}, BLOCKING_OP_WAIT_DURATION_MILLIS);
}
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out,
FileDescriptor err, String[] args, ShellCallback callback,
@@ -335,7 +384,7 @@ public class LocationTimeZoneManagerService extends Binder {
ipw.println("LocationTimeZoneManagerService:");
ipw.increaseIndent();
if (mLocationTimeZoneDetectorController == null) {
ipw.println("{Uninitialized}");
ipw.println("{Stopped}");
} else {
mLocationTimeZoneDetectorController.dump(ipw, args);
}

View File

@@ -32,6 +32,8 @@ class LocationTimeZoneManagerShellCommand extends ShellCommand {
private static final List<String> VALID_PROVIDER_NAMES =
Arrays.asList(PRIMARY_PROVIDER_NAME, SECONDARY_PROVIDER_NAME);
private static final String CMD_START = "start";
private static final String CMD_STOP = "stop";
private static final String CMD_SEND_PROVIDER_TEST_COMMAND = "send_provider_test_command";
private final LocationTimeZoneManagerService mService;
@@ -47,6 +49,12 @@ class LocationTimeZoneManagerShellCommand extends ShellCommand {
}
switch (cmd) {
case CMD_START: {
return runStart();
}
case CMD_STOP: {
return runStop();
}
case CMD_SEND_PROVIDER_TEST_COMMAND: {
return runSendProviderTestCommand();
}
@@ -62,7 +70,12 @@ class LocationTimeZoneManagerShellCommand extends ShellCommand {
pw.println("Location Time Zone Manager (location_time_zone_manager) commands:");
pw.println(" help");
pw.println(" Print this help text.");
pw.printf(" %s <provider name> <test command>\n", CMD_SEND_PROVIDER_TEST_COMMAND);
pw.printf(" %s\n", CMD_START);
pw.println(" Starts the location_time_zone_manager, creating time zone providers.");
pw.printf(" %s\n", CMD_STOP);
pw.println(" Stops the location_time_zone_manager, destroying time zone providers.");
pw.printf(" %s <provider name> <test command>\n",
CMD_SEND_PROVIDER_TEST_COMMAND);
pw.println(" Passes a test command to the named provider.");
pw.println();
pw.printf("%s details:\n", CMD_SEND_PROVIDER_TEST_COMMAND);
@@ -91,6 +104,30 @@ class LocationTimeZoneManagerShellCommand extends ShellCommand {
pw.println();
}
private int runStart() {
try {
mService.start();
} catch (RuntimeException e) {
reportError(e);
return 1;
}
PrintWriter outPrintWriter = getOutPrintWriter();
outPrintWriter.println("Service started");
return 0;
}
private int runStop() {
try {
mService.stop();
} catch (RuntimeException e) {
reportError(e);
return 1;
}
PrintWriter outPrintWriter = getOutPrintWriter();
outPrintWriter.println("Service stopped");
return 0;
}
private int runSendProviderTestCommand() {
PrintWriter outPrintWriter = getOutPrintWriter();

View File

@@ -21,6 +21,7 @@ import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESU
import static com.android.server.location.timezone.LocationTimeZoneManagerService.debugLog;
import static com.android.server.location.timezone.LocationTimeZoneManagerService.warnLog;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_CERTAIN;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING;
@@ -52,8 +53,7 @@ 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.
* zone provider. The provider implementation will typically have logic running in another process.
*
* <p>The provider is supplied with a {@link ProviderListener} via {@link
* #initialize(ProviderListener)}. This starts communication of asynchronous detection / error
@@ -61,6 +61,10 @@ import java.util.Objects;
* ProviderListener#onProviderStateChange} method. This call must be made on the
* {@link Handler} thread from the {@link ThreadingDomain} passed to the constructor.
*
* <p>This class is also responsible for monitoring the initialization timeout for a provider. i.e.
* if the provider fails to send its first suggestion within a certain time, this is the component
* responsible for generating the necessary "uncertain" event.
*
* <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.
@@ -86,7 +90,7 @@ abstract class LocationTimeZoneProvider implements Dumpable {
@IntDef(prefix = "PROVIDER_STATE_",
value = { PROVIDER_STATE_UNKNOWN, PROVIDER_STATE_STARTED_INITIALIZING,
PROVIDER_STATE_STARTED_CERTAIN, PROVIDER_STATE_STARTED_UNCERTAIN,
PROVIDER_STATE_STOPPED, PROVIDER_STATE_PERM_FAILED })
PROVIDER_STATE_STOPPED, PROVIDER_STATE_PERM_FAILED, PROVIDER_STATE_DESTROYED })
@interface ProviderStateEnum {}
/**
@@ -117,12 +121,19 @@ abstract class LocationTimeZoneProvider implements Dumpable {
static final int PROVIDER_STATE_STOPPED = 4;
/**
* The provider has failed and cannot be re-started.
* The provider has failed and cannot be restarted. This is a terminated state triggered by
* the provider itself.
*
* Providers may enter this state after a provider is started.
* Providers may enter this state any time after a provider is started.
*/
static final int PROVIDER_STATE_PERM_FAILED = 5;
/**
* The provider has been destroyed by the controller and cannot be restarted. Similar to
* {@link #PROVIDER_STATE_PERM_FAILED} except that a provider is set into this state.
*/
static final int PROVIDER_STATE_DESTROYED = 6;
/** The {@link LocationTimeZoneProvider} the state is for. */
public final @NonNull LocationTimeZoneProvider provider;
@@ -201,12 +212,14 @@ abstract class LocationTimeZoneProvider implements Dumpable {
case PROVIDER_STATE_STARTED_INITIALIZING:
case PROVIDER_STATE_STARTED_CERTAIN:
case PROVIDER_STATE_STARTED_UNCERTAIN: {
// These can go to each other or PROVIDER_STATE_PERM_FAILED.
// These can go to each other or either of PROVIDER_STATE_PERM_FAILED and
// PROVIDER_STATE_DESTROYED.
break;
}
case PROVIDER_STATE_PERM_FAILED: {
case PROVIDER_STATE_PERM_FAILED:
case PROVIDER_STATE_DESTROYED: {
throw new IllegalArgumentException("Illegal transition out of "
+ prettyPrintStateEnum(PROVIDER_STATE_UNKNOWN));
+ prettyPrintStateEnum(this.stateEnum));
}
default: {
throw new IllegalArgumentException("Invalid this.stateEnum=" + this.stateEnum);
@@ -237,10 +250,12 @@ abstract class LocationTimeZoneProvider implements Dumpable {
}
break;
}
case PROVIDER_STATE_PERM_FAILED: {
case PROVIDER_STATE_PERM_FAILED:
case PROVIDER_STATE_DESTROYED: {
if (event != null || currentUserConfig != null) {
throw new IllegalArgumentException(
"Perf failed state: event and currentUserConfig must be null"
"Terminal state: event and currentUserConfig must be null"
+ ", newStateEnum=" + newStateEnum
+ ", event=" + event
+ ", currentUserConfig=" + currentUserConfig);
}
@@ -260,6 +275,12 @@ abstract class LocationTimeZoneProvider implements Dumpable {
|| stateEnum == PROVIDER_STATE_STARTED_UNCERTAIN;
}
/** Returns {@code true} if {@link #stateEnum} is one of the terminated states. */
boolean isTerminated() {
return stateEnum == PROVIDER_STATE_PERM_FAILED
|| stateEnum == PROVIDER_STATE_DESTROYED;
}
@Override
public String toString() {
// this.provider is omitted deliberately to avoid recursion, since the provider holds
@@ -304,6 +325,8 @@ abstract class LocationTimeZoneProvider implements Dumpable {
return "Started uncertain (" + PROVIDER_STATE_STARTED_UNCERTAIN + ")";
case PROVIDER_STATE_PERM_FAILED:
return "Perm failure (" + PROVIDER_STATE_PERM_FAILED + ")";
case PROVIDER_STATE_DESTROYED:
return "Destroyed (" + PROVIDER_STATE_DESTROYED + ")";
case PROVIDER_STATE_UNKNOWN:
default:
return "Unknown (" + state + ")";
@@ -340,7 +363,7 @@ abstract class LocationTimeZoneProvider implements Dumpable {
}
/**
* Called before the provider is first used.
* Initializes the provider. Called before the provider is first used.
*/
final void initialize(@NonNull ProviderListener providerListener) {
mThreadingDomain.assertCurrentThread();
@@ -372,8 +395,33 @@ abstract class LocationTimeZoneProvider implements Dumpable {
/**
* Implemented by subclasses to do work during {@link #initialize}.
*/
@GuardedBy("mSharedLock")
abstract void onInitialize();
/**
* Destroys the provider. Called after the provider is stopped. This instance will not be called
* again by the {@link LocationTimeZoneProviderController}.
*/
final void destroy() {
mThreadingDomain.assertCurrentThread();
synchronized (mSharedLock) {
ProviderState currentState = mCurrentState.get();
if (!currentState.isTerminated()) {
ProviderState destroyedState = currentState
.newState(PROVIDER_STATE_DESTROYED, null, null, "destroy() called");
setCurrentState(destroyedState, false);
onDestroy();
}
}
}
/**
* Implemented by subclasses to do work during {@link #destroy()}.
*/
@GuardedBy("mSharedLock")
abstract void onDestroy();
/**
* 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
@@ -460,17 +508,23 @@ abstract class LocationTimeZoneProvider implements Dumpable {
PROVIDER_STATE_STARTED_UNCERTAIN, null /* event */,
currentState.currentUserConfiguration, "initialization timeout");
setCurrentState(newState, true);
} else {
warnLog("handleInitializationTimeout: Initialization timeout triggered when in"
+ " an unexpected state=" + currentState);
}
}
}
/**
* Implemented by subclasses to do work during {@link #startUpdates}.
* Implemented by subclasses to do work during {@link #startUpdates}. This is where the logic
* to start the real provider should be implemented.
*
* @param initializationTimeout the initialization timeout to pass to the real provider
*/
abstract void onStartUpdates(@NonNull Duration initializationTimeout);
/**
* Stops the provider. It is an error* to call this method except when the {@link
* Stops the provider. It is an error to call this method except when the {@link
* #getCurrentState()} is one of the started states. This method must be
* called using the handler thread from the {@link ThreadingDomain}.
*/
@@ -505,6 +559,8 @@ abstract class LocationTimeZoneProvider implements Dumpable {
* {@code false} and a "Not implemented" error message.
*/
void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) {
Objects.requireNonNull(testCommand);
if (callback != null) {
Bundle result = new Bundle();
result.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, false);
@@ -525,11 +581,12 @@ abstract class LocationTimeZoneProvider implements Dumpable {
ProviderState currentState = mCurrentState.get();
int eventType = timeZoneProviderEvent.getType();
switch (currentState.stateEnum) {
case PROVIDER_STATE_DESTROYED:
case PROVIDER_STATE_PERM_FAILED: {
// After entering perm failed, there is nothing to do. The remote peer is
// After entering a terminated state, there is nothing to do. The remote peer is
// supposed to stop sending events after it has reported perm failure.
warnLog("handleTimeZoneProviderEvent: Event=" + timeZoneProviderEvent
+ " received for provider=" + this + " when in failed state");
+ " received for provider=" + this + " when in terminated state");
return;
}
case PROVIDER_STATE_STOPPED: {

View File

@@ -94,6 +94,9 @@ abstract class LocationTimeZoneProviderController implements Dumpable {
@DurationMillisLong
abstract long getUncertaintyTimeoutDelayMillis();
/** Called if the geolocation time zone detection is being reconfigured. */
abstract void destroy();
/**
* Used by {@link LocationTimeZoneProviderController} to obtain information from the surrounding
* service. It can easily be faked for tests.
@@ -108,6 +111,9 @@ abstract class LocationTimeZoneProviderController implements Dumpable {
mSharedLock = threadingDomain.getLockObject();
}
/** Destroys the environment, i.e. deregisters listeners, etc. */
abstract void destroy();
/** Returns the {@link ConfigurationInternal} for the current user of the device. */
abstract ConfigurationInternal getCurrentUserConfigurationInternal();

View File

@@ -70,7 +70,7 @@ abstract class LocationTimeZoneProviderProxy implements Dumpable {
/**
* Initializes the proxy. The supplied listener can expect to receive all events after this
* point. This method also calls {@link #onInitialize()} for subclasses to handle their own
* point. This method calls {@link #onInitialize()} for subclasses to handle their own
* initialization.
*/
void initialize(@NonNull Listener listener) {
@@ -85,10 +85,28 @@ abstract class LocationTimeZoneProviderProxy implements Dumpable {
}
/**
* Initializes the proxy. This is called after {@link #mListener} is set.
* Implemented by subclasses to initializes the proxy. This is called after {@link #mListener}
* is set.
*/
@GuardedBy("mSharedLock")
abstract void onInitialize();
/**
* Destroys the proxy. This method calls {@link #onDestroy()} for subclasses to handle their own
* destruction.
*/
void destroy() {
synchronized (mSharedLock) {
onDestroy();
}
}
/**
* Implemented by subclasses to destroy the proxy.
*/
@GuardedBy("mSharedLock")
abstract void onDestroy();
/**
* Sets a new request for the provider.
*/

View File

@@ -1,93 +0,0 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.location.timezone;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.IndentingPrintWriter;
import java.time.Duration;
/**
* 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 started
* for the first time and sends the appropriate event, which ensures the {@link
* LocationTimeZoneProviderController} won't expect any further {@link
* TimeZoneProviderEvent}s to come from it, and won't attempt to use it
* again.
*/
class 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 onStartUpdates(@NonNull Duration initializationTimeout) {
// 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 onStopUpdates() {
// Ignored - this implementation is always permanently failed.
}
@Override
public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) {
synchronized (mSharedLock) {
ipw.println("{Stubbed LocationTimeZoneProvider}");
ipw.println("mProviderName=" + mProviderName);
ipw.println("mCurrentState=" + mCurrentState);
}
}
@Override
public String toString() {
synchronized (mSharedLock) {
return "NullLocationTimeZoneProvider{"
+ "mProviderName='" + mProviderName + '\''
+ ", mCurrentState='" + mCurrentState + '\''
+ '}';
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.location.timezone;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.os.Bundle;
import android.os.RemoteCallback;
import android.service.timezone.TimeZoneProviderService;
import android.util.IndentingPrintWriter;
/**
* A {@link LocationTimeZoneProviderProxy} that provides minimal responses needed for the {@link
* BinderLocationTimeZoneProvider} to operate correctly when there is no "real" provider
* configured / enabled. This can be used during development / testing, or in a production build
* when the platform supports more providers than are needed for an Android deployment.
*
* <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 LocationTimeZoneProviderProxy} implementation will be
* defaulted to a {@link NullLocationTimeZoneProviderProxy}. The {@link
* NullLocationTimeZoneProviderProxy} sends a "permanent failure" event immediately after being
* started for the first time, which ensures the {@link LocationTimeZoneProviderController} won't
* expect any further {@link TimeZoneProviderEvent}s to come from it, and won't attempt to use it
* again.
*/
class NullLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy {
/** Creates the instance. */
NullLocationTimeZoneProviderProxy(
@NonNull Context context, @NonNull ThreadingDomain threadingDomain) {
super(context, threadingDomain);
}
@Override
void onInitialize() {
// No-op
}
@Override
void onDestroy() {
// No-op
}
@Override
void setRequest(@NonNull TimeZoneProviderRequest request) {
if (request.sendUpdates()) {
TimeZoneProviderEvent event = TimeZoneProviderEvent.createPermanentFailureEvent(
"Provider is disabled");
handleTimeZoneProviderEvent(event);
}
}
@Override
void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) {
if (callback != null) {
Bundle result = new Bundle();
result.putBoolean(TimeZoneProviderService.TEST_COMMAND_RESULT_SUCCESS_KEY, false);
result.putString(TimeZoneProviderService.TEST_COMMAND_RESULT_ERROR_KEY,
"Provider is disabled");
callback.sendResult(result);
}
}
@Override
public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) {
synchronized (mSharedLock) {
ipw.println("{NullLocationTimeZoneProviderProxy}");
}
}
}

View File

@@ -110,6 +110,11 @@ class RealLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy {
}
}
@Override
void onDestroy() {
mServiceWatcher.unregister();
}
private boolean register() {
boolean resolves = mServiceWatcher.checkServiceResolves();
if (resolves) {
@@ -192,6 +197,7 @@ class RealLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy {
@Override
public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) {
synchronized (mSharedLock) {
ipw.println("{RealLocationTimeZoneProviderProxy}");
ipw.println("mRequest=" + mRequest);
mServiceWatcher.dump(null, ipw, args);
}

View File

@@ -69,6 +69,11 @@ class SimulatedLocationTimeZoneProviderProxy extends LocationTimeZoneProviderPro
// No-op - nothing to do for the simulated provider.
}
@Override
void onDestroy() {
// No-op - nothing to do for the simulated provider.
}
void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) {
mThreadingDomain.assertCurrentThread();
@@ -152,6 +157,7 @@ class SimulatedLocationTimeZoneProviderProxy extends LocationTimeZoneProviderPro
@Override
public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) {
synchronized (mSharedLock) {
ipw.println("{SimulatedLocationTimeZoneProviderProxy}");
ipw.println("mRequest=" + mRequest);
ipw.println("mLastEvent=" + mLastEvent);

View File

@@ -21,6 +21,8 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.ShellCommand;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Objects;
@@ -50,6 +52,12 @@ final class TestCommand {
mArgs = Objects.requireNonNull(args);
}
@VisibleForTesting
@NonNull
public static TestCommand createForTests(@NonNull String type, @NonNull Bundle args) {
return new TestCommand(type, args);
}
/**
* Creates a {@link TestCommand} from a {@link ShellCommand}'s remaining arguments.
*

View File

@@ -21,6 +21,8 @@ import android.annotation.NonNull;
import com.android.internal.util.Preconditions;
import java.util.concurrent.Callable;
/**
* A class that can be used to enforce / indicate a set of components that need to share threading
* behavior such as a shared lock object and a common thread, with async execution support.
@@ -58,7 +60,7 @@ abstract class ThreadingDomain {
* being used within a set of components, a lot of races can be avoided.
*/
void assertCurrentThread() {
Preconditions.checkArgument(Thread.currentThread() == getThread());
Preconditions.checkState(Thread.currentThread() == getThread());
}
/**
@@ -66,7 +68,7 @@ abstract class ThreadingDomain {
* Generally useful for documenting expectations in the code and avoiding deadlocks.
*/
void assertNotCurrentThread() {
Preconditions.checkArgument(Thread.currentThread() != getThread());
Preconditions.checkState(Thread.currentThread() != getThread());
}
/**
@@ -74,6 +76,37 @@ abstract class ThreadingDomain {
*/
abstract void post(@NonNull Runnable runnable);
/**
* Executes the supplied runnable and waits for up to the duration specified for it to be
* executed. This is only intended for use by test and/or shell command code as it consumes
* multiple threads and could lead to deadlocks.
*
* <p>An {@link IllegalStateException} will be thrown if calling this method would cause a
* deadlock, e.g. if it is called using the threading domain's own thread.
*/
final void postAndWait(@NonNull Runnable runnable, @DurationMillisLong long durationMillis) {
try {
postAndWait(() -> {
runnable.run();
return null;
}, durationMillis);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Executes the supplied callable and waits for up to the duration specified for it to be
* executed. This is only intended for use by test and/or shell command code as it consumes
* multiple threads and could lead to deadlocks.
*
* <p>An {@link IllegalStateException} will be thrown if calling this method would cause a
* deadlock, e.g. if it is called using the threading domain's own thread.
*/
abstract <V> V postAndWait(
@NonNull Callable<V> callable, @DurationMillisLong long durationMillis)
throws Exception;
/**
* Execute the supplied runnable on the threading domain's thread with a delay.
*/

View File

@@ -30,6 +30,12 @@ public interface TimeZoneDetectorInternal extends Dumpable.Container {
/** Adds a listener that will be invoked when time zone detection configuration is changed. */
void addConfigurationListener(ConfigurationChangeListener listener);
/**
* Removes a listener previously added via {@link
* #addConfigurationListener(ConfigurationChangeListener)}.
*/
void removeConfigurationListener(ConfigurationChangeListener listener);
/** Returns the {@link ConfigurationInternal} for the current user. */
ConfigurationInternal getCurrentUserConfigurationInternal();

View File

@@ -68,6 +68,13 @@ public final class TimeZoneDetectorInternalImpl implements TimeZoneDetectorInter
}
}
@Override
public void removeConfigurationListener(ConfigurationChangeListener listener) {
synchronized (mConfigurationListeners) {
mConfigurationListeners.remove(Objects.requireNonNull(listener));
}
}
@Override
@NonNull
public ConfigurationInternal getCurrentUserConfigurationInternal() {

View File

@@ -960,6 +960,11 @@ public class ControllerImplTest {
mConfigurationInternal = Objects.requireNonNull(configurationInternal);
}
@Override
void destroy() {
// No-op test impl.
}
@Override
ConfigurationInternal getCurrentUserConfigurationInternal() {
return mConfigurationInternal;
@@ -1024,6 +1029,7 @@ public class ControllerImplTest {
/** Used to track historic provider states for tests. */
private final TestState<ProviderState> mTestProviderState = new TestState<>();
private boolean mInitialized;
private boolean mDestroyed;
/**
* Creates the instance.
@@ -1037,6 +1043,11 @@ public class ControllerImplTest {
mInitialized = true;
}
@Override
void onDestroy() {
mDestroyed = true;
}
@Override
void onSetCurrentState(ProviderState newState) {
mTestProviderState.set(newState);

View File

@@ -30,10 +30,12 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/** Tests for {@link HandlerThreadingDomain}. */
@Presubmit
@@ -115,6 +117,7 @@ public class HandlerThreadingDomainTest {
});
domain.post(testLogic);
testLogic.assertCompletesWithin(60, TimeUnit.SECONDS);
assertTrue(testLogic.isComplete());
assertTrue(ranOnExpectedThread.get());
}
@@ -123,17 +126,65 @@ public class HandlerThreadingDomainTest {
ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler);
long beforeExecutionNanos = System.nanoTime();
Duration executionDelay = Duration.ofSeconds(5);
AtomicReference<Long> executionNanosHolder = new AtomicReference<>();
AtomicBoolean ranOnExpectedThread = new AtomicBoolean(false);
LatchedRunnable testLogic = new LatchedRunnable(() -> {
ranOnExpectedThread.set(Thread.currentThread() == mTestHandler.getLooper().getThread());
executionNanosHolder.set(System.nanoTime());
});
domain.postDelayed(testLogic, 5000);
testLogic.assertCompletesWithin(60, TimeUnit.SECONDS);
domain.postDelayed(testLogic, executionDelay.toMillis());
long afterPostNanos = System.nanoTime();
testLogic.assertCompletesWithin(
executionDelay.multipliedBy(10).toMillis(), TimeUnit.MILLISECONDS);
long afterWaitNanos = System.nanoTime();
assertTrue(testLogic.isComplete());
assertTrue(ranOnExpectedThread.get());
// The execution should not take place until at least delayDuration after postDelayed().
Duration actualExecutionDelay =
Duration.ofNanos(executionNanosHolder.get() - beforeExecutionNanos);
assertTrue(actualExecutionDelay.compareTo(executionDelay) >= 0);
// The time taken in postDelayed() should be negligible. Certainly less than the
// executionDelay.
Duration postDuration = Duration.ofNanos(afterPostNanos - beforeExecutionNanos);
assertTrue(postDuration.compareTo(executionDelay) < 0);
// The result should not be ready until at least executionDelay has elapsed.
Duration delayBeforeExecuted = Duration.ofNanos(afterWaitNanos - beforeExecutionNanos);
assertTrue(delayBeforeExecuted.compareTo(executionDelay) >= 0);
}
@Test
public void postAndWait() throws Exception {
ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler);
Duration workDuration = Duration.ofSeconds(5);
AtomicBoolean ranOnExpectedThread = new AtomicBoolean(false);
LatchedRunnable testLogic = new LatchedRunnable(() -> {
ranOnExpectedThread.set(Thread.currentThread() == mTestHandler.getLooper().getThread());
// The work takes workDuration to complete.
try {
Thread.sleep(workDuration.toMillis());
} catch (InterruptedException e) {
throw new AssertionError(e);
}
});
long beforeExecutionNanos = System.nanoTime();
domain.postAndWait(testLogic, workDuration.multipliedBy(10).toMillis());
long afterExecutionNanos = System.nanoTime();
assertTrue(afterExecutionNanos - beforeExecutionNanos >= TimeUnit.SECONDS.toNanos(5));
Duration waitDuration = Duration.ofNanos(afterExecutionNanos - beforeExecutionNanos);
assertTrue(waitDuration.compareTo(workDuration) >= 0);
assertTrue(testLogic.isComplete());
assertTrue(ranOnExpectedThread.get());
}
@Test

View File

@@ -0,0 +1,261 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.location.timezone;
import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_ERROR_KEY;
import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_SUCCESS_KEY;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_CERTAIN;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_UNCERTAIN;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STOPPED;
import static com.android.server.location.timezone.TestSupport.USER1_CONFIG_GEO_DETECTION_ENABLED;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
import android.os.RemoteCallback;
import android.platform.test.annotations.Presubmit;
import android.service.timezone.TimeZoneProviderSuggestion;
import android.util.IndentingPrintWriter;
import com.android.server.location.timezone.LocationTimeZoneProvider.ProviderListener;
import com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState;
import com.android.server.timezonedetector.ConfigurationInternal;
import com.android.server.timezonedetector.TestState;
import org.junit.Before;
import org.junit.Test;
import java.time.Duration;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicReference;
/**
* Tests for {@link LocationTimeZoneProvider}.
*/
@Presubmit
public class LocationTimeZoneProviderTest {
private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 123456789L;
private TestThreadingDomain mTestThreadingDomain;
private TestProviderListener mProviderListener;
@Before
public void setUp() {
mTestThreadingDomain = new TestThreadingDomain();
mProviderListener = new TestProviderListener();
}
@Test
public void lifecycle() {
String providerName = "arbitrary";
TestLocationTimeZoneProvider provider =
new TestLocationTimeZoneProvider(mTestThreadingDomain, providerName);
// initialize()
provider.initialize(mProviderListener);
provider.assertOnInitializeCalled();
ProviderState currentState = provider.getCurrentState();
assertEquals(PROVIDER_STATE_STOPPED, currentState.stateEnum);
assertNull(currentState.currentUserConfiguration);
assertSame(provider, currentState.provider);
mTestThreadingDomain.assertQueueEmpty();
// startUpdates()
ConfigurationInternal config = USER1_CONFIG_GEO_DETECTION_ENABLED;
Duration arbitraryInitializationTimeout = Duration.ofMinutes(5);
Duration arbitraryInitializationTimeoutFuzz = Duration.ofMinutes(2);
provider.startUpdates(config, arbitraryInitializationTimeout,
arbitraryInitializationTimeoutFuzz);
provider.assertOnStartCalled(arbitraryInitializationTimeout);
currentState = provider.getCurrentState();
assertSame(provider, currentState.provider);
assertEquals(PROVIDER_STATE_STARTED_INITIALIZING, currentState.stateEnum);
assertEquals(config, currentState.currentUserConfiguration);
assertNull(currentState.event);
// The initialization timeout should be queued.
Duration expectedInitializationTimeout =
arbitraryInitializationTimeout.plus(arbitraryInitializationTimeoutFuzz);
mTestThreadingDomain.assertSingleDelayedQueueItem(expectedInitializationTimeout);
// We don't intend to trigger the timeout, so clear it.
mTestThreadingDomain.removeAllQueuedRunnables();
// Entering started does not trigger an onProviderStateChanged() as it is requested by the
// controller.
mProviderListener.assertProviderChangeNotReported();
// Simulate a suggestion event being received.
TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder()
.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS)
.setTimeZoneIds(Arrays.asList("Europe/London"))
.build();
TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(suggestion);
provider.simulateProviderEventReceived(event);
currentState = provider.getCurrentState();
assertSame(provider, currentState.provider);
assertEquals(PROVIDER_STATE_STARTED_CERTAIN, currentState.stateEnum);
assertEquals(event, currentState.event);
assertEquals(config, currentState.currentUserConfiguration);
mTestThreadingDomain.assertQueueEmpty();
mProviderListener.assertProviderChangeReported(PROVIDER_STATE_STARTED_CERTAIN);
// Simulate an uncertain event being received.
event = TimeZoneProviderEvent.createUncertainEvent();
provider.simulateProviderEventReceived(event);
currentState = provider.getCurrentState();
assertSame(provider, currentState.provider);
assertEquals(PROVIDER_STATE_STARTED_UNCERTAIN, currentState.stateEnum);
assertEquals(event, currentState.event);
assertEquals(config, currentState.currentUserConfiguration);
mTestThreadingDomain.assertQueueEmpty();
mProviderListener.assertProviderChangeReported(PROVIDER_STATE_STARTED_UNCERTAIN);
// stopUpdates()
provider.stopUpdates();
provider.assertOnStopUpdatesCalled();
currentState = provider.getCurrentState();
assertSame(provider, currentState.provider);
assertEquals(PROVIDER_STATE_STOPPED, currentState.stateEnum);
assertNull(currentState.event);
assertNull(currentState.currentUserConfiguration);
mTestThreadingDomain.assertQueueEmpty();
// Entering stopped does not trigger an onProviderStateChanged() as it is requested by the
// controller.
mProviderListener.assertProviderChangeNotReported();
// destroy()
provider.destroy();
provider.assertOnDestroyCalled();
}
@Test
public void defaultHandleTestCommandImpl() {
String providerName = "primary";
TestLocationTimeZoneProvider provider =
new TestLocationTimeZoneProvider(mTestThreadingDomain, providerName);
TestCommand testCommand = TestCommand.createForTests("test", new Bundle());
AtomicReference<Bundle> resultReference = new AtomicReference<>();
RemoteCallback callback = new RemoteCallback(resultReference::set);
provider.handleTestCommand(testCommand, callback);
Bundle result = resultReference.get();
assertNotNull(result);
assertFalse(result.getBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY));
assertNotNull(result.getString(TEST_COMMAND_RESULT_ERROR_KEY));
}
/** A test stand-in for the real {@link LocationTimeZoneProviderController}'s listener. */
private static class TestProviderListener implements ProviderListener {
private final TestState<ProviderState> mReportedProviderStateChanges = new TestState<>();
@Override
public void onProviderStateChange(ProviderState providerState) {
mReportedProviderStateChanges.set(providerState);
}
void assertProviderChangeReported(int expectedStateEnum) {
mReportedProviderStateChanges.assertChangeCount(1);
ProviderState latest = mReportedProviderStateChanges.getLatest();
assertEquals(expectedStateEnum, latest.stateEnum);
mReportedProviderStateChanges.commitLatest();
}
public void assertProviderChangeNotReported() {
mReportedProviderStateChanges.assertHasNotBeenSet();
}
}
private static class TestLocationTimeZoneProvider extends LocationTimeZoneProvider {
private boolean mOnInitializeCalled;
private boolean mOnDestroyCalled;
private boolean mOnStartUpdatesCalled;
private Duration mInitializationTimeout;
private boolean mOnStopUpdatesCalled;
/** Creates the instance. */
TestLocationTimeZoneProvider(@NonNull ThreadingDomain threadingDomain,
@NonNull String providerName) {
super(threadingDomain, providerName);
}
@Override
void onInitialize() {
mOnInitializeCalled = true;
}
@Override
void onDestroy() {
mOnDestroyCalled = true;
}
@Override
void onStartUpdates(@NonNull Duration initializationTimeout) {
mOnStartUpdatesCalled = true;
mInitializationTimeout = initializationTimeout;
}
@Override
void onStopUpdates() {
mOnStopUpdatesCalled = true;
}
@Override
public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) {
// No-op for tests
}
void assertOnInitializeCalled() {
assertTrue(mOnInitializeCalled);
}
void assertOnStartCalled(Duration expectedInitializationTimeout) {
assertTrue(mOnStartUpdatesCalled);
assertEquals(expectedInitializationTimeout, mInitializationTimeout);
}
void simulateProviderEventReceived(TimeZoneProviderEvent event) {
handleTimeZoneProviderEvent(event);
}
void assertOnStopUpdatesCalled() {
assertTrue(mOnStopUpdatesCalled);
}
void assertOnDestroyCalled() {
assertTrue(mOnDestroyCalled);
}
}
}

View File

@@ -1,151 +0,0 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.location.timezone;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING;
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STOPPED;
import static com.android.server.location.timezone.TestSupport.USER1_CONFIG_GEO_DETECTION_ENABLED;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.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;
import java.time.Duration;
/**
* 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_STOPPED, currentState.stateEnum);
assertNull(currentState.currentUserConfiguration);
assertSame(provider, currentState.provider);
mTestThreadingDomain.assertQueueEmpty();
}
@Test
public void startSchedulesPermFailure() {
String providerName = "primary";
NullLocationTimeZoneProvider provider =
new NullLocationTimeZoneProvider(mTestThreadingDomain, providerName);
provider.initialize(providerState -> mTestController.onProviderStateChange(providerState));
ConfigurationInternal config = USER1_CONFIG_GEO_DETECTION_ENABLED;
Duration arbitraryInitializationTimeout = Duration.ofMinutes(5);
Duration arbitraryInitializationTimeoutFuzz = Duration.ofMinutes(2);
provider.startUpdates(config, arbitraryInitializationTimeout,
arbitraryInitializationTimeoutFuzz);
// The NullProvider should enter the enabled state, but have schedule an immediate runnable
// to switch to perm failure.
ProviderState currentState = provider.getCurrentState();
assertSame(provider, currentState.provider);
assertEquals(PROVIDER_STATE_STARTED_INITIALIZING, currentState.stateEnum);
assertEquals(config, currentState.currentUserConfiguration);
mTestThreadingDomain.assertNextQueueItemIsImmediate();
// 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.
}
@Override
boolean isUncertaintyTimeoutSet() {
// Not needed for provider testing.
return false;
}
@Override
long getUncertaintyTimeoutDelayMillis() {
// Not needed for provider testing.
return 0;
}
void onProviderStateChange(ProviderState providerState) {
this.mProviderState.set(providerState);
}
@Override
public void dump(IndentingPrintWriter pw, String[] args) {
// Not needed for provider testing.
}
void assertProviderChangeTriggered(int expectedStateEnum) {
assertEquals(expectedStateEnum, mProviderState.getLatest().stateEnum);
mProviderState.commitLatest();
}
public void assertProviderChangeNotTriggered() {
mProviderState.assertHasNotBeenSet();
}
}
}

View File

@@ -26,6 +26,7 @@ import java.time.Duration;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Objects;
import java.util.concurrent.Callable;
/**
* A ThreadingDomain that simulates idealized post() semantics. Execution takes place in zero time,
@@ -78,6 +79,11 @@ class TestThreadingDomain extends ThreadingDomain {
postDelayed(r, null, 0);
}
@Override
<V> V postAndWait(Callable<V> callable, long durationMillis) {
throw new UnsupportedOperationException("Not implemented");
}
@Override
void postDelayed(Runnable r, long delayMillis) {
postDelayed(r, null, delayMillis);
@@ -94,6 +100,10 @@ class TestThreadingDomain extends ThreadingDomain {
mQueue.removeIf(runnable -> runnable.token != null && runnable.token == token);
}
void removeAllQueuedRunnables() {
mQueue.clear();
}
void assertSingleDelayedQueueItem(Duration expectedDelay) {
assertQueueLength(1);
assertNextQueueItemIsDelayed(expectedDelay);