Loading core/java/android/app/timezonedetector/ITimeZoneDetectorService.aidl +1 −0 Original line number Original line Diff line number Diff line Loading @@ -41,6 +41,7 @@ interface ITimeZoneDetectorService { TimeZoneConfiguration getConfiguration(); TimeZoneConfiguration getConfiguration(); boolean updateConfiguration(in TimeZoneConfiguration configuration); boolean updateConfiguration(in TimeZoneConfiguration configuration); void addConfigurationListener(ITimeZoneConfigurationListener listener); void addConfigurationListener(ITimeZoneConfigurationListener listener); void removeConfigurationListener(ITimeZoneConfigurationListener listener); boolean suggestManualTimeZone(in ManualTimeZoneSuggestion timeZoneSuggestion); boolean suggestManualTimeZone(in ManualTimeZoneSuggestion timeZoneSuggestion); void suggestTelephonyTimeZone(in TelephonyTimeZoneSuggestion timeZoneSuggestion); void suggestTelephonyTimeZone(in TelephonyTimeZoneSuggestion timeZoneSuggestion); Loading core/java/android/app/timezonedetector/TimeZoneDetector.java +16 −1 Original line number Original line Diff line number Diff line Loading @@ -73,12 +73,27 @@ public interface TimeZoneDetector { @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) boolean updateConfiguration(@NonNull TimeZoneConfiguration configuration); boolean updateConfiguration(@NonNull TimeZoneConfiguration configuration); /** * An interface that can be used to listen for changes to the time zone detector configuration. */ interface TimeZoneConfigurationListener { /** Called when the configuration changes. There are no guarantees about the thread used. */ void onChange(@NonNull TimeZoneConfiguration configuration); } /** /** * Registers a listener that will be informed when the configuration changes. The complete * Registers a listener that will be informed when the configuration changes. The complete * configuration is passed to the listener, not just the properties that have changed. * configuration is passed to the listener, not just the properties that have changed. */ */ @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) void addConfigurationListener(@NonNull ITimeZoneConfigurationListener listener); void addConfigurationListener(@NonNull TimeZoneConfigurationListener listener); /** * Removes a listener previously passed to * {@link #addConfigurationListener(ITimeZoneConfigurationListener)} */ @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) void removeConfigurationListener(@NonNull TimeZoneConfigurationListener listener); /** /** * A shared utility method to create a {@link ManualTimeZoneSuggestion}. * A shared utility method to create a {@link ManualTimeZoneSuggestion}. Loading core/java/android/app/timezonedetector/TimeZoneDetectorImpl.java +66 −6 Original line number Original line Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.content.Context; import android.os.RemoteException; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; import android.os.ServiceManager.ServiceNotFoundException; import android.util.ArraySet; import android.util.Log; import android.util.Log; /** /** Loading @@ -34,6 +35,9 @@ public final class TimeZoneDetectorImpl implements TimeZoneDetector { private final ITimeZoneDetectorService mITimeZoneDetectorService; private final ITimeZoneDetectorService mITimeZoneDetectorService; private ITimeZoneConfigurationListener mConfigurationReceiver; private ArraySet<TimeZoneConfigurationListener> mConfigurationListeners; public TimeZoneDetectorImpl() throws ServiceNotFoundException { public TimeZoneDetectorImpl() throws ServiceNotFoundException { mITimeZoneDetectorService = ITimeZoneDetectorService.Stub.asInterface( mITimeZoneDetectorService = ITimeZoneDetectorService.Stub.asInterface( ServiceManager.getServiceOrThrow(Context.TIME_ZONE_DETECTOR_SERVICE)); ServiceManager.getServiceOrThrow(Context.TIME_ZONE_DETECTOR_SERVICE)); Loading Loading @@ -78,16 +82,72 @@ public final class TimeZoneDetectorImpl implements TimeZoneDetector { } } @Override @Override public void addConfigurationListener(@NonNull ITimeZoneConfigurationListener listener) { public void addConfigurationListener(@NonNull TimeZoneConfigurationListener listener) { if (DEBUG) { if (DEBUG) { Log.d(TAG, "addConfigurationListener called: " + listener); Log.d(TAG, "addConfigurationListener called: " + listener); } } synchronized (this) { if (mConfigurationListeners.contains(listener)) { return; } if (mConfigurationReceiver == null) { ITimeZoneConfigurationListener iListener = new ITimeZoneConfigurationListener.Stub() { @Override public void onChange(@NonNull TimeZoneConfiguration configuration) { notifyConfigurationListeners(configuration); } }; mConfigurationReceiver = iListener; } if (mConfigurationListeners == null) { mConfigurationListeners = new ArraySet<>(); } boolean wasEmpty = mConfigurationListeners.isEmpty(); mConfigurationListeners.add(listener); if (wasEmpty) { try { mITimeZoneDetectorService.addConfigurationListener(mConfigurationReceiver); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } } private void notifyConfigurationListeners(@NonNull TimeZoneConfiguration configuration) { ArraySet<TimeZoneConfigurationListener> configurationListeners; synchronized (this) { configurationListeners = new ArraySet<>(mConfigurationListeners); } int size = configurationListeners.size(); for (int i = 0; i < size; i++) { configurationListeners.valueAt(i).onChange(configuration); } } @Override public void removeConfigurationListener(@NonNull TimeZoneConfigurationListener listener) { if (DEBUG) { Log.d(TAG, "removeConfigurationListener called: " + listener); } synchronized (this) { if (mConfigurationListeners == null) { return; } boolean wasEmpty = mConfigurationListeners.isEmpty(); mConfigurationListeners.remove(listener); if (mConfigurationListeners.isEmpty() && !wasEmpty) { try { try { mITimeZoneDetectorService.addConfigurationListener(listener); mITimeZoneDetectorService.removeConfigurationListener(mConfigurationReceiver); } catch (RemoteException e) { } catch (RemoteException e) { throw e.rethrowFromSystemServer(); throw e.rethrowFromSystemServer(); } } } } } } @Override @Override public boolean suggestManualTimeZone(@NonNull ManualTimeZoneSuggestion timeZoneSuggestion) { public boolean suggestManualTimeZone(@NonNull ManualTimeZoneSuggestion timeZoneSuggestion) { Loading services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java +66 −12 Original line number Original line Diff line number Diff line Loading @@ -49,6 +49,7 @@ import com.android.server.timezonedetector.TimeZoneDetectorStrategy.StrategyList import java.io.FileDescriptor; import java.io.FileDescriptor; import java.io.PrintWriter; import java.io.PrintWriter; import java.util.ArrayList; import java.util.ArrayList; import java.util.Iterator; import java.util.Objects; import java.util.Objects; /** /** Loading Loading @@ -188,20 +189,14 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub int userId = UserHandle.getCallingUserId(); int userId = UserHandle.getCallingUserId(); ConfigListenerInfo listenerInfo = new ConfigListenerInfo(userId, listener); ConfigListenerInfo listenerInfo = new ConfigListenerInfo(userId, listener); final IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { synchronized (mConfigurationListeners) { Slog.i(TAG, "Configuration listener died: " + listenerInfo); mConfigurationListeners.remove(listenerInfo); } } }; synchronized (mConfigurationListeners) { synchronized (mConfigurationListeners) { if (mConfigurationListeners.contains(listenerInfo)) { return; } try { try { // Remove the record of the listener if the client process dies. // Ensure the reference to the listener is removed if the client process dies. listener.asBinder().linkToDeath(deathRecipient, 0 /* flags */); listenerInfo.linkToDeath(); // Only add the listener if we can linkToDeath(). // Only add the listener if we can linkToDeath(). mConfigurationListeners.add(listenerInfo); mConfigurationListeners.add(listenerInfo); Loading @@ -211,6 +206,31 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub } } } } @Override public void removeConfigurationListener(@NonNull ITimeZoneConfigurationListener listener) { enforceManageTimeZoneDetectorConfigurationPermission(); Objects.requireNonNull(listener); int userId = UserHandle.getCallingUserId(); synchronized (mConfigurationListeners) { ConfigListenerInfo toRemove = new ConfigListenerInfo(userId, listener); Iterator<ConfigListenerInfo> listenerIterator = mConfigurationListeners.iterator(); while (listenerIterator.hasNext()) { ConfigListenerInfo currentListenerInfo = listenerIterator.next(); if (currentListenerInfo.equals(toRemove)) { listenerIterator.remove(); // Stop listening for the client process to die. try { currentListenerInfo.unlinkToDeath(); } catch (RemoteException e) { Slog.e(TAG, "Unable to unlinkToDeath() for listener=" + listener, e); } } } } } void handleConfigurationChanged() { void handleConfigurationChanged() { // Note: we could trigger an async time zone detection operation here via a call to // Note: we could trigger an async time zone detection operation here via a call to // handleAutoTimeZoneDetectionChanged(), but that is triggered in response to the underlying // handleAutoTimeZoneDetectionChanged(), but that is triggered in response to the underlying Loading Loading @@ -319,7 +339,7 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub this, in, out, err, args, callback, resultReceiver); this, in, out, err, args, callback, resultReceiver); } } private static class ConfigListenerInfo { private class ConfigListenerInfo implements IBinder.DeathRecipient { private final @UserIdInt int mUserId; private final @UserIdInt int mUserId; private final ITimeZoneConfigurationListener mListener; private final ITimeZoneConfigurationListener mListener; Loading @@ -337,6 +357,40 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub return mListener; return mListener; } } void linkToDeath() throws RemoteException { mListener.asBinder().linkToDeath(this, 0 /* flags */); } void unlinkToDeath() throws RemoteException { mListener.asBinder().unlinkToDeath(this, 0 /* flags */); } @Override public void binderDied() { synchronized (mConfigurationListeners) { Slog.i(TAG, "Configuration listener client died: " + this); mConfigurationListeners.remove(this); } } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ConfigListenerInfo that = (ConfigListenerInfo) o; return mUserId == that.mUserId && mListener.equals(that.mListener); } @Override public int hashCode() { return Objects.hash(mUserId, mListener); } @Override @Override public String toString() { public String toString() { return "ConfigListenerInfo{" return "ConfigListenerInfo{" Loading services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java +79 −14 Original line number Original line Diff line number Diff line Loading @@ -21,12 +21,16 @@ import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_POSSE import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; 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.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.mockito.Mockito.when; import android.app.timezonedetector.ITimeZoneConfigurationListener; import android.app.timezonedetector.ITimeZoneConfigurationListener; Loading Loading @@ -158,10 +162,24 @@ public class TimeZoneDetectorServiceTest { } } } } @Test(expected = SecurityException.class) public void testRemoveConfigurationListener_withoutPermission() { doThrow(new SecurityException("Mock")) .when(mMockContext).enforceCallingPermission(anyString(), any()); ITimeZoneConfigurationListener mockListener = mock(ITimeZoneConfigurationListener.class); try { mTimeZoneDetectorService.removeConfigurationListener(mockListener); fail(); } finally { verify(mMockContext).enforceCallingPermission( eq(android.Manifest.permission.WRITE_SECURE_SETTINGS), anyString()); } } @Test @Test public void testConfigurationChangeListenerRegistrationAndCallbacks() throws Exception { public void testConfigurationChangeListenerRegistrationAndCallbacks() throws Exception { doNothing().when(mMockContext).enforceCallingPermission(anyString(), any()); TimeZoneConfiguration autoDetectDisabledConfiguration = TimeZoneConfiguration autoDetectDisabledConfiguration = createTimeZoneConfiguration(false /* autoDetectionEnabled */); createTimeZoneConfiguration(false /* autoDetectionEnabled */); Loading @@ -169,6 +187,9 @@ public class TimeZoneDetectorServiceTest { IBinder mockListenerBinder = mock(IBinder.class); IBinder mockListenerBinder = mock(IBinder.class); ITimeZoneConfigurationListener mockListener = mock(ITimeZoneConfigurationListener.class); ITimeZoneConfigurationListener mockListener = mock(ITimeZoneConfigurationListener.class); { doNothing().when(mMockContext).enforceCallingPermission(anyString(), any()); when(mockListener.asBinder()).thenReturn(mockListenerBinder); when(mockListener.asBinder()).thenReturn(mockListenerBinder); mTimeZoneDetectorService.addConfigurationListener(mockListener); mTimeZoneDetectorService.addConfigurationListener(mockListener); Loading @@ -176,15 +197,59 @@ public class TimeZoneDetectorServiceTest { verify(mMockContext).enforceCallingPermission( verify(mMockContext).enforceCallingPermission( eq(android.Manifest.permission.WRITE_SECURE_SETTINGS), eq(android.Manifest.permission.WRITE_SECURE_SETTINGS), anyString()); anyString()); verify(mockListenerBinder).linkToDeath(any(), eq(0)); 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. // Simulate the configuration being changed and verify the mockListener was notified. TimeZoneConfiguration autoDetectEnabledConfiguration = TimeZoneConfiguration autoDetectEnabledConfiguration = createTimeZoneConfiguration(true /* autoDetectionEnabled */); createTimeZoneConfiguration(true /* autoDetectionEnabled */); mFakeTimeZoneDetectorStrategy.updateConfiguration( ARBITRARY_USER_ID, autoDetectEnabledConfiguration); mTimeZoneDetectorService.updateConfiguration(autoDetectEnabledConfiguration); verify(mMockContext).enforceCallingPermission( eq(android.Manifest.permission.WRITE_SECURE_SETTINGS), anyString()); verify(mockListener).onChange(autoDetectEnabledConfiguration); verify(mockListener).onChange(autoDetectEnabledConfiguration); 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. mTimeZoneDetectorService.removeConfigurationListener(mockListener); verify(mMockContext).enforceCallingPermission( eq(android.Manifest.permission.WRITE_SECURE_SETTINGS), anyString()); verify(mockListener).asBinder(); verify(mockListenerBinder).unlinkToDeath(any(), eq(0)); verifyNoMoreInteractions(mockListenerBinder, mockListener, mMockContext); reset(mockListenerBinder, mockListener, mMockContext); } { doNothing().when(mMockContext).enforceCallingPermission(anyString(), any()); mTimeZoneDetectorService.updateConfiguration(autoDetectDisabledConfiguration); verify(mMockContext).enforceCallingPermission( eq(android.Manifest.permission.WRITE_SECURE_SETTINGS), anyString()); verify(mockListener, never()).onChange(any()); verifyNoMoreInteractions(mockListenerBinder, mockListener, mMockContext); reset(mockListenerBinder, mockListener, mMockContext); } } } @Test(expected = SecurityException.class) @Test(expected = SecurityException.class) Loading Loading
core/java/android/app/timezonedetector/ITimeZoneDetectorService.aidl +1 −0 Original line number Original line Diff line number Diff line Loading @@ -41,6 +41,7 @@ interface ITimeZoneDetectorService { TimeZoneConfiguration getConfiguration(); TimeZoneConfiguration getConfiguration(); boolean updateConfiguration(in TimeZoneConfiguration configuration); boolean updateConfiguration(in TimeZoneConfiguration configuration); void addConfigurationListener(ITimeZoneConfigurationListener listener); void addConfigurationListener(ITimeZoneConfigurationListener listener); void removeConfigurationListener(ITimeZoneConfigurationListener listener); boolean suggestManualTimeZone(in ManualTimeZoneSuggestion timeZoneSuggestion); boolean suggestManualTimeZone(in ManualTimeZoneSuggestion timeZoneSuggestion); void suggestTelephonyTimeZone(in TelephonyTimeZoneSuggestion timeZoneSuggestion); void suggestTelephonyTimeZone(in TelephonyTimeZoneSuggestion timeZoneSuggestion); Loading
core/java/android/app/timezonedetector/TimeZoneDetector.java +16 −1 Original line number Original line Diff line number Diff line Loading @@ -73,12 +73,27 @@ public interface TimeZoneDetector { @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) boolean updateConfiguration(@NonNull TimeZoneConfiguration configuration); boolean updateConfiguration(@NonNull TimeZoneConfiguration configuration); /** * An interface that can be used to listen for changes to the time zone detector configuration. */ interface TimeZoneConfigurationListener { /** Called when the configuration changes. There are no guarantees about the thread used. */ void onChange(@NonNull TimeZoneConfiguration configuration); } /** /** * Registers a listener that will be informed when the configuration changes. The complete * Registers a listener that will be informed when the configuration changes. The complete * configuration is passed to the listener, not just the properties that have changed. * configuration is passed to the listener, not just the properties that have changed. */ */ @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) void addConfigurationListener(@NonNull ITimeZoneConfigurationListener listener); void addConfigurationListener(@NonNull TimeZoneConfigurationListener listener); /** * Removes a listener previously passed to * {@link #addConfigurationListener(ITimeZoneConfigurationListener)} */ @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) void removeConfigurationListener(@NonNull TimeZoneConfigurationListener listener); /** /** * A shared utility method to create a {@link ManualTimeZoneSuggestion}. * A shared utility method to create a {@link ManualTimeZoneSuggestion}. Loading
core/java/android/app/timezonedetector/TimeZoneDetectorImpl.java +66 −6 Original line number Original line Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.content.Context; import android.os.RemoteException; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; import android.os.ServiceManager.ServiceNotFoundException; import android.util.ArraySet; import android.util.Log; import android.util.Log; /** /** Loading @@ -34,6 +35,9 @@ public final class TimeZoneDetectorImpl implements TimeZoneDetector { private final ITimeZoneDetectorService mITimeZoneDetectorService; private final ITimeZoneDetectorService mITimeZoneDetectorService; private ITimeZoneConfigurationListener mConfigurationReceiver; private ArraySet<TimeZoneConfigurationListener> mConfigurationListeners; public TimeZoneDetectorImpl() throws ServiceNotFoundException { public TimeZoneDetectorImpl() throws ServiceNotFoundException { mITimeZoneDetectorService = ITimeZoneDetectorService.Stub.asInterface( mITimeZoneDetectorService = ITimeZoneDetectorService.Stub.asInterface( ServiceManager.getServiceOrThrow(Context.TIME_ZONE_DETECTOR_SERVICE)); ServiceManager.getServiceOrThrow(Context.TIME_ZONE_DETECTOR_SERVICE)); Loading Loading @@ -78,16 +82,72 @@ public final class TimeZoneDetectorImpl implements TimeZoneDetector { } } @Override @Override public void addConfigurationListener(@NonNull ITimeZoneConfigurationListener listener) { public void addConfigurationListener(@NonNull TimeZoneConfigurationListener listener) { if (DEBUG) { if (DEBUG) { Log.d(TAG, "addConfigurationListener called: " + listener); Log.d(TAG, "addConfigurationListener called: " + listener); } } synchronized (this) { if (mConfigurationListeners.contains(listener)) { return; } if (mConfigurationReceiver == null) { ITimeZoneConfigurationListener iListener = new ITimeZoneConfigurationListener.Stub() { @Override public void onChange(@NonNull TimeZoneConfiguration configuration) { notifyConfigurationListeners(configuration); } }; mConfigurationReceiver = iListener; } if (mConfigurationListeners == null) { mConfigurationListeners = new ArraySet<>(); } boolean wasEmpty = mConfigurationListeners.isEmpty(); mConfigurationListeners.add(listener); if (wasEmpty) { try { mITimeZoneDetectorService.addConfigurationListener(mConfigurationReceiver); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } } private void notifyConfigurationListeners(@NonNull TimeZoneConfiguration configuration) { ArraySet<TimeZoneConfigurationListener> configurationListeners; synchronized (this) { configurationListeners = new ArraySet<>(mConfigurationListeners); } int size = configurationListeners.size(); for (int i = 0; i < size; i++) { configurationListeners.valueAt(i).onChange(configuration); } } @Override public void removeConfigurationListener(@NonNull TimeZoneConfigurationListener listener) { if (DEBUG) { Log.d(TAG, "removeConfigurationListener called: " + listener); } synchronized (this) { if (mConfigurationListeners == null) { return; } boolean wasEmpty = mConfigurationListeners.isEmpty(); mConfigurationListeners.remove(listener); if (mConfigurationListeners.isEmpty() && !wasEmpty) { try { try { mITimeZoneDetectorService.addConfigurationListener(listener); mITimeZoneDetectorService.removeConfigurationListener(mConfigurationReceiver); } catch (RemoteException e) { } catch (RemoteException e) { throw e.rethrowFromSystemServer(); throw e.rethrowFromSystemServer(); } } } } } } @Override @Override public boolean suggestManualTimeZone(@NonNull ManualTimeZoneSuggestion timeZoneSuggestion) { public boolean suggestManualTimeZone(@NonNull ManualTimeZoneSuggestion timeZoneSuggestion) { Loading
services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java +66 −12 Original line number Original line Diff line number Diff line Loading @@ -49,6 +49,7 @@ import com.android.server.timezonedetector.TimeZoneDetectorStrategy.StrategyList import java.io.FileDescriptor; import java.io.FileDescriptor; import java.io.PrintWriter; import java.io.PrintWriter; import java.util.ArrayList; import java.util.ArrayList; import java.util.Iterator; import java.util.Objects; import java.util.Objects; /** /** Loading Loading @@ -188,20 +189,14 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub int userId = UserHandle.getCallingUserId(); int userId = UserHandle.getCallingUserId(); ConfigListenerInfo listenerInfo = new ConfigListenerInfo(userId, listener); ConfigListenerInfo listenerInfo = new ConfigListenerInfo(userId, listener); final IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { synchronized (mConfigurationListeners) { Slog.i(TAG, "Configuration listener died: " + listenerInfo); mConfigurationListeners.remove(listenerInfo); } } }; synchronized (mConfigurationListeners) { synchronized (mConfigurationListeners) { if (mConfigurationListeners.contains(listenerInfo)) { return; } try { try { // Remove the record of the listener if the client process dies. // Ensure the reference to the listener is removed if the client process dies. listener.asBinder().linkToDeath(deathRecipient, 0 /* flags */); listenerInfo.linkToDeath(); // Only add the listener if we can linkToDeath(). // Only add the listener if we can linkToDeath(). mConfigurationListeners.add(listenerInfo); mConfigurationListeners.add(listenerInfo); Loading @@ -211,6 +206,31 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub } } } } @Override public void removeConfigurationListener(@NonNull ITimeZoneConfigurationListener listener) { enforceManageTimeZoneDetectorConfigurationPermission(); Objects.requireNonNull(listener); int userId = UserHandle.getCallingUserId(); synchronized (mConfigurationListeners) { ConfigListenerInfo toRemove = new ConfigListenerInfo(userId, listener); Iterator<ConfigListenerInfo> listenerIterator = mConfigurationListeners.iterator(); while (listenerIterator.hasNext()) { ConfigListenerInfo currentListenerInfo = listenerIterator.next(); if (currentListenerInfo.equals(toRemove)) { listenerIterator.remove(); // Stop listening for the client process to die. try { currentListenerInfo.unlinkToDeath(); } catch (RemoteException e) { Slog.e(TAG, "Unable to unlinkToDeath() for listener=" + listener, e); } } } } } void handleConfigurationChanged() { void handleConfigurationChanged() { // Note: we could trigger an async time zone detection operation here via a call to // Note: we could trigger an async time zone detection operation here via a call to // handleAutoTimeZoneDetectionChanged(), but that is triggered in response to the underlying // handleAutoTimeZoneDetectionChanged(), but that is triggered in response to the underlying Loading Loading @@ -319,7 +339,7 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub this, in, out, err, args, callback, resultReceiver); this, in, out, err, args, callback, resultReceiver); } } private static class ConfigListenerInfo { private class ConfigListenerInfo implements IBinder.DeathRecipient { private final @UserIdInt int mUserId; private final @UserIdInt int mUserId; private final ITimeZoneConfigurationListener mListener; private final ITimeZoneConfigurationListener mListener; Loading @@ -337,6 +357,40 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub return mListener; return mListener; } } void linkToDeath() throws RemoteException { mListener.asBinder().linkToDeath(this, 0 /* flags */); } void unlinkToDeath() throws RemoteException { mListener.asBinder().unlinkToDeath(this, 0 /* flags */); } @Override public void binderDied() { synchronized (mConfigurationListeners) { Slog.i(TAG, "Configuration listener client died: " + this); mConfigurationListeners.remove(this); } } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ConfigListenerInfo that = (ConfigListenerInfo) o; return mUserId == that.mUserId && mListener.equals(that.mListener); } @Override public int hashCode() { return Objects.hash(mUserId, mListener); } @Override @Override public String toString() { public String toString() { return "ConfigListenerInfo{" return "ConfigListenerInfo{" Loading
services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java +79 −14 Original line number Original line Diff line number Diff line Loading @@ -21,12 +21,16 @@ import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_POSSE import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; 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.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.mockito.Mockito.when; import android.app.timezonedetector.ITimeZoneConfigurationListener; import android.app.timezonedetector.ITimeZoneConfigurationListener; Loading Loading @@ -158,10 +162,24 @@ public class TimeZoneDetectorServiceTest { } } } } @Test(expected = SecurityException.class) public void testRemoveConfigurationListener_withoutPermission() { doThrow(new SecurityException("Mock")) .when(mMockContext).enforceCallingPermission(anyString(), any()); ITimeZoneConfigurationListener mockListener = mock(ITimeZoneConfigurationListener.class); try { mTimeZoneDetectorService.removeConfigurationListener(mockListener); fail(); } finally { verify(mMockContext).enforceCallingPermission( eq(android.Manifest.permission.WRITE_SECURE_SETTINGS), anyString()); } } @Test @Test public void testConfigurationChangeListenerRegistrationAndCallbacks() throws Exception { public void testConfigurationChangeListenerRegistrationAndCallbacks() throws Exception { doNothing().when(mMockContext).enforceCallingPermission(anyString(), any()); TimeZoneConfiguration autoDetectDisabledConfiguration = TimeZoneConfiguration autoDetectDisabledConfiguration = createTimeZoneConfiguration(false /* autoDetectionEnabled */); createTimeZoneConfiguration(false /* autoDetectionEnabled */); Loading @@ -169,6 +187,9 @@ public class TimeZoneDetectorServiceTest { IBinder mockListenerBinder = mock(IBinder.class); IBinder mockListenerBinder = mock(IBinder.class); ITimeZoneConfigurationListener mockListener = mock(ITimeZoneConfigurationListener.class); ITimeZoneConfigurationListener mockListener = mock(ITimeZoneConfigurationListener.class); { doNothing().when(mMockContext).enforceCallingPermission(anyString(), any()); when(mockListener.asBinder()).thenReturn(mockListenerBinder); when(mockListener.asBinder()).thenReturn(mockListenerBinder); mTimeZoneDetectorService.addConfigurationListener(mockListener); mTimeZoneDetectorService.addConfigurationListener(mockListener); Loading @@ -176,15 +197,59 @@ public class TimeZoneDetectorServiceTest { verify(mMockContext).enforceCallingPermission( verify(mMockContext).enforceCallingPermission( eq(android.Manifest.permission.WRITE_SECURE_SETTINGS), eq(android.Manifest.permission.WRITE_SECURE_SETTINGS), anyString()); anyString()); verify(mockListenerBinder).linkToDeath(any(), eq(0)); 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. // Simulate the configuration being changed and verify the mockListener was notified. TimeZoneConfiguration autoDetectEnabledConfiguration = TimeZoneConfiguration autoDetectEnabledConfiguration = createTimeZoneConfiguration(true /* autoDetectionEnabled */); createTimeZoneConfiguration(true /* autoDetectionEnabled */); mFakeTimeZoneDetectorStrategy.updateConfiguration( ARBITRARY_USER_ID, autoDetectEnabledConfiguration); mTimeZoneDetectorService.updateConfiguration(autoDetectEnabledConfiguration); verify(mMockContext).enforceCallingPermission( eq(android.Manifest.permission.WRITE_SECURE_SETTINGS), anyString()); verify(mockListener).onChange(autoDetectEnabledConfiguration); verify(mockListener).onChange(autoDetectEnabledConfiguration); 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. mTimeZoneDetectorService.removeConfigurationListener(mockListener); verify(mMockContext).enforceCallingPermission( eq(android.Manifest.permission.WRITE_SECURE_SETTINGS), anyString()); verify(mockListener).asBinder(); verify(mockListenerBinder).unlinkToDeath(any(), eq(0)); verifyNoMoreInteractions(mockListenerBinder, mockListener, mMockContext); reset(mockListenerBinder, mockListener, mMockContext); } { doNothing().when(mMockContext).enforceCallingPermission(anyString(), any()); mTimeZoneDetectorService.updateConfiguration(autoDetectDisabledConfiguration); verify(mMockContext).enforceCallingPermission( eq(android.Manifest.permission.WRITE_SECURE_SETTINGS), anyString()); verify(mockListener, never()).onChange(any()); verifyNoMoreInteractions(mockListenerBinder, mockListener, mMockContext); reset(mockListenerBinder, mockListener, mMockContext); } } } @Test(expected = SecurityException.class) @Test(expected = SecurityException.class) Loading