diff --git a/core/java/android/app/time/ITimeDetectorListener.aidl b/core/java/android/app/time/ITimeDetectorListener.aidl
new file mode 100644
index 0000000000000..0689647861e5e
--- /dev/null
+++ b/core/java/android/app/time/ITimeDetectorListener.aidl
@@ -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();
+}
\ No newline at end of file
diff --git a/core/java/android/app/timedetector/ITimeDetectorService.aidl b/core/java/android/app/timedetector/ITimeDetectorService.aidl
index 9a6c33589123e..fc7afb4831424 100644
--- a/core/java/android/app/timedetector/ITimeDetectorService.aidl
+++ b/core/java/android/app/timedetector/ITimeDetectorService.aidl
@@ -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);
diff --git a/services/core/java/com/android/server/timedetector/ServiceConfigAccessor.java b/services/core/java/com/android/server/timedetector/ServiceConfigAccessor.java
index 05d21723f2875..80ce5986ca79a 100644
--- a/services/core/java/com/android/server/timedetector/ServiceConfigAccessor.java
+++ b/services/core/java/com/android/server/timedetector/ServiceConfigAccessor.java
@@ -53,6 +53,22 @@ public interface ServiceConfigAccessor {
@NonNull
ConfigurationInternal getCurrentUserConfigurationInternal();
+ /**
+ * Updates the configuration properties that control a device's time behavior.
+ *
+ *
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.
- *
- *
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);
}
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorService.java b/services/core/java/com/android/server/timedetector/TimeDetectorService.java
index a6efa73d58eae..105cd78f2a07f 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorService.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorService.java
@@ -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 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
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java
index 898d02e212f46..59db855dcf25d 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java
@@ -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);
}
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
index ad2cb53a3fe86..3086d90a2d2f3 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
@@ -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)