All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.server.timezonedetector.location.LocationTimeZoneManagerService Maven / Gradle / Ivy

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

For details of the time zone suggestion behavior, see {@link * LocationTimeZoneProviderController}. * *

Implementation details: * *

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

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

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

Most operations are executed on the associated handler thread but not all, 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()}. * *

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()}. * *

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

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

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





© 2015 - 2025 Weber Informatics LLC | Privacy Policy