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)