diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java index 0ab775694a..ce1e8b6b6e 100644 --- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java @@ -31,6 +31,7 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_I import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; @@ -86,6 +87,7 @@ public class NavbarButtonsViewController { private static final int FLAG_DISABLE_RECENTS = 1 << 8; private static final int FLAG_DISABLE_BACK = 1 << 9; private static final int FLAG_NOTIFICATION_SHADE_EXPANDED = 1 << 10; + private static final int FLAG_SCREEN_PINNING_ACTIVE = 1 << 10; private static final int MASK_IME_SWITCHER_VISIBLE = FLAG_SWITCHER_SUPPORTED | FLAG_IME_VISIBLE; @@ -152,7 +154,9 @@ public class NavbarButtonsViewController { mPropertyHolders.add(new StatePropertyHolder( mControllers.taskbarViewController.getTaskbarIconAlpha() .getProperty(ALPHA_INDEX_KEYGUARD), - flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0, MultiValueAlpha.VALUE, 1, 0)); + flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0 + && (flags & FLAG_SCREEN_PINNING_ACTIVE) == 0, + MultiValueAlpha.VALUE, 1, 0)); mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController .getKeyguardBgTaskbar(), @@ -286,6 +290,7 @@ public class NavbarButtonsViewController { int shadeExpandedFlags = SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED | SYSUI_STATE_QUICK_SETTINGS_EXPANDED; boolean isNotificationShadeExpanded = (sysUiStateFlags & shadeExpandedFlags) != 0; + boolean isScreenPinningActive = (sysUiStateFlags & SYSUI_STATE_SCREEN_PINNING) != 0; // TODO(b/202218289) we're getting IME as not visible on lockscreen from system updateStateForFlag(FLAG_IME_VISIBLE, isImeVisible); @@ -295,6 +300,7 @@ public class NavbarButtonsViewController { updateStateForFlag(FLAG_DISABLE_RECENTS, isRecentsDisabled); updateStateForFlag(FLAG_DISABLE_BACK, isBackDisabled); updateStateForFlag(FLAG_NOTIFICATION_SHADE_EXPANDED, isNotificationShadeExpanded); + updateStateForFlag(FLAG_SCREEN_PINNING_ACTIVE, isScreenPinningActive); if (mA11yButton != null) { // Only used in 3 button diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index cb9d4a4374..692352b3fd 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -361,6 +361,7 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ mControllers.taskbarStashController.updateStateForSysuiFlags(systemUiStateFlags, fromInit); mControllers.taskbarScrimViewController.updateStateForSysuiFlags(systemUiStateFlags, fromInit); + mControllers.navButtonController.updateSysuiFlags(systemUiStateFlags); } /** diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java index 56e9429120..3cdcdf7f89 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java @@ -29,8 +29,8 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.hardware.display.DisplayManager; import android.net.Uri; +import android.os.Handler; import android.provider.Settings; -import android.util.Log; import android.view.Display; import androidx.annotation.NonNull; @@ -93,7 +93,8 @@ public class TaskbarManager implements DisplayController.DisplayInfoChangeListen Display display = service.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY); mContext = service.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null); - mNavButtonController = new TaskbarNavButtonController(service); + mNavButtonController = new TaskbarNavButtonController(service, + SystemUiProxy.INSTANCE.get(mContext), new Handler()); mUserSetupCompleteListener = isUserSetupComplete -> recreateTaskbar(); mComponentCallbacks = new ComponentCallbacks() { private Configuration mOldConfig = mContext.getResources().getConfiguration(); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java index ae23eda2a4..d23336505a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java @@ -19,8 +19,10 @@ package com.android.launcher3.taskbar; import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS; import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_KEY; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import android.os.Bundle; +import android.os.Handler; import androidx.annotation.IntDef; @@ -40,6 +42,13 @@ import java.lang.annotation.RetentionPolicy; */ public class TaskbarNavButtonController { + /** Allow some time in between the long press for back and recents. */ + static final int SCREEN_PIN_LONG_PRESS_THRESHOLD = 200; + static final int SCREEN_PIN_LONG_PRESS_RESET = SCREEN_PIN_LONG_PRESS_THRESHOLD + 100; + + private long mLastScreenPinLongPress; + private boolean mScreenPinned; + @Retention(RetentionPolicy.SOURCE) @IntDef(value = { BUTTON_BACK, @@ -57,10 +66,20 @@ public class TaskbarNavButtonController { static final int BUTTON_IME_SWITCH = BUTTON_RECENTS << 1; static final int BUTTON_A11Y = BUTTON_IME_SWITCH << 1; - private final TouchInteractionService mService; + private static final int SCREEN_UNPIN_COMBO = BUTTON_BACK | BUTTON_RECENTS; + private int mLongPressedButtons = 0; - public TaskbarNavButtonController(TouchInteractionService service) { + private final TouchInteractionService mService; + private final SystemUiProxy mSystemUiProxy; + private final Handler mHandler; + + private final Runnable mResetLongPress = this::resetScreenUnpin; + + public TaskbarNavButtonController(TouchInteractionService service, + SystemUiProxy systemUiProxy, Handler handler) { mService = service; + mSystemUiProxy = systemUiProxy; + mHandler = handler; } public void onButtonClick(@TaskbarButton int buttonType) { @@ -72,13 +91,13 @@ public class TaskbarNavButtonController { navigateHome(); break; case BUTTON_RECENTS: - navigateToOverview();; + navigateToOverview(); break; case BUTTON_IME_SWITCH: showIMESwitcher(); break; case BUTTON_A11Y: - notifyImeClick(false /* longClick */); + notifyA11yClick(false /* longClick */); break; } } @@ -89,46 +108,98 @@ public class TaskbarNavButtonController { startAssistant(); return true; case BUTTON_A11Y: - notifyImeClick(true /* longClick */); + notifyA11yClick(true /* longClick */); return true; case BUTTON_BACK: - case BUTTON_IME_SWITCH: case BUTTON_RECENTS: + mLongPressedButtons |= buttonType; + return determineScreenUnpin(); + case BUTTON_IME_SWITCH: default: return false; } } + /** + * Checks if the user has long pressed back and recents buttons + * "together" (within {@link #SCREEN_PIN_LONG_PRESS_THRESHOLD})ms + * If so, then requests the system to turn off screen pinning. + * + * @return true if the long press is a valid user action in attempting to unpin an app + * Will always return {@code false} when screen pinning is not active. + * NOTE: Returning true does not mean that screen pinning has stopped + */ + private boolean determineScreenUnpin() { + long timeNow = System.currentTimeMillis(); + if (!mScreenPinned) { + return false; + } + + if (mLastScreenPinLongPress == 0) { + // First button long press registered, just mark time and wait for second button press + mLastScreenPinLongPress = System.currentTimeMillis(); + mHandler.postDelayed(mResetLongPress, SCREEN_PIN_LONG_PRESS_RESET); + return true; + } + + if ((timeNow - mLastScreenPinLongPress) > SCREEN_PIN_LONG_PRESS_THRESHOLD) { + // Too long in-between presses, reset the clock + resetScreenUnpin(); + return false; + } + + if ((mLongPressedButtons & SCREEN_UNPIN_COMBO) == SCREEN_UNPIN_COMBO) { + // Hooray! They did it (finally...) + mSystemUiProxy.stopScreenPinning(); + mHandler.removeCallbacks(mResetLongPress); + resetScreenUnpin(); + } + return true; + } + + private void resetScreenUnpin() { + mLongPressedButtons = 0; + mLastScreenPinLongPress = 0; + } + + public void updateSysuiFlags(int sysuiFlags) { + mScreenPinned = (sysuiFlags & SYSUI_STATE_SCREEN_PINNING) != 0; + } + private void navigateHome() { mService.getOverviewCommandHelper().addCommand(OverviewCommandHelper.TYPE_HOME); } private void navigateToOverview() { + if (mScreenPinned) { + return; + } TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onOverviewToggle"); mService.getOverviewCommandHelper().addCommand(OverviewCommandHelper.TYPE_TOGGLE); } private void executeBack() { - SystemUiProxy.INSTANCE.getNoCreate().onBackPressed(); + mSystemUiProxy.onBackPressed(); } private void showIMESwitcher() { - SystemUiProxy.INSTANCE.getNoCreate().onImeSwitcherPressed(); + mSystemUiProxy.onImeSwitcherPressed(); } - private void notifyImeClick(boolean longClick) { - SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.getNoCreate(); + private void notifyA11yClick(boolean longClick) { if (longClick) { - systemUiProxy.notifyAccessibilityButtonLongClicked(); + mSystemUiProxy.notifyAccessibilityButtonLongClicked(); } else { - systemUiProxy.notifyAccessibilityButtonClicked(mService.getDisplayId()); + mSystemUiProxy.notifyAccessibilityButtonClicked(mService.getDisplayId()); } } private void startAssistant() { + if (mScreenPinned) { + return; + } Bundle args = new Bundle(); args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS); - SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.getNoCreate(); - systemUiProxy.startAssistant(args); + mSystemUiProxy.startAssistant(args); } } diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java new file mode 100644 index 0000000000..ba1a60dd38 --- /dev/null +++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java @@ -0,0 +1,159 @@ +package com.android.launcher3.taskbar; + +import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y; +import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_BACK; +import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_HOME; +import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_IME_SWITCH; +import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_RECENTS; +import static com.android.launcher3.taskbar.TaskbarNavButtonController.SCREEN_PIN_LONG_PRESS_THRESHOLD; +import static com.android.quickstep.OverviewCommandHelper.TYPE_HOME; +import static com.android.quickstep.OverviewCommandHelper.TYPE_TOGGLE; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.Handler; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.quickstep.OverviewCommandHelper; +import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.TouchInteractionService; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class TaskbarNavButtonControllerTest { + + private final static int DISPLAY_ID = 2; + + @Mock + SystemUiProxy mockSystemUiProxy; + @Mock + TouchInteractionService mockService; + @Mock + OverviewCommandHelper mockCommandHelper; + @Mock + Handler mockHandler; + + private TaskbarNavButtonController mNavButtonController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + when(mockService.getDisplayId()).thenReturn(DISPLAY_ID); + when(mockService.getOverviewCommandHelper()).thenReturn(mockCommandHelper); + mNavButtonController = new TaskbarNavButtonController(mockService, + mockSystemUiProxy, mockHandler); + } + + @Test + public void testPressBack() { + mNavButtonController.onButtonClick(BUTTON_BACK); + verify(mockSystemUiProxy, times(1)).onBackPressed(); + } + + @Test + public void testPressImeSwitcher() { + mNavButtonController.onButtonClick(BUTTON_IME_SWITCH); + verify(mockSystemUiProxy, times(1)).onImeSwitcherPressed(); + } + + @Test + public void testPressA11yShortClick() { + mNavButtonController.onButtonClick(BUTTON_A11Y); + verify(mockSystemUiProxy, times(1)) + .notifyAccessibilityButtonClicked(DISPLAY_ID); + } + + @Test + public void testPressA11yLongClick() { + mNavButtonController.onButtonLongClick(BUTTON_A11Y); + verify(mockSystemUiProxy, times(1)).notifyAccessibilityButtonLongClicked(); + } + + @Test + public void testLongPressHome() { + mNavButtonController.onButtonLongClick(BUTTON_HOME); + verify(mockSystemUiProxy, times(1)).startAssistant(any()); + } + + @Test + public void testPressHome() { + mNavButtonController.onButtonClick(BUTTON_HOME); + verify(mockCommandHelper, times(1)).addCommand(TYPE_HOME); + } + + @Test + public void testPressRecents() { + mNavButtonController.onButtonClick(BUTTON_RECENTS); + verify(mockCommandHelper, times(1)).addCommand(TYPE_TOGGLE); + } + + @Test + public void testPressRecentsWithScreenPinned() { + mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING); + mNavButtonController.onButtonClick(BUTTON_RECENTS); + verify(mockCommandHelper, times(0)).addCommand(TYPE_TOGGLE); + } + + @Test + public void testLongPressBackRecentsNotPinned() { + mNavButtonController.onButtonLongClick(BUTTON_RECENTS); + mNavButtonController.onButtonLongClick(BUTTON_BACK); + verify(mockSystemUiProxy, times(0)).stopScreenPinning(); + } + + @Test + public void testLongPressBackRecentsPinned() { + mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING); + mNavButtonController.onButtonLongClick(BUTTON_RECENTS); + mNavButtonController.onButtonLongClick(BUTTON_BACK); + verify(mockSystemUiProxy, times(1)).stopScreenPinning(); + } + + @Test + public void testLongPressBackRecentsTooLongPinned() { + mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING); + mNavButtonController.onButtonLongClick(BUTTON_RECENTS); + try { + Thread.sleep(SCREEN_PIN_LONG_PRESS_THRESHOLD + 5); + } catch (InterruptedException e) { + e.printStackTrace(); + } + mNavButtonController.onButtonLongClick(BUTTON_BACK); + verify(mockSystemUiProxy, times(0)).stopScreenPinning(); + } + + @Test + public void testLongPressBackRecentsMultipleAttemptPinned() { + mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING); + mNavButtonController.onButtonLongClick(BUTTON_RECENTS); + try { + Thread.sleep(SCREEN_PIN_LONG_PRESS_THRESHOLD + 5); + } catch (InterruptedException e) { + e.printStackTrace(); + } + mNavButtonController.onButtonLongClick(BUTTON_BACK); + verify(mockSystemUiProxy, times(0)).stopScreenPinning(); + + // Try again w/in threshold + mNavButtonController.onButtonLongClick(BUTTON_RECENTS); + mNavButtonController.onButtonLongClick(BUTTON_BACK); + verify(mockSystemUiProxy, times(1)).stopScreenPinning(); + } + + @Test + public void testLongPressHomeScreenPinned() { + mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING); + mNavButtonController.onButtonLongClick(BUTTON_HOME); + verify(mockSystemUiProxy, times(0)).startAssistant(any()); + } +}