Implementation of time user configuration update

This adds support for updating user-facing time detector configuration
via the TimeDetectorService.

Bug: 172891783
Bug: 229740080
Test: atest com.android.server.timedetector android.app.time
Change-Id: I109e59e44dbe085d456fc36bcd98375d118940c0
This commit is contained in:
Neil Fuller
2022-05-26 17:58:14 +01:00
parent 8247153b48
commit 7573cc80a6
6 changed files with 301 additions and 23 deletions

View File

@@ -0,0 +1,22 @@
/*
* Copyright (C) 2022 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 android.app.time;
/** {@hide} */
oneway interface ITimeDetectorListener {
void onChange();
}

View File

@@ -17,6 +17,7 @@
package android.app.timedetector;
import android.app.time.ExternalTimeSuggestion;
import android.app.time.ITimeDetectorListener;
import android.app.time.TimeCapabilitiesAndConfig;
import android.app.time.TimeConfiguration;
import android.app.timedetector.GnssTimeSuggestion;
@@ -39,6 +40,9 @@ import android.app.timedetector.TelephonyTimeSuggestion;
*/
interface ITimeDetectorService {
TimeCapabilitiesAndConfig getCapabilitiesAndConfig();
void addListener(ITimeDetectorListener listener);
void removeListener(ITimeDetectorListener listener);
boolean updateConfiguration(in TimeConfiguration timeConfiguration);
void suggestExternalTime( in ExternalTimeSuggestion timeSuggestion);

View File

@@ -53,6 +53,22 @@ public interface ServiceConfigAccessor {
@NonNull
ConfigurationInternal getCurrentUserConfigurationInternal();
/**
* Updates the configuration properties that control a device's time behavior.
*
* <p>This method returns {@code true} if the configuration was changed,
* {@code false} otherwise.
*/
boolean updateConfiguration(
@UserIdInt int userId, @NonNull TimeConfiguration requestedConfiguration);
/**
* Returns a snapshot of the configuration that controls time zone detector behavior for the
* specified user.
*/
@NonNull
ConfigurationInternal getConfigurationInternal(@UserIdInt int userId);
/**
* Returns the absolute threshold below which the system clock need not be updated. i.e. if
* setting the system clock would adjust it by less than this (either backwards or forwards)
@@ -75,20 +91,4 @@ public interface ServiceConfigAccessor {
*/
@NonNull
@Origin int[] getOriginPriorities();
/**
* Updates the configuration properties that control a device's time behavior.
*
* <p>This method returns {@code true} if the configuration was changed,
* {@code false} otherwise.
*/
boolean updateConfiguration(
@UserIdInt int userId, @NonNull TimeConfiguration requestedConfiguration);
/**
* Returns a snapshot of the configuration that controls time zone detector behavior for the
* specified user.
*/
@NonNull
ConfigurationInternal getConfigurationInternal(@UserIdInt int userId);
}

View File

@@ -19,7 +19,9 @@ package com.android.server.timedetector;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.time.ExternalTimeSuggestion;
import android.app.time.ITimeDetectorListener;
import android.app.time.TimeCapabilitiesAndConfig;
import android.app.time.TimeConfiguration;
import android.app.timedetector.GnssTimeSuggestion;
@@ -28,11 +30,17 @@ import android.app.timedetector.ManualTimeSuggestion;
import android.app.timedetector.NetworkTimeSuggestion;
import android.app.timedetector.TelephonyTimeSuggestion;
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;
@@ -51,7 +59,8 @@ import java.util.Objects;
* and making calls async, leaving the (consequently more testable) {@link TimeDetectorStrategy}
* implementation to deal with the logic around time detection.
*/
public final class TimeDetectorService extends ITimeDetectorService.Stub {
public final class TimeDetectorService extends ITimeDetectorService.Stub
implements IBinder.DeathRecipient {
static final String TAG = "time_detector";
public static class Lifecycle extends SystemService {
@@ -85,6 +94,14 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub {
@NonNull private final ServiceConfigAccessor mServiceConfigAccessor;
@NonNull private final TimeDetectorStrategy mTimeDetectorStrategy;
/**
* 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, ITimeDetectorListener> mListeners = new ArrayMap<>();
@VisibleForTesting
public TimeDetectorService(@NonNull Context context, @NonNull Handler handler,
@NonNull ServiceConfigAccessor serviceConfigAccessor,
@@ -103,6 +120,11 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub {
mServiceConfigAccessor = Objects.requireNonNull(serviceConfigAccessor);
mTimeDetectorStrategy = Objects.requireNonNull(timeDetectorStrategy);
mCallerIdentityInjector = Objects.requireNonNull(callerIdentityInjector);
// Wire up a change listener so that ITimeZoneDetectorListeners can be notified when
// the configuration changes for any reason.
mServiceConfigAccessor.addConfigurationInternalChangeListener(
() -> mHandler.post(this::handleConfigurationInternalChangedOnHandlerThread));
}
@Override
@@ -126,10 +148,117 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub {
}
@Override
public boolean updateConfiguration(@NonNull TimeConfiguration timeConfiguration) {
public boolean updateConfiguration(@NonNull TimeConfiguration configuration) {
int callingUserId = mCallerIdentityInjector.getCallingUserId();
return updateConfiguration(callingUserId, configuration);
}
boolean updateConfiguration(@UserIdInt int userId, @NonNull TimeConfiguration configuration) {
// Resolve constants like USER_CURRENT to the true user ID as needed.
int resolvedUserId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
Binder.getCallingUid(), userId, false, false, "updateConfiguration", null);
enforceManageTimeDetectorPermission();
// TODO(b/172891783) Add actual logic
return false;
Objects.requireNonNull(configuration);
final long token = mCallerIdentityInjector.clearCallingIdentity();
try {
return mServiceConfigAccessor.updateConfiguration(resolvedUserId, configuration);
} finally {
mCallerIdentityInjector.restoreCallingIdentity(token);
}
}
@Override
public void addListener(@NonNull ITimeDetectorListener listener) {
enforceManageTimeDetectorPermission();
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 ITimeDetectorListener listener) {
enforceManageTimeDetectorPermission();
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 ITimeDetectorListener processes dies before calling
* {@link #removeListener(ITimeDetectorListener)}.
*/
@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."
+ " mListeners=" + mListeners);
}
}
}
void handleConfigurationInternalChangedOnHandlerThread() {
// 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++) {
ITimeDetectorListener listener = mListeners.valueAt(listenerIndex);
try {
listener.onChange();
} catch (RemoteException e) {
Slog.w(TAG, "Unable to notify listener=" + listener, e);
}
}
}
}
@Override

View File

@@ -196,8 +196,9 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub
boolean updateConfiguration(
@UserIdInt int userId, @NonNull TimeZoneConfiguration configuration) {
userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
userId, false, false, "updateConfiguration", null);
// Resolve constants like USER_CURRENT to the true user ID as needed.
int resolvedUserId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
Binder.getCallingUid(), userId, false, false, "updateConfiguration", null);
enforceManageTimeZoneDetectorPermission();
@@ -205,7 +206,7 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub
final long token = mCallerIdentityInjector.clearCallingIdentity();
try {
return mServiceConfigAccessor.updateConfiguration(userId, configuration);
return mServiceConfigAccessor.updateConfiguration(resolvedUserId, configuration);
} finally {
mCallerIdentityInjector.restoreCallingIdentity(token);
}

View File

@@ -20,15 +20,21 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.app.time.ExternalTimeSuggestion;
import android.app.time.ITimeDetectorListener;
import android.app.time.TimeConfiguration;
import android.app.timedetector.GnssTimeSuggestion;
import android.app.timedetector.ManualTimeSuggestion;
import android.app.timedetector.NetworkTimeSuggestion;
@@ -36,6 +42,7 @@ import android.app.timedetector.TelephonyTimeSuggestion;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.TimestampedValue;
import android.util.IndentingPrintWriter;
@@ -124,6 +131,115 @@ public class TimeDetectorServiceTest {
anyString());
}
@Test(expected = SecurityException.class)
public void testAddListener_withoutPermission() {
doThrow(new SecurityException("Mock"))
.when(mMockContext).enforceCallingPermission(anyString(), any());
ITimeDetectorListener mockListener = mock(ITimeDetectorListener.class);
try {
mTimeDetectorService.addListener(mockListener);
fail("Expected SecurityException");
} finally {
verify(mMockContext).enforceCallingPermission(
eq(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION),
anyString());
}
}
@Test(expected = SecurityException.class)
public void testRemoveListener_withoutPermission() {
doThrow(new SecurityException("Mock"))
.when(mMockContext).enforceCallingPermission(anyString(), any());
ITimeDetectorListener mockListener = mock(ITimeDetectorListener.class);
try {
mTimeDetectorService.removeListener(mockListener);
fail("Expected a SecurityException");
} finally {
verify(mMockContext).enforceCallingPermission(
eq(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION),
anyString());
}
}
@Test
public void testListenerRegistrationAndCallbacks() throws Exception {
ConfigurationInternal initialConfiguration =
createConfigurationInternal(false /* autoDetectionEnabled */);
mFakeServiceConfigAccessor.initializeConfiguration(initialConfiguration);
IBinder mockListenerBinder = mock(IBinder.class);
ITimeDetectorListener mockListener = mock(ITimeDetectorListener.class);
{
doNothing().when(mMockContext).enforceCallingPermission(anyString(), any());
when(mockListener.asBinder()).thenReturn(mockListenerBinder);
mTimeDetectorService.addListener(mockListener);
verify(mMockContext).enforceCallingPermission(
eq(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION),
anyString());
verify(mockListener).asBinder();
verify(mockListenerBinder).linkToDeath(any(), anyInt());
verifyNoMoreInteractions(mockListenerBinder, mockListener, mMockContext);
reset(mockListenerBinder, mockListener, mMockContext);
}
{
doNothing().when(mMockContext).enforceCallingPermission(anyString(), any());
// Simulate the configuration being changed and verify the mockListener was notified.
TimeConfiguration autoDetectEnabledConfiguration =
createTimeConfiguration(true /* autoDetectionEnabled */);
mTimeDetectorService.updateConfiguration(autoDetectEnabledConfiguration);
// The configuration update notification is asynchronous.
mTestHandler.waitForMessagesToBeProcessed();
verify(mMockContext).enforceCallingPermission(
eq(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION),
anyString());
verify(mockListener).onChange();
verifyNoMoreInteractions(mockListenerBinder, mockListener, mMockContext);
reset(mockListenerBinder, mockListener, mMockContext);
}
{
doNothing().when(mMockContext).enforceCallingPermission(anyString(), any());
when(mockListener.asBinder()).thenReturn(mockListenerBinder);
when(mockListenerBinder.unlinkToDeath(any(), anyInt())).thenReturn(true);
// Now remove the listener, change the config again, and verify the listener is not
// called.
mTimeDetectorService.removeListener(mockListener);
verify(mMockContext).enforceCallingPermission(
eq(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION),
anyString());
verify(mockListener).asBinder();
verify(mockListenerBinder).unlinkToDeath(any(), eq(0));
verifyNoMoreInteractions(mockListenerBinder, mockListener, mMockContext);
reset(mockListenerBinder, mockListener, mMockContext);
}
{
doNothing().when(mMockContext).enforceCallingPermission(anyString(), any());
TimeConfiguration autoDetectDisabledConfiguration =
createTimeConfiguration(false /* autoDetectionEnabled */);
mTimeDetectorService.updateConfiguration(autoDetectDisabledConfiguration);
verify(mMockContext).enforceCallingPermission(
eq(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION),
anyString());
verify(mockListener, never()).onChange();
verifyNoMoreInteractions(mockListenerBinder, mockListener, mMockContext);
reset(mockListenerBinder, mockListener, mMockContext);
}
}
@Test(expected = SecurityException.class)
public void testSuggestTelephonyTime_withoutPermission() {
doThrow(new SecurityException("Mock"))
@@ -289,6 +405,12 @@ public class TimeDetectorServiceTest {
mStubbedTimeDetectorStrategy.verifyDumpCalled();
}
private static TimeConfiguration createTimeConfiguration(boolean autoDetectionEnabled) {
return new TimeConfiguration.Builder()
.setAutoDetectionEnabled(autoDetectionEnabled)
.build();
}
private static ConfigurationInternal createConfigurationInternal(boolean autoDetectionEnabled) {
return new ConfigurationInternal.Builder(ARBITRARY_USER_ID)
.setUserConfigAllowed(true)