Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ff161ed8 authored by Stanislav Zholnin's avatar Stanislav Zholnin
Browse files

Defer time zone acceptance heuristic until device unlock.

The heuristic for automatically accepting a time zone change is delayed until the device is unlocked if it was locked at the time of the change. This is done by registering a `BroadcastReceiver` for `ACTION_USER_PRESENT`.

Bug: 417458133
Flag: android.timezone.flags.enable_automatic_time_zone_rejection_logging
Test: atest NotifyingTimeZoneChangeListenerTest

Change-Id: I7cd598f25d9b5a38f9f93c5419a1e5f7ae568888
parent 223cc3e7
Loading
Loading
Loading
Loading
+65 −7
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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;
@@ -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();
@@ -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);
@@ -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")
@@ -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
@@ -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) {
+68 −2
Original line number Diff line number Diff line
@@ -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;
@@ -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;

@@ -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;
@@ -99,6 +105,8 @@ public class NotifyingTimeZoneChangeListenerTest {
    private FakeEnvironment mFakeEnvironment;
    private int mUid;

    @Mock private KeyguardManager mockKeyguardManager;

    private NotifyingTimeZoneChangeListener mTimeZoneChangeTracker;

    @Before
@@ -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);

@@ -145,7 +154,8 @@ public class NotifyingTimeZoneChangeListenerTest {
                        mContext,
                        mServiceConfigAccessor,
                        mNotificationManager,
                        mFakeEnvironment);
                        mFakeEnvironment,
                        mockKeyguardManager);
    }

    @After
@@ -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();