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:
Neil Fuller
2021-01-06 18:05:16 +00:00
parent 5736696b68
commit 616df78f9e
14 changed files with 623 additions and 280 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.
*/

View File

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

View File

@@ -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
+ '}';
}
}

View File

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

View File

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

View File

@@ -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.
*/

View File

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