Add LocationTimeZoneProvider test infrastructure
Add test infrastructure for the location time zone manager.
This test command injection facility is a generalization of the
pre-existing SimulatedBinderProviderEvent and so replaces it with a
distinct send_provider_test_command. This can be used to test the
ControllerImpl without using the real LocationTimeZoneProvider(s) (which
are disabled when the provider is set to simulation mode).
A command to a simulated secondary provider:
$ adb shell setprop persist.sys.geotz.secondary simulated
$ adb shell reboot
$ adb shell cmd location_time_zone_manager send_provider_test_command \
secondary on-bind
$ adb shell cmd location_time_zone_manager send_provider_test_command \
secondary success \
"tz=string_array:Europe/London\&Europe/Paris"
See also the following for help:
$ adb shell cmd time_zone_detector
$ adb shell cmd location_time_zone_manager
Bug: 152746105
Test: See steps above
Change-Id: I7259778ad7af8f2937cb3b4b8751a02c149d2b21
This commit is contained in:
@@ -22,7 +22,6 @@ import android.service.timezone.ITimeZoneProviderManager;
|
||||
* @hide
|
||||
*/
|
||||
oneway interface ITimeZoneProvider {
|
||||
void setTimeZoneProviderManager(in @nullable ITimeZoneProviderManager manager);
|
||||
void startUpdates(in long initializationTimeoutMillis);
|
||||
void startUpdates(in ITimeZoneProviderManager manager, in long initializationTimeoutMillis);
|
||||
void stopUpdates();
|
||||
}
|
||||
|
||||
@@ -112,7 +112,18 @@ public abstract class TimeZoneProviderService extends Service {
|
||||
|
||||
private static final String TAG = "TimeZoneProviderService";
|
||||
|
||||
private final Handler mHandler = BackgroundThread.getHandler();
|
||||
/**
|
||||
* The test command result key indicating whether a command succeeded. Value type: boolean
|
||||
* @hide
|
||||
*/
|
||||
public static final String TEST_COMMAND_RESULT_SUCCESS_KEY = "SUCCESS";
|
||||
|
||||
/**
|
||||
* The test command result key for the error message present when {@link
|
||||
* #TEST_COMMAND_RESULT_SUCCESS_KEY} is false. Value type: string
|
||||
* @hide
|
||||
*/
|
||||
public static final String TEST_COMMAND_RESULT_ERROR_KEY = "ERROR";
|
||||
|
||||
/**
|
||||
* The Intent action that the primary location-derived time zone provider service must respond
|
||||
@@ -132,6 +143,8 @@ public abstract class TimeZoneProviderService extends Service {
|
||||
|
||||
private final TimeZoneProviderServiceWrapper mWrapper = new TimeZoneProviderServiceWrapper();
|
||||
|
||||
private final Handler mHandler = BackgroundThread.getHandler();
|
||||
|
||||
/** Set by {@link #mHandler} thread. */
|
||||
@Nullable
|
||||
private ITimeZoneProviderManager mManager;
|
||||
@@ -198,11 +211,22 @@ public abstract class TimeZoneProviderService extends Service {
|
||||
});
|
||||
}
|
||||
|
||||
private void onStartUpdatesInternal(@NonNull ITimeZoneProviderManager manager,
|
||||
@DurationMillisLong long initializationTimeoutMillis) {
|
||||
mManager = manager;
|
||||
onStartUpdates(initializationTimeoutMillis);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the provider sending updates.
|
||||
*/
|
||||
public abstract void onStartUpdates(@DurationMillisLong long initializationTimeoutMillis);
|
||||
|
||||
private void onStopUpdatesInternal() {
|
||||
onStopUpdates();
|
||||
mManager = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the provider sending updates.
|
||||
*/
|
||||
@@ -210,18 +234,14 @@ public abstract class TimeZoneProviderService extends Service {
|
||||
|
||||
private class TimeZoneProviderServiceWrapper extends ITimeZoneProvider.Stub {
|
||||
|
||||
@Override
|
||||
public void setTimeZoneProviderManager(ITimeZoneProviderManager manager) {
|
||||
public void startUpdates(@NonNull ITimeZoneProviderManager manager,
|
||||
@DurationMillisLong long initializationTimeoutMillis) {
|
||||
Objects.requireNonNull(manager);
|
||||
mHandler.post(() -> TimeZoneProviderService.this.mManager = manager);
|
||||
}
|
||||
|
||||
public void startUpdates(@DurationMillisLong long initializationTimeoutMillis) {
|
||||
mHandler.post(() -> onStartUpdates(initializationTimeoutMillis));
|
||||
mHandler.post(() -> onStartUpdatesInternal(manager, initializationTimeoutMillis));
|
||||
}
|
||||
|
||||
public void stopUpdates() {
|
||||
mHandler.post(TimeZoneProviderService.this::onStopUpdates);
|
||||
mHandler.post(TimeZoneProviderService.this::onStopUpdatesInternal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ import static com.android.server.location.timezone.LocationTimeZoneProvider.Prov
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.os.RemoteCallback;
|
||||
import android.util.IndentingPrintWriter;
|
||||
import android.util.Slog;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
@@ -161,6 +161,16 @@ class BinderLocationTimeZoneProvider extends LocationTimeZoneProvider {
|
||||
mProxy.setRequest(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Passes the supplied test command to the current proxy.
|
||||
*/
|
||||
@Override
|
||||
void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) {
|
||||
mThreadingDomain.assertCurrentThread();
|
||||
|
||||
mProxy.handleTestCommand(testCommand, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) {
|
||||
synchronized (mSharedLock) {
|
||||
@@ -191,19 +201,4 @@ class BinderLocationTimeZoneProvider extends LocationTimeZoneProvider {
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
mThreadingDomain.assertCurrentThread();
|
||||
|
||||
if (!(mProxy instanceof SimulatedLocationTimeZoneProviderProxy)) {
|
||||
Slog.w(TAG, mProxy + " is not a " + SimulatedLocationTimeZoneProviderProxy.class
|
||||
+ ", event=" + event);
|
||||
return;
|
||||
}
|
||||
((SimulatedLocationTimeZoneProviderProxy) mProxy).simulate(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import static com.android.server.location.timezone.TimeZoneProviderEvent.EVENT_T
|
||||
import android.annotation.DurationMillisLong;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.os.RemoteCallback;
|
||||
import android.util.IndentingPrintWriter;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
@@ -117,7 +118,7 @@ class ControllerImpl extends LocationTimeZoneProviderController {
|
||||
mThreadingDomain.assertCurrentThread();
|
||||
|
||||
synchronized (mSharedLock) {
|
||||
debugLog("onEnvironmentConfigChanged()");
|
||||
debugLog("onConfigChanged()");
|
||||
|
||||
ConfigurationInternal oldConfig = mCurrentUserConfiguration;
|
||||
ConfigurationInternal newConfig = mEnvironment.getCurrentUserConfigurationInternal();
|
||||
@@ -553,6 +554,7 @@ class ControllerImpl extends LocationTimeZoneProviderController {
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static GeolocationTimeZoneSuggestion createUncertainSuggestion(@NonNull String reason) {
|
||||
GeolocationTimeZoneSuggestion suggestion = new GeolocationTimeZoneSuggestion(null);
|
||||
suggestion.addDebugInfo(reason);
|
||||
@@ -560,30 +562,42 @@ class ControllerImpl extends LocationTimeZoneProviderController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Passes a test command to the specified provider. If the provider name does not match a
|
||||
* known provider, then the command is logged and discarded.
|
||||
*/
|
||||
void simulateBinderProviderEvent(@NonNull SimulatedBinderProviderEvent event) {
|
||||
void handleProviderTestCommand(
|
||||
@NonNull String providerName, @NonNull TestCommand testCommand,
|
||||
@Nullable RemoteCallback callback) {
|
||||
mThreadingDomain.assertCurrentThread();
|
||||
|
||||
String targetProviderName = event.getProviderName();
|
||||
LocationTimeZoneProvider targetProvider = getLocationTimeZoneProvider(providerName);
|
||||
if (targetProvider == null) {
|
||||
warnLog("Unable to process test command:"
|
||||
+ " providerName=" + providerName + ", testCommand=" + testCommand);
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (mSharedLock) {
|
||||
try {
|
||||
targetProvider.handleTestCommand(testCommand, callback);
|
||||
} catch (Exception e) {
|
||||
warnLog("Unable to process test command:"
|
||||
+ " providerName=" + providerName + ", testCommand=" + testCommand, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private LocationTimeZoneProvider getLocationTimeZoneProvider(@NonNull String providerName) {
|
||||
LocationTimeZoneProvider targetProvider;
|
||||
if (Objects.equals(mPrimaryProvider.getName(), targetProviderName)) {
|
||||
if (Objects.equals(mPrimaryProvider.getName(), providerName)) {
|
||||
targetProvider = mPrimaryProvider;
|
||||
} else if (Objects.equals(mSecondaryProvider.getName(), targetProviderName)) {
|
||||
} else if (Objects.equals(mSecondaryProvider.getName(), providerName)) {
|
||||
targetProvider = mSecondaryProvider;
|
||||
} else {
|
||||
warnLog("Unable to process simulated binder provider event,"
|
||||
+ " unknown providerName in event=" + event);
|
||||
return;
|
||||
warnLog("Bad providerName=" + providerName);
|
||||
targetProvider = null;
|
||||
}
|
||||
if (!(targetProvider instanceof BinderLocationTimeZoneProvider)) {
|
||||
warnLog("Unable to process simulated binder provider event,"
|
||||
+ " provider=" + targetProvider
|
||||
+ " is not a " + BinderLocationTimeZoneProvider.class
|
||||
+ ", event=" + event);
|
||||
return;
|
||||
}
|
||||
((BinderLocationTimeZoneProvider) targetProvider).simulateBinderProviderEvent(event);
|
||||
return targetProvider;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.RemoteCallback;
|
||||
import android.os.ResultReceiver;
|
||||
import android.os.ShellCallback;
|
||||
import android.os.SystemProperties;
|
||||
@@ -40,7 +42,11 @@ import com.android.server.timezonedetector.TimeZoneDetectorService;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.PrintWriter;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* A service class that acts as a container for the {@link LocationTimeZoneProviderController},
|
||||
@@ -116,8 +122,11 @@ public class LocationTimeZoneManagerService extends Binder {
|
||||
static final String PRIMARY_PROVIDER_NAME = "primary";
|
||||
static final String SECONDARY_PROVIDER_NAME = "secondary";
|
||||
|
||||
private static final String SIMULATION_MODE_SYSTEM_PROPERTY_PREFIX =
|
||||
"persist.sys.location_tz_simulation_mode.";
|
||||
static final String PROVIDER_MODE_OVERRIDE_SYSTEM_PROPERTY_PREFIX = "persist.sys.geotz.";
|
||||
static final String PROVIDER_MODE_SIMULATED = "simulated";
|
||||
static final String PROVIDER_MODE_DISABLED = "disabled";
|
||||
|
||||
private static final long BLOCKING_OP_WAIT_DURATION_MILLIS = Duration.ofSeconds(20).toMillis();
|
||||
|
||||
private static final String ATTRIBUTION_TAG = "LocationTimeZoneService";
|
||||
|
||||
@@ -187,8 +196,7 @@ public class LocationTimeZoneManagerService extends Binder {
|
||||
}
|
||||
|
||||
private LocationTimeZoneProvider createPrimaryProvider() {
|
||||
Resources resources = mContext.getResources();
|
||||
if (!resources.getBoolean(R.bool.config_enablePrimaryLocationTimeZoneProvider)) {
|
||||
if (isDisabled(PRIMARY_PROVIDER_NAME)) {
|
||||
return new NullLocationTimeZoneProvider(mThreadingDomain, PRIMARY_PROVIDER_NAME);
|
||||
}
|
||||
|
||||
@@ -209,8 +217,7 @@ public class LocationTimeZoneManagerService extends Binder {
|
||||
}
|
||||
|
||||
private LocationTimeZoneProvider createSecondaryProvider() {
|
||||
Resources resources = mContext.getResources();
|
||||
if (!resources.getBoolean(R.bool.config_enableSecondaryLocationTimeZoneProvider)) {
|
||||
if (isDisabled(SECONDARY_PROVIDER_NAME)) {
|
||||
return new NullLocationTimeZoneProvider(mThreadingDomain, SECONDARY_PROVIDER_NAME);
|
||||
}
|
||||
|
||||
@@ -230,9 +237,41 @@ public class LocationTimeZoneManagerService extends Binder {
|
||||
return new BinderLocationTimeZoneProvider(mThreadingDomain, SECONDARY_PROVIDER_NAME, proxy);
|
||||
}
|
||||
|
||||
private boolean isInSimulationMode(String providerName) {
|
||||
return SystemProperties.getBoolean(
|
||||
SIMULATION_MODE_SYSTEM_PROPERTY_PREFIX + providerName, false);
|
||||
/** Used for bug triage and in tests to simulate provider events. */
|
||||
private static boolean isInSimulationMode(String providerName) {
|
||||
return isProviderModeSetInSystemProperties(providerName, PROVIDER_MODE_SIMULATED);
|
||||
}
|
||||
|
||||
/** Used for bug triage, tests and experiments to remove a provider. */
|
||||
private boolean isDisabled(String providerName) {
|
||||
return !isProviderEnabledInConfig(providerName)
|
||||
|| isProviderModeSetInSystemProperties(providerName, PROVIDER_MODE_DISABLED);
|
||||
}
|
||||
|
||||
private boolean isProviderEnabledInConfig(String providerName) {
|
||||
int providerEnabledConfigId;
|
||||
switch (providerName) {
|
||||
case PRIMARY_PROVIDER_NAME: {
|
||||
providerEnabledConfigId = R.bool.config_enablePrimaryLocationTimeZoneProvider;
|
||||
break;
|
||||
}
|
||||
case SECONDARY_PROVIDER_NAME: {
|
||||
providerEnabledConfigId = R.bool.config_enableSecondaryLocationTimeZoneProvider;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new IllegalArgumentException(providerName);
|
||||
}
|
||||
}
|
||||
Resources resources = mContext.getResources();
|
||||
return resources.getBoolean(providerEnabledConfigId);
|
||||
}
|
||||
|
||||
private static boolean isProviderModeSetInSystemProperties(
|
||||
@NonNull String providerName, @NonNull String mode) {
|
||||
String systemPropertyProviderMode = SystemProperties.get(
|
||||
PROVIDER_MODE_OVERRIDE_SYSTEM_PROPERTY_PREFIX + providerName, null);
|
||||
return Objects.equals(systemPropertyProviderMode, mode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -244,19 +283,45 @@ public class LocationTimeZoneManagerService extends Binder {
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously passes a {@link SimulatedBinderProviderEvent] to the appropriate provider.
|
||||
* The device must be in simulation mode, otherwise an {@link IllegalStateException} will be
|
||||
* thrown.
|
||||
* Passes a {@link TestCommand} to the specified provider and waits for the response.
|
||||
*/
|
||||
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.");
|
||||
@NonNull
|
||||
Bundle handleProviderTestCommand(
|
||||
@NonNull String providerName, @NonNull TestCommand testCommand) {
|
||||
enforceManageTimeZoneDetectorPermission();
|
||||
|
||||
// Because this method blocks and posts work to the threading domain thread, it would cause
|
||||
// a deadlock if it were called by the threading domain thread.
|
||||
mThreadingDomain.assertNotCurrentThread();
|
||||
|
||||
AtomicReference<Bundle> resultReference = new AtomicReference<>();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
RemoteCallback remoteCallback = new RemoteCallback(x -> {
|
||||
resultReference.set(x);
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
mThreadingDomain.post(() -> {
|
||||
synchronized (mSharedLock) {
|
||||
if (mLocationTimeZoneDetectorController == null) {
|
||||
remoteCallback.sendResult(null);
|
||||
return;
|
||||
}
|
||||
mLocationTimeZoneDetectorController.handleProviderTestCommand(
|
||||
providerName, testCommand, remoteCallback);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// Wait, but not indefinitely.
|
||||
if (!latch.await(BLOCKING_OP_WAIT_DURATION_MILLIS, TimeUnit.MILLISECONDS)) {
|
||||
throw new RuntimeException("Command did not complete in time");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
mThreadingDomain.post(
|
||||
() -> mLocationTimeZoneDetectorController.simulateBinderProviderEvent(event));
|
||||
|
||||
return resultReference.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -293,4 +358,10 @@ public class LocationTimeZoneManagerService extends Binder {
|
||||
Slog.w(TAG, msg, t);
|
||||
}
|
||||
}
|
||||
|
||||
private void enforceManageTimeZoneDetectorPermission() {
|
||||
mContext.enforceCallingPermission(
|
||||
android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION,
|
||||
"manage time and time zone detection");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,25 @@
|
||||
*/
|
||||
package com.android.server.location.timezone;
|
||||
|
||||
import static com.android.server.location.timezone.LocationTimeZoneManagerService.PRIMARY_PROVIDER_NAME;
|
||||
import static com.android.server.location.timezone.LocationTimeZoneManagerService.SECONDARY_PROVIDER_NAME;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.os.Bundle;
|
||||
import android.os.ShellCommand;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/** Implements the shell command interface for {@link LocationTimeZoneManagerService}. */
|
||||
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_SEND_PROVIDER_TEST_COMMAND = "send_provider_test_command";
|
||||
|
||||
private final LocationTimeZoneManagerService mService;
|
||||
|
||||
LocationTimeZoneManagerShellCommand(LocationTimeZoneManagerService service) {
|
||||
@@ -35,8 +47,8 @@ class LocationTimeZoneManagerShellCommand extends ShellCommand {
|
||||
}
|
||||
|
||||
switch (cmd) {
|
||||
case "simulate_binder": {
|
||||
return runSimulateBinderEvent();
|
||||
case CMD_SEND_PROVIDER_TEST_COMMAND: {
|
||||
return runSendProviderTestCommand();
|
||||
}
|
||||
default: {
|
||||
return handleDefaultCommands(cmd);
|
||||
@@ -44,37 +56,82 @@ class LocationTimeZoneManagerShellCommand extends ShellCommand {
|
||||
}
|
||||
}
|
||||
|
||||
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.printf(" %s <provider name> <test command>\n", CMD_SEND_PROVIDER_TEST_COMMAND);
|
||||
pw.println(" Passes a test command to the named provider.");
|
||||
pw.println();
|
||||
SimulatedBinderProviderEvent.printCommandLineOpts(pw);
|
||||
pw.printf("%s details:\n", CMD_SEND_PROVIDER_TEST_COMMAND);
|
||||
pw.println();
|
||||
pw.printf("<provider name> = One of %s\n", VALID_PROVIDER_NAMES);
|
||||
pw.println();
|
||||
pw.println("<test command> encoding:");
|
||||
pw.println();
|
||||
TestCommand.printShellCommandEncodingHelp(pw);
|
||||
pw.println();
|
||||
pw.printf("Provider modes can be modified by setting the \"%s<provider name>\" system"
|
||||
+ " property and restarting the service or rebooting the device.\n",
|
||||
LocationTimeZoneManagerService.PROVIDER_MODE_OVERRIDE_SYSTEM_PROPERTY_PREFIX);
|
||||
pw.println("Values are:");
|
||||
pw.printf(" %s - simulation mode (see below for commands)\n",
|
||||
LocationTimeZoneManagerService.PROVIDER_MODE_SIMULATED);
|
||||
pw.printf(" %s - disabled mode\n", LocationTimeZoneManagerService.PROVIDER_MODE_DISABLED);
|
||||
pw.println();
|
||||
pw.println("Simulated providers can be used to test the system server behavior or to"
|
||||
+ " reproduce bugs without the complexity of using real providers.");
|
||||
pw.println();
|
||||
pw.println("The test commands for simulated providers are:");
|
||||
SimulatedLocationTimeZoneProviderProxy.printTestCommandShellHelp(pw);
|
||||
pw.println();
|
||||
pw.println("Test commands cannot currently be passed to real provider implementations.");
|
||||
pw.println();
|
||||
}
|
||||
|
||||
private int runSendProviderTestCommand() {
|
||||
PrintWriter outPrintWriter = getOutPrintWriter();
|
||||
|
||||
String providerName;
|
||||
TestCommand testCommand;
|
||||
try {
|
||||
providerName = validateProviderName(getNextArgRequired());
|
||||
testCommand = createTestCommandFromNextShellArg();
|
||||
} catch (RuntimeException e) {
|
||||
reportError(e);
|
||||
return 1;
|
||||
}
|
||||
|
||||
outPrintWriter.println("Injecting testCommand=" + testCommand
|
||||
+ " to providerName=" + providerName);
|
||||
try {
|
||||
Bundle result = mService.handleProviderTestCommand(providerName, testCommand);
|
||||
outPrintWriter.println(result);
|
||||
} catch (RuntimeException e) {
|
||||
reportError(e);
|
||||
return 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private TestCommand createTestCommandFromNextShellArg() {
|
||||
return TestCommand.createFromShellCommandArgs(this);
|
||||
}
|
||||
|
||||
private void reportError(Throwable e) {
|
||||
PrintWriter errPrintWriter = getErrPrintWriter();
|
||||
errPrintWriter.println("Error: ");
|
||||
e.printStackTrace(errPrintWriter);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
static String validateProviderName(@NonNull String value) {
|
||||
if (!VALID_PROVIDER_NAMES.contains(value)) {
|
||||
throw new IllegalArgumentException("Unknown provider name=" + value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package com.android.server.location.timezone;
|
||||
|
||||
import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_ERROR_KEY;
|
||||
import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_SUCCESS_KEY;
|
||||
|
||||
import static com.android.server.location.timezone.LocationTimeZoneManagerService.debugLog;
|
||||
import static com.android.server.location.timezone.LocationTimeZoneManagerService.warnLog;
|
||||
import static com.android.server.location.timezone.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
|
||||
@@ -31,7 +34,9 @@ import android.annotation.ElapsedRealtimeLong;
|
||||
import android.annotation.IntDef;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.RemoteCallback;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
@@ -493,6 +498,21 @@ abstract class LocationTimeZoneProvider implements Dumpable {
|
||||
*/
|
||||
abstract void onStopUpdates();
|
||||
|
||||
/**
|
||||
* Overridden by subclasses to handle the supplied {@link TestCommand}. If {@code callback} is
|
||||
* non-null, the default implementation sends a result {@link Bundle} with {@link
|
||||
* android.service.timezone.TimeZoneProviderService#TEST_COMMAND_RESULT_SUCCESS_KEY} set to
|
||||
* {@code false} and a "Not implemented" error message.
|
||||
*/
|
||||
void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) {
|
||||
if (callback != null) {
|
||||
Bundle result = new Bundle();
|
||||
result.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, false);
|
||||
result.putString(TEST_COMMAND_RESULT_ERROR_KEY, "Not implemented");
|
||||
callback.sendResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
/** For subclasses to invoke when a {@link TimeZoneProviderEvent} has been received. */
|
||||
final void handleTimeZoneProviderEvent(@NonNull TimeZoneProviderEvent timeZoneProviderEvent) {
|
||||
mThreadingDomain.assertCurrentThread();
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.RemoteCallback;
|
||||
import android.util.IndentingPrintWriter;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
@@ -93,6 +94,13 @@ abstract class LocationTimeZoneProviderProxy implements Dumpable {
|
||||
*/
|
||||
abstract void setRequest(@NonNull TimeZoneProviderRequest request);
|
||||
|
||||
/**
|
||||
* Processes the supplied test command. An optional callback can be supplied to listen for a
|
||||
* response.
|
||||
*/
|
||||
abstract void handleTestCommand(@NonNull TestCommand testCommand,
|
||||
@Nullable RemoteCallback callback);
|
||||
|
||||
/**
|
||||
* Handles a {@link TimeZoneProviderEvent} from a remote process.
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package com.android.server.location.timezone;
|
||||
|
||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||
import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_ERROR_KEY;
|
||||
import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_SUCCESS_KEY;
|
||||
|
||||
import static com.android.server.location.timezone.LocationTimeZoneManagerService.warnLog;
|
||||
|
||||
@@ -28,9 +30,10 @@ import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.os.RemoteCallback;
|
||||
import android.service.timezone.ITimeZoneProvider;
|
||||
import android.service.timezone.ITimeZoneProviderManager;
|
||||
import android.service.timezone.TimeZoneProviderSuggestion;
|
||||
@@ -118,18 +121,15 @@ class RealLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy {
|
||||
private void onBind(IBinder binder, ComponentName componentName) {
|
||||
mThreadingDomain.assertCurrentThread();
|
||||
|
||||
ITimeZoneProvider provider = ITimeZoneProvider.Stub.asInterface(binder);
|
||||
|
||||
synchronized (mSharedLock) {
|
||||
try {
|
||||
mManagerProxy = new ManagerProxy();
|
||||
provider.setTimeZoneProviderManager(mManagerProxy);
|
||||
trySendCurrentRequest();
|
||||
mListener.onProviderBound();
|
||||
} catch (RemoteException e) {
|
||||
// This is not expected to happen.
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
// When a new remote is first bound we create the ManagerProxy that will be passed to
|
||||
// it. By creating a new one for each bind the ManagerProxy can check whether it is
|
||||
// still the current proxy and if not it can ignore incoming calls.
|
||||
mManagerProxy = new ManagerProxy();
|
||||
mListener.onProviderBound();
|
||||
|
||||
// Send the current request to the remote.
|
||||
trySendCurrentRequest();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,8 @@ class RealLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy {
|
||||
mThreadingDomain.assertCurrentThread();
|
||||
|
||||
synchronized (mSharedLock) {
|
||||
// Clear the ManagerProxy used with the old remote so we will ignore calls from any old
|
||||
// remotes that somehow hold a reference to it.
|
||||
mManagerProxy = null;
|
||||
mListener.onProviderUnbound();
|
||||
}
|
||||
@@ -150,23 +152,43 @@ class RealLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy {
|
||||
synchronized (mSharedLock) {
|
||||
mRequest = request;
|
||||
|
||||
// Two possible outcomes here: Either we are already bound to a remote service, in
|
||||
// which case trySendCurrentRequest() will communicate the request immediately, or we
|
||||
// are not bound to the remote service yet, in which case it will be sent during
|
||||
// onBindOnHandlerThread() instead.
|
||||
trySendCurrentRequest();
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mSharedLock")
|
||||
private void trySendCurrentRequest() {
|
||||
ManagerProxy managerProxy = mManagerProxy;
|
||||
TimeZoneProviderRequest request = mRequest;
|
||||
mServiceWatcher.runOnBinder(binder -> {
|
||||
ITimeZoneProvider service = ITimeZoneProvider.Stub.asInterface(binder);
|
||||
if (request.sendUpdates()) {
|
||||
service.startUpdates(request.getInitializationTimeout().toMillis());
|
||||
service.startUpdates(managerProxy, request.getInitializationTimeout().toMillis());
|
||||
} else {
|
||||
service.stopUpdates();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A stubbed implementation.
|
||||
*/
|
||||
@Override
|
||||
void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) {
|
||||
mThreadingDomain.assertCurrentThread();
|
||||
|
||||
if (callback != null) {
|
||||
Bundle result = new Bundle();
|
||||
result.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, false);
|
||||
result.putString(TEST_COMMAND_RESULT_ERROR_KEY, "Not implemented");
|
||||
callback.sendResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) {
|
||||
synchronized (mSharedLock) {
|
||||
@@ -205,6 +227,8 @@ class RealLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy {
|
||||
private void onTimeZoneProviderEvent(TimeZoneProviderEvent event) {
|
||||
synchronized (mSharedLock) {
|
||||
if (mManagerProxy != this) {
|
||||
// Ignore incoming calls if this instance is no longer the current
|
||||
// mManagerProxy.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.location.timezone;
|
||||
|
||||
import static com.android.server.location.timezone.LocationTimeZoneManagerService.PRIMARY_PROVIDER_NAME;
|
||||
import static com.android.server.location.timezone.LocationTimeZoneManagerService.SECONDARY_PROVIDER_NAME;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.os.ShellCommand;
|
||||
import android.os.SystemClock;
|
||||
import android.service.timezone.TimeZoneProviderSuggestion;
|
||||
|
||||
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, SECONDARY_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 mType;
|
||||
@Nullable private final TimeZoneProviderEvent mTimeZoneProviderEvent;
|
||||
|
||||
private SimulatedBinderProviderEvent(@NonNull String providerName, int eventType,
|
||||
@Nullable TimeZoneProviderEvent timeZoneProviderEvent) {
|
||||
this.mProviderName = Objects.requireNonNull(providerName);
|
||||
this.mType = eventType;
|
||||
this.mTimeZoneProviderEvent = timeZoneProviderEvent;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
String getProviderName() {
|
||||
return mProviderName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
TimeZoneProviderEvent getTimeZoneProviderEvent() {
|
||||
return mTimeZoneProviderEvent;
|
||||
}
|
||||
|
||||
int getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
/** 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|timeZoneProviderEvent"
|
||||
+ " <location time zone event args>]");
|
||||
pw.println();
|
||||
pw.println("<provider name> = " + VALID_PROVIDER_NAMES);
|
||||
pw.println("<time zone provider event args> ="
|
||||
+ " [PERMANENT_FAILURE|UNCERTAIN|SUGGESTION <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 "timeZoneProviderEvent": {
|
||||
TimeZoneProviderEvent event = parseTimeZoneProviderEventArgs(shellCommand);
|
||||
return new SimulatedBinderProviderEvent(providerName,
|
||||
INJECTED_EVENT_TYPE_LOCATION_TIME_ZONE_EVENT, event);
|
||||
}
|
||||
default: {
|
||||
throw new IllegalArgumentException("Unknown simulated event type=" + injectedEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static TimeZoneProviderEvent parseTimeZoneProviderEventArgs(ShellCommand shellCommand) {
|
||||
TimeZoneProviderEvent event;
|
||||
String eventTypeString = shellCommand.getNextArgRequired();
|
||||
switch (eventTypeString.toUpperCase()) {
|
||||
case "PERMANENT_FAILURE": {
|
||||
event = TimeZoneProviderEvent.createPermanentFailureEvent("Simulated");
|
||||
break;
|
||||
}
|
||||
case "UNCERTAIN": {
|
||||
event = TimeZoneProviderEvent.createUncertainEvent();
|
||||
break;
|
||||
}
|
||||
case "SUGGESTION": {
|
||||
TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder()
|
||||
.setElapsedRealtimeMillis(SystemClock.elapsedRealtime())
|
||||
.setTimeZoneIds(parseTimeZoneArgs(shellCommand))
|
||||
.build();
|
||||
event = TimeZoneProviderEvent.createSuggestionEvent(suggestion);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new IllegalArgumentException("Error: Unknown eventType: " + eventTypeString);
|
||||
}
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
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
|
||||
+ ", mType=" + mType
|
||||
+ ", mTimeZoneProviderEvent=" + mTimeZoneProviderEvent
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
@@ -16,18 +16,23 @@
|
||||
|
||||
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 static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_ERROR_KEY;
|
||||
import static android.service.timezone.TimeZoneProviderService.TEST_COMMAND_RESULT_SUCCESS_KEY;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.RemoteCallback;
|
||||
import android.os.SystemClock;
|
||||
import android.service.timezone.TimeZoneProviderSuggestion;
|
||||
import android.util.IndentingPrintWriter;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.server.timezonedetector.ReferenceWithHistory;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@@ -36,6 +41,17 @@ import java.util.Objects;
|
||||
*/
|
||||
class SimulatedLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy {
|
||||
|
||||
private static final String TEST_COMMAND_NAME_ON_BIND = "on_bind";
|
||||
private static final String TEST_COMMAND_NAME_ON_UNBIND = "on_unbind";
|
||||
private static final String TEST_COMMAND_NAME_PERM_FAIL = "perm_fail";
|
||||
private static final String TEST_COMMAND_NAME_UNCERTAIN = "uncertain";
|
||||
private static final String TEST_COMMAND_NAME_SUCCESS = "success";
|
||||
|
||||
/**
|
||||
* Used for {@link #TEST_COMMAND_NAME_SUCCESS}.
|
||||
*/
|
||||
private static final String KEY_TIME_ZONE = "tz";
|
||||
|
||||
@GuardedBy("mSharedLock")
|
||||
@NonNull private TimeZoneProviderRequest mRequest;
|
||||
|
||||
@@ -53,38 +69,56 @@ class SimulatedLocationTimeZoneProviderProxy extends LocationTimeZoneProviderPro
|
||||
// No-op - nothing to do for the simulated provider.
|
||||
}
|
||||
|
||||
void simulate(@NonNull SimulatedBinderProviderEvent event) {
|
||||
void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) {
|
||||
mThreadingDomain.assertCurrentThread();
|
||||
|
||||
Objects.requireNonNull(event);
|
||||
Objects.requireNonNull(testCommand);
|
||||
|
||||
synchronized (mSharedLock) {
|
||||
switch (event.getType()) {
|
||||
case INJECTED_EVENT_TYPE_ON_BIND: {
|
||||
mLastEvent.set("Simulating onProviderBound(), event=" + event);
|
||||
Bundle resultBundle = new Bundle();
|
||||
switch (testCommand.getName()) {
|
||||
case TEST_COMMAND_NAME_ON_BIND: {
|
||||
mLastEvent.set("Simulating onProviderBound(), testCommand=" + testCommand);
|
||||
mThreadingDomain.post(this::onBindOnHandlerThread);
|
||||
resultBundle.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, true);
|
||||
break;
|
||||
}
|
||||
case INJECTED_EVENT_TYPE_ON_UNBIND: {
|
||||
mLastEvent.set("Simulating onProviderUnbound(), event=" + event);
|
||||
case TEST_COMMAND_NAME_ON_UNBIND: {
|
||||
mLastEvent.set("Simulating onProviderUnbound(), testCommand=" + testCommand);
|
||||
mThreadingDomain.post(this::onUnbindOnHandlerThread);
|
||||
resultBundle.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, true);
|
||||
break;
|
||||
}
|
||||
case INJECTED_EVENT_TYPE_LOCATION_TIME_ZONE_EVENT: {
|
||||
case TEST_COMMAND_NAME_PERM_FAIL:
|
||||
case TEST_COMMAND_NAME_UNCERTAIN:
|
||||
case TEST_COMMAND_NAME_SUCCESS: {
|
||||
if (!mRequest.sendUpdates()) {
|
||||
mLastEvent.set("Test event=" + event + " is testing an invalid case:"
|
||||
+ " reporting is off. mRequest=" + mRequest);
|
||||
String errorMsg = "testCommand=" + testCommand
|
||||
+ " is testing an invalid case:"
|
||||
+ " updates are off. mRequest=" + mRequest;
|
||||
mLastEvent.set(errorMsg);
|
||||
resultBundle.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, false);
|
||||
resultBundle.putString(TEST_COMMAND_RESULT_ERROR_KEY, errorMsg);
|
||||
break;
|
||||
}
|
||||
mLastEvent.set("Simulating TimeZoneProviderResult, event=" + event);
|
||||
handleTimeZoneProviderEvent(event.getTimeZoneProviderEvent());
|
||||
mLastEvent.set("Simulating TimeZoneProviderEvent, testCommand=" + testCommand);
|
||||
TimeZoneProviderEvent timeZoneProviderEvent =
|
||||
createTimeZoneProviderEventFromTestCommand(testCommand);
|
||||
handleTimeZoneProviderEvent(timeZoneProviderEvent);
|
||||
resultBundle.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, true);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
mLastEvent.set("Unknown simulated event type. event=" + event);
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown simulated event type. event=" + event);
|
||||
String errorMsg = "Unknown test event type. testCommand=" + testCommand;
|
||||
mLastEvent.set(errorMsg);
|
||||
resultBundle.putBoolean(TEST_COMMAND_RESULT_SUCCESS_KEY, false);
|
||||
resultBundle.putString(TEST_COMMAND_RESULT_ERROR_KEY, errorMsg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (callback != null) {
|
||||
callback.sendResult(resultBundle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,4 +161,47 @@ class SimulatedLocationTimeZoneProviderProxy extends LocationTimeZoneProviderPro
|
||||
ipw.decreaseIndent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the command line options that to create a {@link TestCommand} that can be passed to
|
||||
* {@link #createTimeZoneProviderEventFromTestCommand(TestCommand)}.
|
||||
*/
|
||||
static void printTestCommandShellHelp(@NonNull PrintWriter pw) {
|
||||
pw.printf("%s\n", TEST_COMMAND_NAME_ON_BIND);
|
||||
pw.printf("%s\n", TEST_COMMAND_NAME_ON_UNBIND);
|
||||
pw.printf("%s\n", TEST_COMMAND_NAME_PERM_FAIL);
|
||||
pw.printf("%s\n", TEST_COMMAND_NAME_UNCERTAIN);
|
||||
pw.printf("%s %s=string_array:<time zone id>[&<time zone id>]+\n",
|
||||
TEST_COMMAND_NAME_SUCCESS, KEY_TIME_ZONE);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static TimeZoneProviderEvent createTimeZoneProviderEventFromTestCommand(
|
||||
@NonNull TestCommand testCommand) {
|
||||
String name = testCommand.getName();
|
||||
switch (name) {
|
||||
case TEST_COMMAND_NAME_PERM_FAIL: {
|
||||
return TimeZoneProviderEvent.createPermanentFailureEvent("Simulated failure");
|
||||
}
|
||||
case TEST_COMMAND_NAME_UNCERTAIN: {
|
||||
return TimeZoneProviderEvent.createUncertainEvent();
|
||||
}
|
||||
case TEST_COMMAND_NAME_SUCCESS: {
|
||||
Bundle args = testCommand.getArgs();
|
||||
String[] timeZoneIds = args.getStringArray(KEY_TIME_ZONE);
|
||||
if (timeZoneIds == null) {
|
||||
throw new IllegalArgumentException("No " + KEY_TIME_ZONE + " arg found");
|
||||
}
|
||||
TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder()
|
||||
.setTimeZoneIds(Arrays.asList(timeZoneIds))
|
||||
.setElapsedRealtimeMillis(SystemClock.elapsedRealtime())
|
||||
.build();
|
||||
return TimeZoneProviderEvent.createSuggestionEvent(suggestion);
|
||||
}
|
||||
default: {
|
||||
String msg = String.format("Error: Unknown command name %s", name);
|
||||
throw new IllegalArgumentException(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.ShellCommand;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A command used to trigger behaviors in a component during tests. Routing to the correct
|
||||
* component is not handled by this class. The meaning of the {@code name} and {@code args}
|
||||
* properties are component-specific.
|
||||
*
|
||||
* <p>{@link TestCommand}s can be encoded as arguments in a shell command. See
|
||||
* {@link #createFromShellCommandArgs(ShellCommand)} and {@link
|
||||
* #printShellCommandEncodingHelp(PrintWriter)}.
|
||||
*/
|
||||
final class TestCommand {
|
||||
|
||||
private static final Pattern SHELL_ARG_PATTERN = Pattern.compile("([^=]+)=([^:]+):(.*)");
|
||||
private static final Pattern SHELL_ARG_VALUE_SPLIT_PATTERN = Pattern.compile("&");
|
||||
|
||||
@NonNull private final String mName;
|
||||
@NonNull private final Bundle mArgs;
|
||||
|
||||
/** Creates a {@link TestCommand} from components. */
|
||||
private TestCommand(@NonNull String type, @NonNull Bundle args) {
|
||||
mName = Objects.requireNonNull(type);
|
||||
mArgs = Objects.requireNonNull(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link TestCommand} from a {@link ShellCommand}'s remaining arguments.
|
||||
*
|
||||
* See {@link #printShellCommandEncodingHelp(PrintWriter)} for encoding details.
|
||||
*/
|
||||
@NonNull
|
||||
public static TestCommand createFromShellCommandArgs(@NonNull ShellCommand shellCommand) {
|
||||
String name = shellCommand.getNextArgRequired();
|
||||
Bundle args = new Bundle();
|
||||
String argKeyAndValue;
|
||||
while ((argKeyAndValue = shellCommand.getNextArg()) != null) {
|
||||
Matcher matcher = SHELL_ARG_PATTERN.matcher(argKeyAndValue);
|
||||
if (!matcher.matches()) {
|
||||
throw new IllegalArgumentException(
|
||||
argKeyAndValue + " does not match " + SHELL_ARG_PATTERN);
|
||||
}
|
||||
String key = matcher.group(1);
|
||||
String type = matcher.group(2);
|
||||
String encodedValue = matcher.group(3);
|
||||
Object value = getTypedValue(type, encodedValue);
|
||||
args.putObject(key, value);
|
||||
}
|
||||
return new TestCommand(name, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the command's name.
|
||||
*/
|
||||
@NonNull
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the arg values. Returns an empty bundle if there are no args.
|
||||
*/
|
||||
@NonNull
|
||||
public Bundle getArgs() {
|
||||
return mArgs.deepCopy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TestCommand{"
|
||||
+ "mName=" + mName
|
||||
+ ", mArgs=" + mArgs
|
||||
+ '}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the text format that {@link #createFromShellCommandArgs(ShellCommand)} understands.
|
||||
*/
|
||||
public static void printShellCommandEncodingHelp(@NonNull PrintWriter pw) {
|
||||
pw.println("Test commands are encoded on the command line as: <name> <arg>*");
|
||||
pw.println();
|
||||
pw.println("The <name> is a string");
|
||||
pw.println("The <arg> encoding is: \"key=type:value\"");
|
||||
pw.println();
|
||||
pw.println("e.g. \"myKey=string:myValue\" represents an argument with the key \"myKey\""
|
||||
+ " and a string value of \"myValue\"");
|
||||
pw.println("Values are one or more URI-encoded strings separated by & characters. Only some"
|
||||
+ " types support multiple values, e.g. string arrays.");
|
||||
pw.println();
|
||||
pw.println("Recognized types are: string, boolean, double, long, string_array.");
|
||||
pw.println();
|
||||
pw.println("When passing test commands via adb shell, the & can be escaped by quoting the"
|
||||
+ " <arg> and escaping the & with \\");
|
||||
pw.println("For example:");
|
||||
pw.println(" $ adb shell ... my-command \"key1=string_array:value1\\&value2\"");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
TestCommand that = (TestCommand) o;
|
||||
return mName.equals(that.mName)
|
||||
&& mArgs.kindofEquals(that.mArgs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(mName, mArgs);
|
||||
}
|
||||
|
||||
|
||||
private static Object getTypedValue(String type, String encodedValue) {
|
||||
// The value is stored in a URL encoding. Multiple value types have values separated with
|
||||
// a & character.
|
||||
String[] values = SHELL_ARG_VALUE_SPLIT_PATTERN.split(encodedValue);
|
||||
|
||||
// URI decode the values.
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = Uri.decode(values[i]);
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "boolean": {
|
||||
checkSingleValue(values);
|
||||
return Boolean.parseBoolean(values[0]);
|
||||
}
|
||||
case "double": {
|
||||
checkSingleValue(values);
|
||||
return Double.parseDouble(values[0]);
|
||||
}
|
||||
case "long": {
|
||||
checkSingleValue(values);
|
||||
return Long.parseLong(values[0]);
|
||||
}
|
||||
case "string": {
|
||||
checkSingleValue(values);
|
||||
return values[0];
|
||||
}
|
||||
case "string_array": {
|
||||
return values;
|
||||
}
|
||||
default: {
|
||||
throw new IllegalArgumentException("Unknown type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void checkSingleValue(String[] values) {
|
||||
if (values.length != 1) {
|
||||
throw new IllegalArgumentException("Expected a single value, but there were multiple: "
|
||||
+ Arrays.toString(values));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,14 @@ abstract class ThreadingDomain {
|
||||
Preconditions.checkArgument(Thread.currentThread() == getThread());
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts the currently executing thread is not the one associated with this threading domain.
|
||||
* Generally useful for documenting expectations in the code and avoiding deadlocks.
|
||||
*/
|
||||
void assertNotCurrentThread() {
|
||||
Preconditions.checkArgument(Thread.currentThread() != getThread());
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the supplied runnable on the threading domain's thread.
|
||||
*/
|
||||
|
||||
@@ -84,6 +84,28 @@ public class HandlerThreadingDomainTest {
|
||||
assertFalse(exceptionThrown.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void assertNotCurrentThread() throws Exception {
|
||||
ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler);
|
||||
|
||||
// Expect no exception (current thread != handler thread)
|
||||
domain.assertNotCurrentThread();
|
||||
|
||||
AtomicBoolean exceptionThrown = new AtomicBoolean(false);
|
||||
LatchedRunnable testCode = new LatchedRunnable(() -> {
|
||||
// Expect an exception (current thread == handler thread)
|
||||
try {
|
||||
domain.assertNotCurrentThread();
|
||||
fail("Expected exception");
|
||||
} catch (RuntimeException expected) {
|
||||
exceptionThrown.set(true);
|
||||
}
|
||||
});
|
||||
mTestHandler.post(testCode);
|
||||
testCode.assertCompletesWithin(60, TimeUnit.SECONDS);
|
||||
assertTrue(exceptionThrown.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void post() throws Exception {
|
||||
ThreadingDomain domain = new HandlerThreadingDomain(mTestHandler);
|
||||
|
||||
Reference in New Issue
Block a user