From f4d8a9d3a2bd3b8e830c6d6bbbf53432ab10d05a Mon Sep 17 00:00:00 2001 From: helencheuk Date: Fri, 11 Apr 2025 15:13:09 +0100 Subject: [PATCH] [Action Corner] Handle overview action in launcher Add a new command type "TOGGLE_OVERVIEW_PREVIOUS" to toggle between overview and the previous task or homepage. Renamed the existing command types to make them more precise Bug: 409036363 Flag: com.android.systemui.shared.cursor_hot_corner Test: OverviewCommandHelperTest Change-Id: I7bab39adb0c933e16ac222fafc8be3f60fe36cc1 --- .../quickstep/OverviewCommandHelper.kt | 61 ++++++++++----- .../quickstep/TouchInteractionService.java | 21 ++++-- .../actioncorner/ActionCornerHandler.kt | 39 ++++++++++ .../quickstep/OverviewCommandHelperTest.kt | 74 +++++++++++++++++-- 4 files changed, 163 insertions(+), 32 deletions(-) create mode 100644 quickstep/src/com/android/quickstep/actioncorner/ActionCornerHandler.kt diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt index 11e1dce7b2..8034c2e5b6 100644 --- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt +++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt @@ -48,11 +48,12 @@ import com.android.launcher3.util.RunnableList import com.android.launcher3.util.coroutines.DispatcherProvider import com.android.launcher3.util.coroutines.ProductionDispatchers import com.android.quickstep.OverviewCommandHelper.CommandInfo.CommandStatus -import com.android.quickstep.OverviewCommandHelper.CommandType.HIDE +import com.android.quickstep.OverviewCommandHelper.CommandType.HIDE_ALT_TAB import com.android.quickstep.OverviewCommandHelper.CommandType.HOME -import com.android.quickstep.OverviewCommandHelper.CommandType.KEYBOARD_INPUT -import com.android.quickstep.OverviewCommandHelper.CommandType.SHOW +import com.android.quickstep.OverviewCommandHelper.CommandType.SHOW_ALT_TAB +import com.android.quickstep.OverviewCommandHelper.CommandType.SHOW_WITH_FOCUS import com.android.quickstep.OverviewCommandHelper.CommandType.TOGGLE +import com.android.quickstep.OverviewCommandHelper.CommandType.TOGGLE_OVERVIEW_PREVIOUS import com.android.quickstep.fallback.window.RecentsWindowFlags.Companion.enableOverviewInWindow import com.android.quickstep.util.ActiveGestureLog import com.android.quickstep.util.ActiveGestureProtoLogProxy @@ -159,7 +160,10 @@ constructor( .toIntArray(), ) - fun canStartHomeSafely(): Boolean = commandQueue.isEmpty() || commandQueue.first().type == HOME + fun canStartHomeSafely(): Boolean = + commandQueue.isEmpty() || + commandQueue.first().type == HOME || + commandQueue.first().type == TOGGLE_OVERVIEW_PREVIOUS /** Clear pending or completed commands from the queue */ fun clearPendingCommands() { @@ -248,9 +252,9 @@ constructor( onCallbackResult: () -> Unit, ): Boolean = when (command.type) { - SHOW -> true // already visible - KEYBOARD_INPUT, - HIDE -> { + SHOW_WITH_FOCUS -> true // already visible + SHOW_ALT_TAB, + HIDE_ALT_TAB -> { if (recentsView.isHandlingTouch) { true } else { @@ -269,7 +273,15 @@ constructor( onCallbackResult, ) } - + TOGGLE_OVERVIEW_PREVIOUS -> { + val taskView = recentsView.runningTaskView + if (taskView == null) { + recentsView.startHome() + } else { + taskView.launchWithAnimation() + } + true + } HOME -> { recentsView.startHome() true @@ -341,7 +353,7 @@ constructor( } when (command.type) { - HIDE -> { + HIDE_ALT_TAB -> { if ( taskbarUIController == null || !shouldShowAltTabKqs(deviceProfile, command.displayId) @@ -353,7 +365,7 @@ constructor( if (keyboardTaskFocusIndex == -1) return true } - KEYBOARD_INPUT -> + SHOW_ALT_TAB -> if ( taskbarUIController != null && shouldShowAltTabKqs(deviceProfile, command.displayId) @@ -374,14 +386,15 @@ constructor( return true } - SHOW -> + SHOW_WITH_FOCUS -> // When Recents is not currently visible, the command's type is SHOW // when overview is triggered via the keyboard overview button or Action+Tab // keys (Not Alt+Tab which is KQS). The overview button on-screen in 3-button // nav is TYPE_TOGGLE. keyboardTaskFocusIndex = 0 - TOGGLE -> {} + TOGGLE, + TOGGLE_OVERVIEW_PREVIOUS -> {} } recentsView?.setKeyboardTaskFocusIndex( @@ -560,7 +573,11 @@ constructor( private fun updateRecentsViewFocus(command: CommandInfo) { val recentsView: RecentsView<*, *> = getVisibleRecentsView(command.displayId) ?: return - if (command.type != KEYBOARD_INPUT && command.type != HIDE && command.type != SHOW) { + if ( + command.type != SHOW_ALT_TAB && + command.type != HIDE_ALT_TAB && + command.type != SHOW_WITH_FOCUS + ) { return } @@ -581,7 +598,7 @@ constructor( private fun onRecentsViewFocusUpdated(command: CommandInfo) { val recentsView: RecentsView<*, *> = getVisibleRecentsView(command.displayId) ?: return - if (command.type != HIDE || keyboardTaskFocusIndex == PagedView.INVALID_PAGE) { + if (command.type != HIDE_ALT_TAB || keyboardTaskFocusIndex == PagedView.INVALID_PAGE) { return } recentsView.setKeyboardTaskFocusIndex(PagedView.INVALID_PAGE) @@ -603,8 +620,8 @@ constructor( val container = containerInterface.getCreatedContainer() ?: return val event = when (command.type) { - SHOW -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT - HIDE -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH + SHOW_WITH_FOCUS -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT + HIDE_ALT_TAB -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH TOGGLE -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON else -> return } @@ -659,11 +676,17 @@ constructor( } enum class CommandType { - SHOW, - KEYBOARD_INPUT, - HIDE, + SHOW_WITH_FOCUS, + SHOW_ALT_TAB, + HIDE_ALT_TAB, + /** Toggle between overview and the next task */ TOGGLE, // Navigate to Overview HOME, // Navigate to Home + /** + * Toggle between Overview and the previous screen before launching Overview, which can + * either be a task or the home screen. + */ + TOGGLE_OVERVIEW_PREVIOUS, } companion object { diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java index c91655eddd..db4ff9a0e4 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -94,6 +94,7 @@ import com.android.launcher3.util.ScreenOnTracker; import com.android.launcher3.util.TraceHelper; import com.android.quickstep.OverviewCommandHelper.CommandType; import com.android.quickstep.OverviewComponentObserver.OverviewChangeListener; +import com.android.quickstep.actioncorner.ActionCornerHandler; import com.android.quickstep.fallback.window.RecentsWindowFlags; import com.android.quickstep.fallback.window.RecentsWindowManager; import com.android.quickstep.fallback.window.RecentsWindowSwipeHandler; @@ -238,9 +239,9 @@ public class TouchInteractionService extends Service { int displayId = enableAltTabKqsOnConnectedDisplays.isTrue() ? SystemUiProxy.INSTANCE.get(tis).getFocusState().getFocusedDisplayId() : DEFAULT_DISPLAY; - tis.mOverviewCommandHelper.addCommand(CommandType.KEYBOARD_INPUT, displayId); + tis.mOverviewCommandHelper.addCommand(CommandType.SHOW_ALT_TAB, displayId); } else { - tis.mOverviewCommandHelper.addCommand(CommandType.SHOW); + tis.mOverviewCommandHelper.addCommand(CommandType.SHOW_WITH_FOCUS); } }); } @@ -254,7 +255,7 @@ public class TouchInteractionService extends Service { int displayId = enableAltTabKqsOnConnectedDisplays.isTrue() ? SystemUiProxy.INSTANCE.get(tis).getFocusState().getFocusedDisplayId() : DEFAULT_DISPLAY; - tis.mOverviewCommandHelper.addCommand(CommandType.HIDE, displayId); + tis.mOverviewCommandHelper.addCommand(CommandType.HIDE_ALT_TAB, displayId); } }); } @@ -451,7 +452,13 @@ public class TouchInteractionService extends Service { @Override public void onActionCornerActivated(int action, int displayId) { - //TODO: b/409036363 - Handle Home and Overview action corner + MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis -> { + ActionCornerHandler actionCornerHandler = tis.mActionCornerHandler; + if (actionCornerHandler == null) { + return; + } + actionCornerHandler.handleAction(action, displayId); + })); } private void executeForTouchInteractionService( @@ -595,9 +602,9 @@ public class TouchInteractionService extends Service { @Override public void onHideOverview(int displayId) { if (enableOverviewOnConnectedDisplays()) { - mOverviewCommandHelper.addCommand(CommandType.HIDE, displayId); + mOverviewCommandHelper.addCommand(CommandType.HIDE_ALT_TAB, displayId); } else { - mOverviewCommandHelper.addCommand(CommandType.HIDE, DEFAULT_DISPLAY); + mOverviewCommandHelper.addCommand(CommandType.HIDE_ALT_TAB, DEFAULT_DISPLAY); } } }; @@ -620,6 +627,7 @@ public class TouchInteractionService extends Service { private InputEventReceiver mInputEventReceiver; private TaskbarManager mTaskbarManager; + private ActionCornerHandler mActionCornerHandler; private Function mSwipeUpProxyProvider = i -> null; private AllAppsActionManager mAllAppsActionManager; private ActiveTrackpadList mTrackpadsConnected; @@ -755,6 +763,7 @@ public class TouchInteractionService extends Service { mOverviewCommandHelper = new OverviewCommandHelper(this, mOverviewComponentObserver, mDisplayRepository, mTaskbarManager, mTaskAnimationManagerRepository); + mActionCornerHandler = new ActionCornerHandler(mOverviewCommandHelper); mUserUnlocked = true; mInputConsumer.registerInputConsumer(); mDeviceStateRepository.forEach(/* createIfAbsent= */ true, deviceState -> diff --git a/quickstep/src/com/android/quickstep/actioncorner/ActionCornerHandler.kt b/quickstep/src/com/android/quickstep/actioncorner/ActionCornerHandler.kt new file mode 100644 index 0000000000..e99b393176 --- /dev/null +++ b/quickstep/src/com/android/quickstep/actioncorner/ActionCornerHandler.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2025 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.quickstep.actioncorner + +import com.android.quickstep.OverviewCommandHelper +import com.android.quickstep.OverviewCommandHelper.CommandType.TOGGLE_OVERVIEW_PREVIOUS +import com.android.systemui.shared.system.actioncorner.ActionCornerConstants.Action +import com.android.systemui.shared.system.actioncorner.ActionCornerConstants.HOME +import com.android.systemui.shared.system.actioncorner.ActionCornerConstants.OVERVIEW + +/** + * Handles actions triggered from action corners that are mapped to specific functionalities. + * Launcher supports both overview and home actions. + */ +class ActionCornerHandler(private val overviewCommandHelper: OverviewCommandHelper) { + + fun handleAction(@Action action: Int, displayId: Int) { + when (action) { + // TODO(b/410798748): handle projected mode when launching overview + OVERVIEW -> overviewCommandHelper.addCommandsForAllDisplays(TOGGLE_OVERVIEW_PREVIOUS) + HOME -> {} // TODO(b/409036363): handle HOME action + else -> {} + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt index c96ccbdce6..7e73d47605 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt @@ -21,12 +21,18 @@ import android.view.Display.DEFAULT_DISPLAY import androidx.test.filters.SmallTest import com.android.app.displaylib.DisplayRepository import com.android.launcher3.Flags +import com.android.launcher3.LauncherState +import com.android.launcher3.statemanager.StateManager +import com.android.launcher3.statemanager.StatefulActivity +import com.android.launcher3.uioverrides.QuickstepLauncher import com.android.launcher3.util.LauncherMultivalentJUnit import com.android.launcher3.util.TestDispatcherProvider import com.android.launcher3.util.rule.setFlags import com.android.quickstep.OverviewCommandHelper.CommandInfo import com.android.quickstep.OverviewCommandHelper.CommandInfo.CommandStatus import com.android.quickstep.OverviewCommandHelper.CommandType +import com.android.quickstep.views.RecentsView +import com.android.quickstep.views.TaskView import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay @@ -46,6 +52,7 @@ import org.mockito.Mockito.doAnswer import org.mockito.Mockito.spy import org.mockito.kotlin.any import org.mockito.kotlin.mock +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest @@ -63,6 +70,10 @@ class OverviewCommandHelperTest { private val displayRepository: DisplayRepository = mock() private val executeCommandDisplayIds = mutableListOf() + private val recentView: RecentsView<*, *> = mock() + private val stateManager: StateManager> = mock() + private val containerInterface: BaseActivityInterface = mock() + private fun setupDefaultDisplay() { whenever(displayRepository.displayIds).thenReturn(MutableStateFlow(setOf(DEFAULT_DISPLAY))) } @@ -79,18 +90,30 @@ class OverviewCommandHelperTest { setupDefaultDisplay() + val overviewComponentObserver = mock() + whenever(overviewComponentObserver.getContainerInterface(any())) + .thenReturn(containerInterface) + whenever(recentView.getStateManager()).thenReturn(stateManager) + whenever(containerInterface.switchToRecentsIfVisible(any())).thenReturn(true) + sut = spy( OverviewCommandHelper( touchInteractionService = mock(), - overviewComponentObserver = mock(), + overviewComponentObserver = overviewComponentObserver, dispatcherProvider = TestDispatcherProvider(dispatcher), displayRepository = displayRepository, taskbarManager = mock(), taskAnimationManagerRepository = mock(), ) ) + } + private fun addCallbackDelay(delayInMillis: Long = 0) { + pendingCallbacksWithDelays.add(delayInMillis) + } + + private fun mockExecuteCommand() { doAnswer { invocation -> val pendingCallback = invocation.arguments[1] as () -> Unit @@ -111,13 +134,10 @@ class OverviewCommandHelperTest { .executeCommand(any(), any()) } - private fun addCallbackDelay(delayInMillis: Long = 0) { - pendingCallbacksWithDelays.add(delayInMillis) - } - @Test fun whenFirstCommandIsAdded_executeCommandImmediately() = testScope.runTest { + mockExecuteCommand() // Add command to queue val commandInfo: CommandInfo = sut.addCommand(CommandType.HOME)!! assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) @@ -128,6 +148,7 @@ class OverviewCommandHelperTest { @Test fun whenFirstCommandIsAdded_executeCommandImmediately_WithCallbackDelay() = testScope.runTest { + mockExecuteCommand() addCallbackDelay(100) // Add command to queue @@ -145,6 +166,7 @@ class OverviewCommandHelperTest { @Test fun whenFirstCommandIsPendingCallback_NextCommandWillWait() = testScope.runTest { + mockExecuteCommand() // Add command to queue addCallbackDelay(100) val commandType1 = CommandType.HOME @@ -152,7 +174,7 @@ class OverviewCommandHelperTest { assertThat(commandInfo1.status).isEqualTo(CommandStatus.IDLE) addCallbackDelay(100) - val commandType2 = CommandType.SHOW + val commandType2 = CommandType.SHOW_ALT_TAB val commandInfo2: CommandInfo = sut.addCommand(commandType2)!! assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE) @@ -171,6 +193,7 @@ class OverviewCommandHelperTest { @Test fun whenCommandTakesTooLong_TriggerTimeout_AndExecuteNextCommand() = testScope.runTest { + mockExecuteCommand() // Add command to queue addCallbackDelay(QUEUE_TIMEOUT) val commandType1 = CommandType.HOME @@ -178,7 +201,7 @@ class OverviewCommandHelperTest { assertThat(commandInfo1.status).isEqualTo(CommandStatus.IDLE) addCallbackDelay(100) - val commandType2 = CommandType.SHOW + val commandType2 = CommandType.SHOW_ALT_TAB val commandInfo2: CommandInfo = sut.addCommand(commandType2)!! assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE) @@ -197,6 +220,7 @@ class OverviewCommandHelperTest { @Test fun whenAllDisplaysCommandIsAdded_singleCommandProcessedForDefaultDisplay() = testScope.runTest { + mockExecuteCommand() executeCommandDisplayIds.clear() // Add command to queue val commandInfo: CommandInfo = sut.addCommandsForAllDisplays(CommandType.HOME)!! @@ -209,6 +233,7 @@ class OverviewCommandHelperTest { @Test fun whenAllDisplaysCommandIsAdded_multipleCommandsProcessedForMultipleDisplays() = testScope.runTest { + mockExecuteCommand() setupMultipleDisplays() executeCommandDisplayIds.clear() // Add command to queue @@ -223,6 +248,7 @@ class OverviewCommandHelperTest { @Test fun whenAllExceptDisplayCommandIsAdded_otherDisplayProcessed() = testScope.runTest { + mockExecuteCommand() setupMultipleDisplays() executeCommandDisplayIds.clear() // Add command to queue @@ -237,6 +263,7 @@ class OverviewCommandHelperTest { @Test fun whenSingleDisplayCommandIsAdded_thatDisplayIsProcessed() = testScope.runTest { + mockExecuteCommand() executeCommandDisplayIds.clear() val displayId = 5 // Add command to queue @@ -247,6 +274,39 @@ class OverviewCommandHelperTest { assertThat(executeCommandDisplayIds).containsExactly(displayId) } + @Test + fun recentViewNotVisible_toggleOverviewPrev_goToOverview() = + testScope.runTest { + whenever(containerInterface.getVisibleRecentsView>()).thenReturn(null) + sut.addCommand(CommandType.TOGGLE_OVERVIEW_PREVIOUS)!! + runCurrent() + verify(containerInterface).switchToRecentsIfVisible(any()) + } + + @Test + fun recentViewVisible_toggleOverviewPrev_goToHome() = + testScope.runTest { + whenever(containerInterface.getVisibleRecentsView>()) + .thenReturn(recentView) + sut.addCommand(CommandType.TOGGLE_OVERVIEW_PREVIOUS)!! + runCurrent() + verify(recentView).startHome() + } + + @Test + fun recentViewVisible_hasRunningTask_toggleOverviewPrev_goToPrevTask() = + testScope.runTest { + whenever(containerInterface.getVisibleRecentsView>()) + .thenReturn(recentView) + val mockTask = mock() + whenever(recentView.runningTaskView).thenReturn(mockTask) + + sut.addCommand(CommandType.TOGGLE_OVERVIEW_PREVIOUS)!! + runCurrent() + + verify(mockTask).launchWithAnimation() + } + private companion object { const val QUEUE_TIMEOUT = 5001L const val EXTERNAL_DISPLAY_ID = 1