blob: d8d44d47be624be5e57cdfd9acb148d6557a5368 [file] [log] [blame]
/*
* 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.timezonedetector.location;
import static android.app.time.LocationTimeZoneManager.SERVICE_NAME;
import static com.android.server.timezonedetector.ServiceConfigAccessor.PROVIDER_MODE_DISABLED;
import static com.android.server.timezonedetector.ServiceConfigAccessor.PROVIDER_MODE_SIMULATED;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
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.service.timezone.TimeZoneProviderService;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.Preconditions;
import com.android.server.FgThread;
import com.android.server.SystemService;
import com.android.server.timezonedetector.Dumpable;
import com.android.server.timezonedetector.ServiceConfigAccessor;
import com.android.server.timezonedetector.TimeZoneDetectorInternal;
import com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderMetricsLogger;
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},
* which determines what {@link com.android.server.timezonedetector.GeolocationTimeZoneSuggestion}
* are made to the {@link TimeZoneDetectorInternal}, and the {@link LocationTimeZoneProvider}s that
* (indirectly) generate {@link TimeZoneProviderEvent}s.
*
* <p>For details of the time zone suggestion behavior, see {@link
* LocationTimeZoneProviderController}.
*
* <p>Implementation details:
*
* <p>For simplicity, with the exception of a few outliers like {@link #dump}, all processing in
* this service (and package-private helper objects) takes place on a single thread / handler, the
* one indicated by {@link ThreadingDomain}. Because methods like {@link #dump} can be invoked on
* another thread, the service and its related objects must still be thread-safe.
*
* <p>For testing / reproduction of bugs, it is possible to put providers into "simulation
* mode" where the real binder clients are replaced by {@link
* SimulatedLocationTimeZoneProviderProxy}. This means that the real client providers are never
* bound (ensuring no real location events will be received) and simulated events / behaviors
* can be injected via the command line.
*
* <p>See {@code adb shell cmd location_time_zone_manager help}" for details and more options.
*/
public class LocationTimeZoneManagerService extends Binder {
/**
* Controls lifecycle of the {@link LocationTimeZoneManagerService}.
*/
public static class Lifecycle extends SystemService {
private LocationTimeZoneManagerService mService;
@NonNull
private final ServiceConfigAccessor mServerConfigAccessor;
public Lifecycle(@NonNull Context context) {
super(Objects.requireNonNull(context));
mServerConfigAccessor = ServiceConfigAccessor.getInstance(context);
}
@Override
public void onStart() {
Context context = getContext();
if (mServerConfigAccessor.isGeoTimeZoneDetectionFeatureSupportedInConfig()) {
mService = new LocationTimeZoneManagerService(context);
// The service currently exposes no LocalService or Binder API, but it extends
// Binder and is registered as a binder service so it can receive shell commands.
publishBinderService(SERVICE_NAME, mService);
} else {
Slog.d(TAG, "Geo time zone detection feature is disabled in config");
}
}
@Override
public void onBootPhase(@BootPhase int phase) {
if (mServerConfigAccessor.isGeoTimeZoneDetectionFeatureSupportedInConfig()) {
if (phase == PHASE_SYSTEM_SERVICES_READY) {
// The location service must be functioning after this boot phase.
mService.onSystemReady();
} else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
// Some providers rely on non-platform code (e.g. gcore), so we wait to
// initialize providers until third party code is allowed to run.
mService.onSystemThirdPartyAppsCanStart();
}
}
}
}
static final String TAG = "LocationTZDetector";
private static final long BLOCKING_OP_WAIT_DURATION_MILLIS = Duration.ofSeconds(20).toMillis();
private static final String ATTRIBUTION_TAG = "LocationTimeZoneService";
@GuardedBy("mSharedLock")
private final ProviderConfig mPrimaryProviderConfig = new ProviderConfig(
0 /* index */, "primary",
TimeZoneProviderService.PRIMARY_LOCATION_TIME_ZONE_PROVIDER_SERVICE_INTERFACE);
@GuardedBy("mSharedLock")
private final ProviderConfig mSecondaryProviderConfig = new ProviderConfig(
1 /* index */, "secondary",
TimeZoneProviderService.SECONDARY_LOCATION_TIME_ZONE_PROVIDER_SERVICE_INTERFACE);
@NonNull private final Context mContext;
/**
* The {@link ThreadingDomain} used to supply the shared lock object used by the controller and
* related components.
*
* <p>Most operations are executed on the associated handler thread <em>but not all</em>, hence
* the requirement for additional synchronization using a shared lock.
*/
@NonNull private final ThreadingDomain mThreadingDomain;
/** A handler associated with the {@link #mThreadingDomain}. */
@NonNull private final Handler mHandler;
/** The shared lock from {@link #mThreadingDomain}. */
@NonNull private final Object mSharedLock;
@NonNull
private final ServiceConfigAccessor mServiceConfigAccessor;
// Lazily initialized. Can be null if the service has been stopped.
@GuardedBy("mSharedLock")
private ControllerImpl mLocationTimeZoneDetectorController;
// Lazily initialized. Can be null if the service has been stopped.
@GuardedBy("mSharedLock")
private ControllerEnvironmentImpl mEnvironment;
LocationTimeZoneManagerService(Context context) {
mContext = context.createAttributionContext(ATTRIBUTION_TAG);
mHandler = FgThread.getHandler();
mThreadingDomain = new HandlerThreadingDomain(mHandler);
mSharedLock = mThreadingDomain.getLockObject();
mServiceConfigAccessor = ServiceConfigAccessor.getInstance(mContext);
}
// According to the SystemService docs: All lifecycle methods are called from the system
// server's main looper thread.
void onSystemReady() {
mServiceConfigAccessor.addListener(this::handleServiceConfigurationChangedOnMainThread);
}
private void handleServiceConfigurationChangedOnMainThread() {
// This method is called on the main thread, but service logic takes place on the threading
// domain thread, so we post the work there.
// The way all service-level configuration changes are handled is to just restart this
// service - this is simple and effective, and service configuration changes should be rare.
mThreadingDomain.post(this::restartIfRequiredOnDomainThread);
}
private void restartIfRequiredOnDomainThread() {
mThreadingDomain.assertCurrentThread();
synchronized (mSharedLock) {
// Avoid starting the service if it is currently stopped. This is required because
// server flags are used by tests to set behavior with the service stopped, and we don't
// want the service being restarted after each flag is set.
if (mLocationTimeZoneDetectorController != null) {
// Stop and start the service, waiting until completion.
stopOnDomainThread();
startOnDomainThread();
}
}
}
// According to the SystemService docs: All lifecycle methods are called from the system
// server's main looper thread.
void onSystemThirdPartyAppsCanStart() {
// Do not wait for completion as it would delay boot.
final boolean waitForCompletion = false;
startInternal(waitForCompletion);
}
/**
* Starts the service during server initialization or during tests after a call to
* {@link #stop()}.
*
* <p>Because this method posts work to the {@code mThreadingDomain} thread and waits for
* completion, it cannot be called from the {@code mThreadingDomain} thread.
*/
void start() {
enforceManageTimeZoneDetectorPermission();
final boolean waitForCompletion = true;
startInternal(waitForCompletion);
}
/**
* Starts the service during server initialization, if the configuration changes or during tests
* after a call to {@link #stop()}.
*
* <p>To avoid tests needing to sleep, when {@code waitForCompletion} is {@code true}, this
* method will not return until all the system server components have started.
*
* <p>Because this method posts work to the {@code mThreadingDomain} thread, it cannot be
* called from the {@code mThreadingDomain} thread when {@code waitForCompletion} is true.
*/
private void startInternal(boolean waitForCompletion) {
Runnable runnable = this::startOnDomainThread;
if (waitForCompletion) {
mThreadingDomain.postAndWait(runnable, BLOCKING_OP_WAIT_DURATION_MILLIS);
} else {
mThreadingDomain.post(runnable);
}
}
private void startOnDomainThread() {
mThreadingDomain.assertCurrentThread();
synchronized (mSharedLock) {
if (!mServiceConfigAccessor.isGeoTimeZoneDetectionFeatureSupported()) {
debugLog("Not starting " + SERVICE_NAME + ": it is disabled in service config");
return;
}
if (mLocationTimeZoneDetectorController == null) {
LocationTimeZoneProvider primary = mPrimaryProviderConfig.createProvider();
LocationTimeZoneProvider secondary = mSecondaryProviderConfig.createProvider();
ControllerImpl controller =
new ControllerImpl(mThreadingDomain, primary, secondary);
ControllerEnvironmentImpl environment = new ControllerEnvironmentImpl(
mThreadingDomain, mServiceConfigAccessor, controller);
ControllerCallbackImpl callback = new ControllerCallbackImpl(mThreadingDomain);
controller.initialize(environment, callback);
mEnvironment = environment;
mLocationTimeZoneDetectorController = controller;
}
}
}
/**
* Stops the service for tests and other rare cases. To avoid tests needing to sleep, this
* method will not return until all the system server components have stopped.
*
* <p>Because this method posts work to the {@code mThreadingDomain} thread and waits it cannot
* be called from the {@code mThreadingDomain} thread.
*/
void stop() {
enforceManageTimeZoneDetectorPermission();
mThreadingDomain.postAndWait(this::stopOnDomainThread, BLOCKING_OP_WAIT_DURATION_MILLIS);
}
private void stopOnDomainThread() {
mThreadingDomain.assertCurrentThread();
synchronized (mSharedLock) {
if (mLocationTimeZoneDetectorController != null) {
mLocationTimeZoneDetectorController.destroy();
mLocationTimeZoneDetectorController = null;
mEnvironment.destroy();
mEnvironment = null;
}
}
}
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out,
FileDescriptor err, String[] args, ShellCallback callback,
ResultReceiver resultReceiver) {
(new LocationTimeZoneManagerShellCommand(this)).exec(
this, in, out, err, args, callback, resultReceiver);
}
/** Sets this service into provider state recording mode for tests. */
void setProviderStateRecordingEnabled(boolean enabled) {
enforceManageTimeZoneDetectorPermission();
mThreadingDomain.postAndWait(() -> {
synchronized (mSharedLock) {
if (mLocationTimeZoneDetectorController != null) {
mLocationTimeZoneDetectorController.setProviderStateRecordingEnabled(enabled);
}
}
}, BLOCKING_OP_WAIT_DURATION_MILLIS);
}
/**
* Returns a snapshot of the current controller state for tests. Returns {@code null} if the
* service is stopped.
*/
@Nullable
LocationTimeZoneManagerServiceState getStateForTests() {
enforceManageTimeZoneDetectorPermission();
try {
return mThreadingDomain.postAndWait(
() -> {
synchronized (mSharedLock) {
if (mLocationTimeZoneDetectorController == null) {
return null;
}
return mLocationTimeZoneDetectorController.getStateForTests();
}
},
BLOCKING_OP_WAIT_DURATION_MILLIS);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Passes a {@link TestCommand} to the specified provider and waits for the response.
*/
@NonNull
Bundle handleProviderTestCommand(@IntRange(from = 0, to = 1) int providerIndex,
@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(
providerIndex, 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);
}
return resultReference.get();
}
@Override
protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw,
@Nullable String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
// Called on an arbitrary thread at any time.
synchronized (mSharedLock) {
ipw.println("LocationTimeZoneManagerService:");
ipw.increaseIndent();
ipw.println("Primary provider config:");
ipw.increaseIndent();
mPrimaryProviderConfig.dump(ipw, args);
ipw.decreaseIndent();
ipw.println("Secondary provider config:");
ipw.increaseIndent();
mSecondaryProviderConfig.dump(ipw, args);
ipw.decreaseIndent();
if (mLocationTimeZoneDetectorController == null) {
ipw.println("{Stopped}");
} else {
mLocationTimeZoneDetectorController.dump(ipw, args);
}
ipw.decreaseIndent();
}
}
static void debugLog(String msg) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Slog.d(TAG, msg);
}
}
static void infoLog(String msg) {
if (Log.isLoggable(TAG, Log.INFO)) {
Slog.i(TAG, msg);
}
}
static void warnLog(String msg) {
warnLog(msg, null);
}
static void warnLog(String msg, @Nullable Throwable t) {
if (Log.isLoggable(TAG, Log.WARN)) {
Slog.w(TAG, msg, t);
}
}
private void enforceManageTimeZoneDetectorPermission() {
mContext.enforceCallingPermission(
android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION,
"manage time and time zone detection");
}
/** An inner class for managing a provider's config. */
private final class ProviderConfig implements Dumpable {
@IntRange(from = 0, to = 1) private final int mIndex;
@NonNull private final String mName;
@NonNull private final String mServiceAction;
ProviderConfig(@IntRange(from = 0, to = 1) int index, @NonNull String name,
@NonNull String serviceAction) {
Preconditions.checkArgument(index >= 0 && index <= 1);
mIndex = index;
mName = Objects.requireNonNull(name);
mServiceAction = Objects.requireNonNull(serviceAction);
}
@NonNull
LocationTimeZoneProvider createProvider() {
LocationTimeZoneProviderProxy proxy = createProxy();
ProviderMetricsLogger providerMetricsLogger = new RealProviderMetricsLogger(mIndex);
return new BinderLocationTimeZoneProvider(
providerMetricsLogger, mThreadingDomain, mName, proxy);
}
@GuardedBy("mSharedLock")
@Override
public void dump(IndentingPrintWriter ipw, String[] args) {
ipw.printf("getMode()=%s\n", getMode());
ipw.printf("getPackageName()=%s\n", getPackageName());
}
@NonNull
private LocationTimeZoneProviderProxy createProxy() {
String mode = getMode();
if (Objects.equals(mode, PROVIDER_MODE_SIMULATED)) {
return new SimulatedLocationTimeZoneProviderProxy(mContext, mThreadingDomain);
} else if (Objects.equals(mode, PROVIDER_MODE_DISABLED)) {
return new NullLocationTimeZoneProviderProxy(mContext, mThreadingDomain);
} else {
// mode == PROVIDER_MODE_OVERRIDE_ENABLED (or unknown).
return createRealProxy();
}
}
/** Returns the mode of the provider. */
@NonNull
private String getMode() {
if (mIndex == 0) {
return mServiceConfigAccessor.getPrimaryLocationTimeZoneProviderMode();
} else {
return mServiceConfigAccessor.getSecondaryLocationTimeZoneProviderMode();
}
}
@NonNull
private RealLocationTimeZoneProviderProxy createRealProxy() {
String providerServiceAction = mServiceAction;
String providerPackageName = getPackageName();
return new RealLocationTimeZoneProviderProxy(
mContext, mHandler, mThreadingDomain, providerServiceAction,
providerPackageName);
}
@NonNull
private String getPackageName() {
if (mIndex == 0) {
return mServiceConfigAccessor.getPrimaryLocationTimeZoneProviderPackageName();
} else {
return mServiceConfigAccessor.getSecondaryLocationTimeZoneProviderPackageName();
}
}
}
}