Loading services/core/java/com/android/server/timezonedetector/NotifyingTimeZoneChangeListener.java +65 −7 Original line number Diff line number Diff line Loading @@ -31,9 +31,11 @@ import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_LOW; import android.annotation.DurationMillisLong; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.app.ActivityManagerInternal; import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; Loading Loading @@ -121,6 +123,7 @@ public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener { private final Context mContext; private final NotificationManager mNotificationManager; private final ActivityManagerInternal mActivityManagerInternal; private final KeyguardManager mKeyguardManager; // For scheduling callbacks private final Handler mHandler; Loading Loading @@ -163,6 +166,10 @@ public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener { @GuardedBy("mConfigurationLock") private boolean mIsRegistered; @VisibleForTesting @GuardedBy("mConfigurationLock") UserPresentReceiver mUserPresentReceiver; private int mAcceptedManualChanges; private int mAcceptedTelephonyChanges; private int mAcceptedLocationChanges; Loading @@ -184,7 +191,8 @@ public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener { context, serviceConfigAccessor, context.getSystemService(NotificationManager.class), environment); environment, context.getSystemService(KeyguardManager.class)); // Pretend there was an update to initialize configuration. changeTracker.handleConfigurationUpdate(); Loading @@ -198,7 +206,8 @@ public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener { Context context, ServiceConfigAccessor serviceConfigAccessor, NotificationManager notificationManager, @NonNull Environment environment) { @NonNull Environment environment, KeyguardManager keyguardManager) { mHandler = Objects.requireNonNull(handler); mContext = Objects.requireNonNull(context); mServiceConfigAccessor = Objects.requireNonNull(serviceConfigAccessor); Loading @@ -207,6 +216,7 @@ public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener { mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); mNotificationManager = notificationManager; mEnvironment = Objects.requireNonNull(environment); mKeyguardManager = keyguardManager; } @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL") Loading Loading @@ -387,7 +397,7 @@ public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener { // Schedule a callback for the new time zone so that we can implement "user accepted // the change because they didn't revert it" scheduleChangeAcceptedHeuristicCallback(trackedChangeEvent, AUTO_REVERT_THRESHOLD); scheduleChangeAcceptedHeuristicCallback(trackedChangeEvent.getId()); } if (lastTimeZoneChangeRecord != null Loading Loading @@ -476,10 +486,58 @@ public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener { < AUTO_REVERT_THRESHOLD); } private void scheduleChangeAcceptedHeuristicCallback( TimeZoneChangeRecord trackedChangeEvent, @DurationMillisLong long delayMillis) { private void scheduleChangeAcceptedHeuristicCallback(int trackedChangeEventId) { if (!android.timezone.flags.Flags.enableAutomaticTimeZoneRejectionLogging()) { mHandler.postDelayed( () -> changeAcceptedTimeHeuristicCallback(trackedChangeEventId), AUTO_REVERT_THRESHOLD); return; } if (mKeyguardManager == null || !mKeyguardManager.isDeviceLocked()) { mHandler.postDelayed( () -> changeAcceptedTimeHeuristicCallback(trackedChangeEvent.getId()), delayMillis); () -> changeAcceptedTimeHeuristicCallback(trackedChangeEventId), AUTO_REVERT_THRESHOLD); return; } resetUserPresentReceiver(new UserPresentReceiver(trackedChangeEventId)); } // Registering receiver to wait until device is unlocked. private void resetUserPresentReceiver(@Nullable UserPresentReceiver userPresentReceiver) { synchronized (mConfigurationLock) { if (mUserPresentReceiver != null) { try { mContext.unregisterReceiver(mUserPresentReceiver); } catch (IllegalArgumentException e) { // Handle the case where the receiver might have already been unregistered } } mUserPresentReceiver = userPresentReceiver; if (mUserPresentReceiver != null) { IntentFilter filter = new IntentFilter(Intent.ACTION_USER_PRESENT); mContext.registerReceiver(mUserPresentReceiver, filter); } } } // BroadcastReceiver to listen for ACTION_USER_PRESENT. @VisibleForTesting class UserPresentReceiver extends BroadcastReceiver { private final int trackedChangeEventId; public UserPresentReceiver(int trackedChangeEventId) { this.trackedChangeEventId = trackedChangeEventId; } @Override public void onReceive(Context context, Intent intent) { if (Intent.ACTION_USER_PRESENT.equals(intent.getAction())) { mHandler.postDelayed( () -> changeAcceptedTimeHeuristicCallback(trackedChangeEventId), AUTO_REVERT_THRESHOLD); resetUserPresentReceiver(null); } } } private void changeAcceptedTimeHeuristicCallback(int changeEventId) { Loading services/tests/timetests/src/com/android/server/timezonedetector/NotifyingTimeZoneChangeListenerTest.java +68 −2 Original line number Diff line number Diff line Loading @@ -32,14 +32,19 @@ import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_LOW; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.spy; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationManager; import android.app.UiAutomation; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.BroadcastReceiver; import android.os.HandlerThread; import android.os.Process; import android.os.UserHandle; Loading @@ -62,6 +67,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; Loading Loading @@ -89,7 +95,7 @@ public class NotifyingTimeZoneChangeListenerTest { private static final String INTERACT_ACROSS_USERS_FULL_PERMISSION = "android.permission.INTERACT_ACROSS_USERS_FULL"; @Mock private Context mContext; private Context mContext; private UiAutomation mUiAutomation; private FakeNotificationManager mNotificationManager; Loading @@ -99,6 +105,8 @@ public class NotifyingTimeZoneChangeListenerTest { private FakeEnvironment mFakeEnvironment; private int mUid; @Mock private KeyguardManager mockKeyguardManager; private NotifyingTimeZoneChangeListener mTimeZoneChangeTracker; @Before Loading Loading @@ -134,6 +142,7 @@ public class NotifyingTimeZoneChangeListenerTest { mServiceConfigAccessor.initializeCurrentUserConfiguration(config); mContext = InstrumentationRegistry.getInstrumentation().getContext(); mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); mUiAutomation.adoptShellPermissionIdentity(INTERACT_ACROSS_USERS_FULL_PERMISSION); Loading @@ -145,7 +154,8 @@ public class NotifyingTimeZoneChangeListenerTest { mContext, mServiceConfigAccessor, mNotificationManager, mFakeEnvironment); mFakeEnvironment, mockKeyguardManager); } @After Loading Loading @@ -519,6 +529,62 @@ public class NotifyingTimeZoneChangeListenerTest { mHandler.assertTotalMessagesEnqueued(2); } @Test @EnableFlags(android.timezone.flags.Flags.FLAG_ENABLE_AUTOMATIC_TIME_ZONE_REJECTION_LOGGING) public void process_automaticDetection_deviceLocked_defersHeuristic() { enableNotificationsWithManualChangeTracking(); Mockito.when(mockKeyguardManager.isDeviceLocked()).thenReturn(true); TimeZoneChangeEvent event = new TimeZoneChangeEvent( /* elapsedRealtimeMillis= */ 0, /* unixEpochTimeMillis= */ 1726597800000L, /* origin= */ ORIGIN_TELEPHONY, /* userId= */ mUid, /* oldZoneId= */ "Europe/Paris", /* newZoneId= */ "Europe/London", /* oldConfidence= */ TIME_ZONE_CONFIDENCE_HIGH, /* newConfidence= */ TIME_ZONE_CONFIDENCE_HIGH, /* cause= */ "NO_REASON"); mTimeZoneChangeTracker.process(event); // Verify that the heuristic callback is NOT posted immediately. mHandler.assertTotalMessagesEnqueued(0); // Simulate unlocking the device. Intent userPresentIntent = new Intent(Intent.ACTION_USER_PRESENT); mTimeZoneChangeTracker.mUserPresentReceiver.onReceive(mContext, userPresentIntent); // Now, the handler message should be enqueued. mHandler.assertTotalMessagesEnqueued(1); } @Test @EnableFlags(android.timezone.flags.Flags.FLAG_ENABLE_AUTOMATIC_TIME_ZONE_REJECTION_LOGGING) public void process_automaticDetection_deviceUnlocked_notDefersHeuristic() { enableNotificationsWithManualChangeTracking(); Mockito.when(mockKeyguardManager.isDeviceLocked()).thenReturn(false); TimeZoneChangeEvent event = new TimeZoneChangeEvent( /* elapsedRealtimeMillis= */ 0, /* unixEpochTimeMillis= */ 1726597800000L, /* origin= */ ORIGIN_TELEPHONY, /* userId= */ mUid, /* oldZoneId= */ "Europe/Paris", /* newZoneId= */ "Europe/London", /* oldConfidence= */ TIME_ZONE_CONFIDENCE_HIGH, /* newConfidence= */ TIME_ZONE_CONFIDENCE_HIGH, /* cause= */ "NO_REASON"); mTimeZoneChangeTracker.process(event); // Verify that the heuristic callback is posted immediately. mHandler.assertTotalMessagesEnqueued(1); } private void enableLocationTimeZoneDetection() { ConfigurationInternal oldConfiguration = mServiceConfigAccessor.getCurrentUserConfigurationInternal(); Loading Loading
services/core/java/com/android/server/timezonedetector/NotifyingTimeZoneChangeListener.java +65 −7 Original line number Diff line number Diff line Loading @@ -31,9 +31,11 @@ import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_LOW; import android.annotation.DurationMillisLong; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.app.ActivityManagerInternal; import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; Loading Loading @@ -121,6 +123,7 @@ public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener { private final Context mContext; private final NotificationManager mNotificationManager; private final ActivityManagerInternal mActivityManagerInternal; private final KeyguardManager mKeyguardManager; // For scheduling callbacks private final Handler mHandler; Loading Loading @@ -163,6 +166,10 @@ public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener { @GuardedBy("mConfigurationLock") private boolean mIsRegistered; @VisibleForTesting @GuardedBy("mConfigurationLock") UserPresentReceiver mUserPresentReceiver; private int mAcceptedManualChanges; private int mAcceptedTelephonyChanges; private int mAcceptedLocationChanges; Loading @@ -184,7 +191,8 @@ public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener { context, serviceConfigAccessor, context.getSystemService(NotificationManager.class), environment); environment, context.getSystemService(KeyguardManager.class)); // Pretend there was an update to initialize configuration. changeTracker.handleConfigurationUpdate(); Loading @@ -198,7 +206,8 @@ public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener { Context context, ServiceConfigAccessor serviceConfigAccessor, NotificationManager notificationManager, @NonNull Environment environment) { @NonNull Environment environment, KeyguardManager keyguardManager) { mHandler = Objects.requireNonNull(handler); mContext = Objects.requireNonNull(context); mServiceConfigAccessor = Objects.requireNonNull(serviceConfigAccessor); Loading @@ -207,6 +216,7 @@ public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener { mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); mNotificationManager = notificationManager; mEnvironment = Objects.requireNonNull(environment); mKeyguardManager = keyguardManager; } @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL") Loading Loading @@ -387,7 +397,7 @@ public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener { // Schedule a callback for the new time zone so that we can implement "user accepted // the change because they didn't revert it" scheduleChangeAcceptedHeuristicCallback(trackedChangeEvent, AUTO_REVERT_THRESHOLD); scheduleChangeAcceptedHeuristicCallback(trackedChangeEvent.getId()); } if (lastTimeZoneChangeRecord != null Loading Loading @@ -476,10 +486,58 @@ public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener { < AUTO_REVERT_THRESHOLD); } private void scheduleChangeAcceptedHeuristicCallback( TimeZoneChangeRecord trackedChangeEvent, @DurationMillisLong long delayMillis) { private void scheduleChangeAcceptedHeuristicCallback(int trackedChangeEventId) { if (!android.timezone.flags.Flags.enableAutomaticTimeZoneRejectionLogging()) { mHandler.postDelayed( () -> changeAcceptedTimeHeuristicCallback(trackedChangeEventId), AUTO_REVERT_THRESHOLD); return; } if (mKeyguardManager == null || !mKeyguardManager.isDeviceLocked()) { mHandler.postDelayed( () -> changeAcceptedTimeHeuristicCallback(trackedChangeEvent.getId()), delayMillis); () -> changeAcceptedTimeHeuristicCallback(trackedChangeEventId), AUTO_REVERT_THRESHOLD); return; } resetUserPresentReceiver(new UserPresentReceiver(trackedChangeEventId)); } // Registering receiver to wait until device is unlocked. private void resetUserPresentReceiver(@Nullable UserPresentReceiver userPresentReceiver) { synchronized (mConfigurationLock) { if (mUserPresentReceiver != null) { try { mContext.unregisterReceiver(mUserPresentReceiver); } catch (IllegalArgumentException e) { // Handle the case where the receiver might have already been unregistered } } mUserPresentReceiver = userPresentReceiver; if (mUserPresentReceiver != null) { IntentFilter filter = new IntentFilter(Intent.ACTION_USER_PRESENT); mContext.registerReceiver(mUserPresentReceiver, filter); } } } // BroadcastReceiver to listen for ACTION_USER_PRESENT. @VisibleForTesting class UserPresentReceiver extends BroadcastReceiver { private final int trackedChangeEventId; public UserPresentReceiver(int trackedChangeEventId) { this.trackedChangeEventId = trackedChangeEventId; } @Override public void onReceive(Context context, Intent intent) { if (Intent.ACTION_USER_PRESENT.equals(intent.getAction())) { mHandler.postDelayed( () -> changeAcceptedTimeHeuristicCallback(trackedChangeEventId), AUTO_REVERT_THRESHOLD); resetUserPresentReceiver(null); } } } private void changeAcceptedTimeHeuristicCallback(int changeEventId) { Loading
services/tests/timetests/src/com/android/server/timezonedetector/NotifyingTimeZoneChangeListenerTest.java +68 −2 Original line number Diff line number Diff line Loading @@ -32,14 +32,19 @@ import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_LOW; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.spy; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationManager; import android.app.UiAutomation; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.BroadcastReceiver; import android.os.HandlerThread; import android.os.Process; import android.os.UserHandle; Loading @@ -62,6 +67,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; Loading Loading @@ -89,7 +95,7 @@ public class NotifyingTimeZoneChangeListenerTest { private static final String INTERACT_ACROSS_USERS_FULL_PERMISSION = "android.permission.INTERACT_ACROSS_USERS_FULL"; @Mock private Context mContext; private Context mContext; private UiAutomation mUiAutomation; private FakeNotificationManager mNotificationManager; Loading @@ -99,6 +105,8 @@ public class NotifyingTimeZoneChangeListenerTest { private FakeEnvironment mFakeEnvironment; private int mUid; @Mock private KeyguardManager mockKeyguardManager; private NotifyingTimeZoneChangeListener mTimeZoneChangeTracker; @Before Loading Loading @@ -134,6 +142,7 @@ public class NotifyingTimeZoneChangeListenerTest { mServiceConfigAccessor.initializeCurrentUserConfiguration(config); mContext = InstrumentationRegistry.getInstrumentation().getContext(); mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); mUiAutomation.adoptShellPermissionIdentity(INTERACT_ACROSS_USERS_FULL_PERMISSION); Loading @@ -145,7 +154,8 @@ public class NotifyingTimeZoneChangeListenerTest { mContext, mServiceConfigAccessor, mNotificationManager, mFakeEnvironment); mFakeEnvironment, mockKeyguardManager); } @After Loading Loading @@ -519,6 +529,62 @@ public class NotifyingTimeZoneChangeListenerTest { mHandler.assertTotalMessagesEnqueued(2); } @Test @EnableFlags(android.timezone.flags.Flags.FLAG_ENABLE_AUTOMATIC_TIME_ZONE_REJECTION_LOGGING) public void process_automaticDetection_deviceLocked_defersHeuristic() { enableNotificationsWithManualChangeTracking(); Mockito.when(mockKeyguardManager.isDeviceLocked()).thenReturn(true); TimeZoneChangeEvent event = new TimeZoneChangeEvent( /* elapsedRealtimeMillis= */ 0, /* unixEpochTimeMillis= */ 1726597800000L, /* origin= */ ORIGIN_TELEPHONY, /* userId= */ mUid, /* oldZoneId= */ "Europe/Paris", /* newZoneId= */ "Europe/London", /* oldConfidence= */ TIME_ZONE_CONFIDENCE_HIGH, /* newConfidence= */ TIME_ZONE_CONFIDENCE_HIGH, /* cause= */ "NO_REASON"); mTimeZoneChangeTracker.process(event); // Verify that the heuristic callback is NOT posted immediately. mHandler.assertTotalMessagesEnqueued(0); // Simulate unlocking the device. Intent userPresentIntent = new Intent(Intent.ACTION_USER_PRESENT); mTimeZoneChangeTracker.mUserPresentReceiver.onReceive(mContext, userPresentIntent); // Now, the handler message should be enqueued. mHandler.assertTotalMessagesEnqueued(1); } @Test @EnableFlags(android.timezone.flags.Flags.FLAG_ENABLE_AUTOMATIC_TIME_ZONE_REJECTION_LOGGING) public void process_automaticDetection_deviceUnlocked_notDefersHeuristic() { enableNotificationsWithManualChangeTracking(); Mockito.when(mockKeyguardManager.isDeviceLocked()).thenReturn(false); TimeZoneChangeEvent event = new TimeZoneChangeEvent( /* elapsedRealtimeMillis= */ 0, /* unixEpochTimeMillis= */ 1726597800000L, /* origin= */ ORIGIN_TELEPHONY, /* userId= */ mUid, /* oldZoneId= */ "Europe/Paris", /* newZoneId= */ "Europe/London", /* oldConfidence= */ TIME_ZONE_CONFIDENCE_HIGH, /* newConfidence= */ TIME_ZONE_CONFIDENCE_HIGH, /* cause= */ "NO_REASON"); mTimeZoneChangeTracker.process(event); // Verify that the heuristic callback is posted immediately. mHandler.assertTotalMessagesEnqueued(1); } private void enableLocationTimeZoneDetection() { ConfigurationInternal oldConfiguration = mServiceConfigAccessor.getCurrentUserConfigurationInternal(); Loading