diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java index 9e25555eec..f1c0f3e210 100644 --- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java +++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java @@ -17,7 +17,6 @@ package com.android.quickstep; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.content.Intent.ACTION_USER_UNLOCKED; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.launcher3.util.DisplayController.CHANGE_ALL; @@ -50,10 +49,8 @@ import android.content.Context; import android.graphics.Region; import android.inputmethodservice.InputMethodService; import android.net.Uri; -import android.os.Process; import android.os.RemoteException; import android.os.SystemProperties; -import android.os.UserManager; import android.provider.Settings; import android.view.MotionEvent; @@ -63,9 +60,9 @@ import androidx.annotation.NonNull; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener; import com.android.launcher3.util.DisplayController.Info; +import com.android.launcher3.util.LockedUserState; import com.android.launcher3.util.NavigationMode; import com.android.launcher3.util.SettingsCache; -import com.android.launcher3.util.SimpleBroadcastReceiver; import com.android.quickstep.TopTaskTracker.CachedTaskInfo; import com.android.quickstep.util.NavBarPosition; import com.android.systemui.shared.system.ActivityManagerWrapper; @@ -109,15 +106,6 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener { private final boolean mIsOneHandedModeSupported; private boolean mPipIsActive; - private boolean mIsUserUnlocked; - private final ArrayList mUserUnlockedActions = new ArrayList<>(); - private final SimpleBroadcastReceiver mUserUnlockedReceiver = new SimpleBroadcastReceiver(i -> { - if (ACTION_USER_UNLOCKED.equals(i.getAction())) { - mIsUserUnlocked = true; - notifyUserUnlocked(); - } - }); - private int mGestureBlockingTaskId = -1; private @NonNull Region mExclusionRegion = new Region(); private SystemGestureExclusionListenerCompat mExclusionListener; @@ -143,14 +131,6 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener { runOnDestroy(mRotationTouchHelper::destroy); } - // Register for user unlocked if necessary - mIsUserUnlocked = context.getSystemService(UserManager.class) - .isUserUnlocked(Process.myUserHandle()); - if (!mIsUserUnlocked) { - mUserUnlockedReceiver.register(mContext, ACTION_USER_UNLOCKED); - } - runOnDestroy(() -> mUserUnlockedReceiver.unregisterReceiverSafely(mContext)); - // Register for exclusion updates mExclusionListener = new SystemGestureExclusionListenerCompat(mDisplayId) { @Override @@ -309,25 +289,6 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener { return mDisplayId; } - /** - * Adds a callback for when a user is unlocked. If the user is already unlocked, this listener - * will be called back immediately. - */ - public void runOnUserUnlocked(Runnable action) { - if (mIsUserUnlocked) { - action.run(); - } else { - mUserUnlockedActions.add(action); - } - } - - /** - * @return whether the user is unlocked. - */ - public boolean isUserUnlocked() { - return mIsUserUnlocked; - } - /** * @return whether the user has completed setup wizard */ @@ -335,14 +296,6 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener { return mIsUserSetupComplete; } - private void notifyUserUnlocked() { - for (Runnable action : mUserUnlockedActions) { - action.run(); - } - mUserUnlockedActions.clear(); - mUserUnlockedReceiver.unregisterReceiverSafely(mContext); - } - /** * Sets the task id where gestures should be blocked */ @@ -585,7 +538,7 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener { pw.println(" assistantAvailable=" + mAssistantAvailable); pw.println(" assistantDisabled=" + QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags)); - pw.println(" isUserUnlocked=" + mIsUserUnlocked); + pw.println(" isUserUnlocked=" + LockedUserState.get(mContext).isUserUnlocked()); pw.println(" isOneHandedModeEnabled=" + mIsOneHandedModeEnabled); pw.println(" isSwipeToNotificationEnabled=" + mIsSwipeToNotificationEnabled); pw.println(" deferredGestureRegion=" + mDeferredGestureRegion.getBounds()); diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java index 5d17cc77f4..65eac6d86e 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -87,6 +87,7 @@ import com.android.launcher3.tracing.LauncherTraceProto; import com.android.launcher3.tracing.TouchInteractionServiceProto; import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.LockedUserState; import com.android.launcher3.util.OnboardingPrefs; import com.android.launcher3.util.TraceHelper; import com.android.quickstep.inputconsumers.AccessibilityInputConsumer; @@ -406,8 +407,8 @@ public class TouchInteractionService extends Service mRotationTouchHelper = mDeviceState.getRotationTouchHelper(); // Call runOnUserUnlocked() before any other callbacks to ensure everything is initialized. - mDeviceState.runOnUserUnlocked(this::onUserUnlocked); - mDeviceState.runOnUserUnlocked(mTaskbarManager::onUserUnlocked); + LockedUserState.get(this).runOnUserUnlocked(this::onUserUnlocked); + LockedUserState.get(this).runOnUserUnlocked(mTaskbarManager::onUserUnlocked); mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged); ProtoTracer.INSTANCE.get(this).add(this); @@ -477,7 +478,7 @@ public class TouchInteractionService extends Service } private void resetHomeBounceSeenOnQuickstepEnabledFirstTime() { - if (!mDeviceState.isUserUnlocked() || mDeviceState.isButtonNavMode()) { + if (!LockedUserState.get(this).isUserUnlocked() || mDeviceState.isButtonNavMode()) { // Skip if not yet unlocked (can't read user shared prefs) or if the current navigation // mode doesn't have gestures return; @@ -520,7 +521,7 @@ public class TouchInteractionService extends Service @UiThread private void onSystemUiFlagsChanged(int lastSysUIFlags) { - if (mDeviceState.isUserUnlocked()) { + if (LockedUserState.get(this).isUserUnlocked()) { int systemUiStateFlags = mDeviceState.getSystemUiStateFlags(); SystemUiProxy.INSTANCE.get(this).setLastSystemUiStateFlags(systemUiStateFlags); mOverviewComponentObserver.onSystemUiStateChanged(); @@ -565,7 +566,7 @@ public class TouchInteractionService extends Service @UiThread private void onAssistantVisibilityChanged() { - if (mDeviceState.isUserUnlocked()) { + if (LockedUserState.get(this).isUserUnlocked()) { mOverviewComponentObserver.getActivityInterface().onAssistantVisibilityChanged( mDeviceState.getAssistantVisibility()); } @@ -575,7 +576,7 @@ public class TouchInteractionService extends Service public void onDestroy() { Log.d(TAG, "Touch service destroyed: user=" + getUserId()); sIsInitialized = false; - if (mDeviceState.isUserUnlocked()) { + if (LockedUserState.get(this).isUserUnlocked()) { mInputConsumer.unregisterInputConsumer(); mOverviewComponentObserver.onDestroy(); } @@ -609,7 +610,7 @@ public class TouchInteractionService extends Service TestLogging.recordMotionEvent( TestProtocol.SEQUENCE_TIS, "TouchInteractionService.onInputEvent", event); - if (!mDeviceState.isUserUnlocked()) { + if (!LockedUserState.get(this).isUserUnlocked()) { return; } @@ -631,7 +632,8 @@ public class TouchInteractionService extends Service mGestureState = newGestureState; mConsumer = newConsumer(prevGestureState, mGestureState, event); mUncheckedConsumer = mConsumer; - } else if (mDeviceState.isUserUnlocked() && mDeviceState.isFullyGesturalNavMode() + } else if (LockedUserState.get(this).isUserUnlocked() + && mDeviceState.isFullyGesturalNavMode() && mDeviceState.canTriggerAssistantAction(event)) { mGestureState = createGestureState(mGestureState); // Do not change mConsumer as if there is an ongoing QuickSwitch gesture, we @@ -751,7 +753,7 @@ public class TouchInteractionService extends Service boolean canStartSystemGesture = mDeviceState.canStartSystemGesture(); - if (!mDeviceState.isUserUnlocked()) { + if (!LockedUserState.get(this).isUserUnlocked()) { CompoundString reasonString = newCompoundString("device locked"); InputConsumer consumer; if (canStartSystemGesture) { @@ -1098,7 +1100,7 @@ public class TouchInteractionService extends Service } private void preloadOverview(boolean fromInit, boolean forSUWAllSet) { - if (!mDeviceState.isUserUnlocked()) { + if (!LockedUserState.get(this).isUserUnlocked()) { return; } @@ -1130,7 +1132,7 @@ public class TouchInteractionService extends Service @Override public void onConfigurationChanged(Configuration newConfig) { - if (!mDeviceState.isUserUnlocked()) { + if (!LockedUserState.get(this).isUserUnlocked()) { return; } final BaseActivityInterface activityInterface = @@ -1171,7 +1173,7 @@ public class TouchInteractionService extends Service } else { // Dump everything FeatureFlags.dump(pw); - if (mDeviceState.isUserUnlocked()) { + if (LockedUserState.get(this).isUserUnlocked()) { PluginManagerWrapper.INSTANCE.get(getBaseContext()).dump(pw); } mDeviceState.dump(pw); diff --git a/src/com/android/launcher3/util/LockedUserState.kt b/src/com/android/launcher3/util/LockedUserState.kt new file mode 100644 index 0000000000..7b49583b86 --- /dev/null +++ b/src/com/android/launcher3/util/LockedUserState.kt @@ -0,0 +1,57 @@ +package com.android.launcher3.util + +import android.content.Context +import android.content.Intent +import android.os.Process +import android.os.UserManager +import androidx.annotation.VisibleForTesting + +class LockedUserState(private val mContext: Context) : SafeCloseable { + var isUserUnlocked: Boolean + private set + private val mUserUnlockedActions: RunnableList = RunnableList() + + @VisibleForTesting + val mUserUnlockedReceiver = SimpleBroadcastReceiver { + if (Intent.ACTION_USER_UNLOCKED == it.action) { + isUserUnlocked = true + notifyUserUnlocked() + } + } + + init { + isUserUnlocked = + mContext + .getSystemService(UserManager::class.java)!! + .isUserUnlocked(Process.myUserHandle()) + if (isUserUnlocked) { + notifyUserUnlocked() + } else { + mUserUnlockedReceiver.register(mContext, Intent.ACTION_USER_UNLOCKED) + } + } + + private fun notifyUserUnlocked() { + mUserUnlockedActions.executeAllAndDestroy() + mUserUnlockedReceiver.unregisterReceiverSafely(mContext) + } + + /** Stops the receiver from listening for ACTION_USER_UNLOCK broadcasts. */ + override fun close() { + mUserUnlockedReceiver.unregisterReceiverSafely(mContext) + } + + /** + * Adds a `Runnable` to be executed when a user is unlocked. If the user is already unlocked, + * this runnable will run immediately because RunnableList will already have been destroyed. + */ + fun runOnUserUnlocked(action: Runnable) { + mUserUnlockedActions.add(action) + } + + companion object { + @VisibleForTesting val INSTANCE = MainThreadInitializedObject { LockedUserState(it) } + + @JvmStatic fun get(context: Context): LockedUserState = INSTANCE.get(context) + } +} diff --git a/tests/src/com/android/launcher3/util/LockedUserStateTest.kt b/tests/src/com/android/launcher3/util/LockedUserStateTest.kt new file mode 100644 index 0000000000..84156e7101 --- /dev/null +++ b/tests/src/com/android/launcher3/util/LockedUserStateTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 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 com.android.launcher3.util + +import android.content.Context +import android.content.Intent +import android.os.Process +import android.os.UserManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +/** Unit tests for {@link LockedUserUtil} */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class LockedUserStateTest { + + @Mock lateinit var userManager: UserManager + @Mock lateinit var context: Context + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + `when`(context.getSystemService(UserManager::class.java)).thenReturn(userManager) + } + + @Test + fun runOnUserUnlocked_runs_action_immediately_if_already_unlocked() { + `when`(userManager.isUserUnlocked(Process.myUserHandle())).thenReturn(true) + LockedUserState.INSTANCE.initializeForTesting(LockedUserState(context)) + val action: Runnable = mock() + + LockedUserState.get(context).runOnUserUnlocked(action) + verify(action).run() + } + + @Test + fun runOnUserUnlocked_waits_to_run_action_until_user_is_unlocked() { + `when`(userManager.isUserUnlocked(Process.myUserHandle())).thenReturn(false) + LockedUserState.INSTANCE.initializeForTesting(LockedUserState(context)) + val action: Runnable = mock() + + LockedUserState.get(context).runOnUserUnlocked(action) + verifyZeroInteractions(action) + + LockedUserState.get(context) + .mUserUnlockedReceiver + .onReceive(context, Intent(Intent.ACTION_USER_UNLOCKED)) + + verify(action).run() + } + + @Test + fun isUserUnlocked_returns_true_when_user_is_unlocked() { + `when`(userManager.isUserUnlocked(Process.myUserHandle())).thenReturn(true) + LockedUserState.INSTANCE.initializeForTesting(LockedUserState(context)) + assertThat(LockedUserState.get(context).isUserUnlocked).isTrue() + } + + @Test + fun isUserUnlocked_returns_false_when_user_is_locked() { + `when`(userManager.isUserUnlocked(Process.myUserHandle())).thenReturn(false) + LockedUserState.INSTANCE.initializeForTesting(LockedUserState(context)) + assertThat(LockedUserState.get(context).isUserUnlocked).isFalse() + } +}