Merge "Implementation of time user configuration update"
This commit is contained in:
22
core/java/android/app/time/ITimeDetectorListener.aidl
Normal file
22
core/java/android/app/time/ITimeDetectorListener.aidl
Normal 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();
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user