blob: b1b537be1d295d91f0908c229a379c4b1804cff8 [file] [log] [blame]
/*
* Copyright (C) 2019 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;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.time.ITimeZoneDetectorListener;
import android.app.time.TimeZoneCapabilitiesAndConfig;
import android.app.time.TimeZoneConfiguration;
import android.app.timezonedetector.ITimeZoneDetectorService;
import android.app.timezonedetector.ManualTimeZoneSuggestion;
import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
import android.content.Context;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.util.ArrayMap;
import android.util.IndentingPrintWriter;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.DumpUtils;
import com.android.server.FgThread;
import com.android.server.SystemService;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Objects;
/**
* The implementation of ITimeZoneDetectorService.aidl.
*
* <p>This service is implemented as a wrapper around {@link TimeZoneDetectorStrategy}. It handles
* interaction with Android framework classes, enforcing caller permissions, capturing user identity
* and making calls async, leaving the (consequently more testable) {@link TimeZoneDetectorStrategy}
* implementation to deal with the logic around time zone detection.
*/
public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub
implements IBinder.DeathRecipient {
static final String TAG = "time_zone_detector";
/**
* Handles the service lifecycle for {@link TimeZoneDetectorService} and
* {@link TimeZoneDetectorInternalImpl}.
*/
public static final class Lifecycle extends SystemService {
public Lifecycle(@NonNull Context context) {
super(context);
}
@Override
public void onStart() {
// Obtain / create the shared dependencies.
Context context = getContext();
Handler handler = FgThread.getHandler();
ServiceConfigAccessor serviceConfigAccessor =
ServiceConfigAccessor.getInstance(context);
TimeZoneDetectorStrategy timeZoneDetectorStrategy =
TimeZoneDetectorStrategyImpl.create(context, handler, serviceConfigAccessor);
// Create and publish the local service for use by internal callers.
TimeZoneDetectorInternal internal =
new TimeZoneDetectorInternalImpl(context, handler, timeZoneDetectorStrategy);
publishLocalService(TimeZoneDetectorInternal.class, internal);
// Publish the binder service so it can be accessed from other (appropriately
// permissioned) processes.
TimeZoneDetectorService service = TimeZoneDetectorService.create(
context, handler, timeZoneDetectorStrategy);
publishBinderService(Context.TIME_ZONE_DETECTOR_SERVICE, service);
}
}
@NonNull
private final Context mContext;
@NonNull
private final Handler mHandler;
@NonNull
private final CallerIdentityInjector mCallerIdentityInjector;
@NonNull
private final TimeZoneDetectorStrategy mTimeZoneDetectorStrategy;
/**
* Holds the listeners. The key is the {@link IBinder} associated with the listener, the value
* is the listener itself.
*/
@GuardedBy("mListeners")
@NonNull
private final ArrayMap<IBinder, ITimeZoneDetectorListener> mListeners =
new ArrayMap<>();
private static TimeZoneDetectorService create(
@NonNull Context context, @NonNull Handler handler,
@NonNull TimeZoneDetectorStrategy timeZoneDetectorStrategy) {
CallerIdentityInjector callerIdentityInjector = CallerIdentityInjector.REAL;
TimeZoneDetectorService service = new TimeZoneDetectorService(
context, handler, callerIdentityInjector, timeZoneDetectorStrategy);
return service;
}
@VisibleForTesting
public TimeZoneDetectorService(@NonNull Context context, @NonNull Handler handler,
@NonNull CallerIdentityInjector callerIdentityInjector,
@NonNull TimeZoneDetectorStrategy timeZoneDetectorStrategy) {
mContext = Objects.requireNonNull(context);
mHandler = Objects.requireNonNull(handler);
mCallerIdentityInjector = Objects.requireNonNull(callerIdentityInjector);
mTimeZoneDetectorStrategy = Objects.requireNonNull(timeZoneDetectorStrategy);
// Wire up a change listener so that ITimeZoneDetectorListeners can be notified when
// the configuration changes for any reason.
mTimeZoneDetectorStrategy.addConfigChangeListener(this::handleConfigurationChanged);
}
@Override
@NonNull
public TimeZoneCapabilitiesAndConfig getCapabilitiesAndConfig() {
int userId = mCallerIdentityInjector.getCallingUserId();
return getCapabilitiesAndConfig(userId);
}
TimeZoneCapabilitiesAndConfig getCapabilitiesAndConfig(@UserIdInt int userId) {
enforceManageTimeZoneDetectorPermission();
final long token = mCallerIdentityInjector.clearCallingIdentity();
try {
ConfigurationInternal configurationInternal =
mTimeZoneDetectorStrategy.getConfigurationInternal(userId);
return configurationInternal.createCapabilitiesAndConfig();
} finally {
mCallerIdentityInjector.restoreCallingIdentity(token);
}
}
@Override
public boolean updateConfiguration(@NonNull TimeZoneConfiguration configuration) {
int callingUserId = mCallerIdentityInjector.getCallingUserId();
return updateConfiguration(callingUserId, configuration);
}
boolean updateConfiguration(
@UserIdInt int userId, @NonNull TimeZoneConfiguration configuration) {
userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
userId, false, false, "updateConfiguration", null);
enforceManageTimeZoneDetectorPermission();
Objects.requireNonNull(configuration);
final long token = mCallerIdentityInjector.clearCallingIdentity();
try {
return mTimeZoneDetectorStrategy.updateConfiguration(userId, configuration);
} finally {
mCallerIdentityInjector.restoreCallingIdentity(token);
}
}
@Override
public void addListener(@NonNull ITimeZoneDetectorListener listener) {
enforceManageTimeZoneDetectorPermission();
Objects.requireNonNull(listener);
synchronized (mListeners) {
IBinder listenerBinder = listener.asBinder();
if (mListeners.containsKey(listenerBinder)) {
return;
}
try {
// Ensure the reference to the listener will be removed if the client process dies.
listenerBinder.linkToDeath(this, 0 /* flags */);
// Only add the listener if we can linkToDeath().
mListeners.put(listenerBinder, listener);
} catch (RemoteException e) {
Slog.e(TAG, "Unable to linkToDeath() for listener=" + listener, e);
}
}
}
@Override
public void removeListener(@NonNull ITimeZoneDetectorListener listener) {
enforceManageTimeZoneDetectorPermission();
Objects.requireNonNull(listener);
synchronized (mListeners) {
IBinder listenerBinder = listener.asBinder();
boolean removedListener = false;
if (mListeners.remove(listenerBinder) != null) {
// Stop listening for the client process to die.
listenerBinder.unlinkToDeath(this, 0 /* flags */);
removedListener = true;
}
if (!removedListener) {
Slog.w(TAG, "Client asked to remove listener=" + listener
+ ", but no listeners were removed."
+ " mListeners=" + mListeners);
}
}
}
@Override
public void binderDied() {
// Should not be used as binderDied(IBinder who) is overridden.
Slog.wtf(TAG, "binderDied() called unexpectedly.");
}
/**
* Called when one of the ITimeZoneDetectorListener processes dies before calling
* {@link #removeListener(ITimeZoneDetectorListener)}.
*/
@Override
public void binderDied(IBinder who) {
synchronized (mListeners) {
boolean removedListener = false;
final int listenerCount = mListeners.size();
for (int listenerIndex = listenerCount - 1; listenerIndex >= 0; listenerIndex--) {
IBinder listenerBinder = mListeners.keyAt(listenerIndex);
if (listenerBinder.equals(who)) {
mListeners.removeAt(listenerIndex);
removedListener = true;
break;
}
}
if (!removedListener) {
Slog.w(TAG, "Notified of binder death for who=" + who
+ ", but did not remove any listeners."
+ " mConfigurationListeners=" + mListeners);
}
}
}
void handleConfigurationChanged() {
// Configuration has changed, but each user may have a different view of the configuration.
// It's possible that this will cause unnecessary notifications but that shouldn't be a
// problem.
synchronized (mListeners) {
final int listenerCount = mListeners.size();
for (int listenerIndex = 0; listenerIndex < listenerCount; listenerIndex++) {
ITimeZoneDetectorListener listener = mListeners.valueAt(listenerIndex);
try {
listener.onChange();
} catch (RemoteException e) {
Slog.w(TAG, "Unable to notify listener=" + listener, e);
}
}
}
}
/** Provided for command-line access. This is not exposed as a binder API. */
void suggestGeolocationTimeZone(@NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion) {
enforceSuggestGeolocationTimeZonePermission();
Objects.requireNonNull(timeZoneSuggestion);
mHandler.post(
() -> mTimeZoneDetectorStrategy.suggestGeolocationTimeZone(timeZoneSuggestion));
}
@Override
public boolean suggestManualTimeZone(@NonNull ManualTimeZoneSuggestion timeZoneSuggestion) {
enforceSuggestManualTimeZonePermission();
Objects.requireNonNull(timeZoneSuggestion);
int userId = mCallerIdentityInjector.getCallingUserId();
final long token = mCallerIdentityInjector.clearCallingIdentity();
try {
return mTimeZoneDetectorStrategy.suggestManualTimeZone(userId, timeZoneSuggestion);
} finally {
mCallerIdentityInjector.restoreCallingIdentity(token);
}
}
@Override
public void suggestTelephonyTimeZone(@NonNull TelephonyTimeZoneSuggestion timeZoneSuggestion) {
enforceSuggestTelephonyTimeZonePermission();
Objects.requireNonNull(timeZoneSuggestion);
mHandler.post(() -> mTimeZoneDetectorStrategy.suggestTelephonyTimeZone(timeZoneSuggestion));
}
boolean isTelephonyTimeZoneDetectionSupported() {
enforceManageTimeZoneDetectorPermission();
return ServiceConfigAccessor.getInstance(mContext)
.isTelephonyTimeZoneDetectionFeatureSupported();
}
boolean isGeoTimeZoneDetectionSupported() {
enforceManageTimeZoneDetectorPermission();
return ServiceConfigAccessor.getInstance(mContext)
.isGeoTimeZoneDetectionFeatureSupported();
}
@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);
mTimeZoneDetectorStrategy.dump(ipw, args);
ipw.flush();
}
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
new TimeZoneDetectorShellCommand(this).exec(
this, in, out, err, args, callback, resultReceiver);
}
private void enforceManageTimeZoneDetectorPermission() {
mContext.enforceCallingPermission(
android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION,
"manage time and time zone detection");
}
private void enforceSuggestGeolocationTimeZonePermission() {
// The associated method is only used for the shell command interface, it's not possible to
// call it via Binder, and Shell currently can set the time zone directly anyway.
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.SET_TIME_ZONE,
"suggest geolocation time zone");
}
private void enforceSuggestTelephonyTimeZonePermission() {
mContext.enforceCallingPermission(
android.Manifest.permission.SUGGEST_TELEPHONY_TIME_AND_ZONE,
"suggest telephony time and time zone");
}
private void enforceSuggestManualTimeZonePermission() {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.SUGGEST_MANUAL_TIME_AND_ZONE,
"suggest manual time and time zone");
}
}