From 69a9346b07c74dcd110dc9b5f13198342cc09681 Mon Sep 17 00:00:00 2001 From: Will Osborn Date: Tue, 11 Mar 2025 17:13:50 +0000 Subject: [PATCH] Make OverviewCommandHelper commands and OverviewCommandHelperTest display-aware Test: locally tested on Tangor Flag: EXEMPT refactor Bug: 397942185 Change-Id: Ib2c3b2662413d15926215c18421168e93ce2b117 --- .../quickstep/OverviewCommandHelper.kt | 95 ++++++++++++++----- .../quickstep/OverviewCommandHelperTest.kt | 82 +++++++++++++++- 2 files changed, 153 insertions(+), 24 deletions(-) diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt index 152630a5a0..6a9c3dd519 100644 --- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt +++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt @@ -91,27 +91,33 @@ constructor( */ private var keyboardTaskFocusIndex = -1 - // TODO (b/397942185): get per-display interface - private val containerInterface: BaseContainerInterface<*, *> - get() = overviewComponentObserver.getContainerInterface(DEFAULT_DISPLAY) + private fun getContainerInterface(displayId: Int) = + overviewComponentObserver.getContainerInterface(displayId) - // TODO (b/397942185): get per-display RecentsView - private val visibleRecentsView: RecentsView<*, *>? - get() = containerInterface.getVisibleRecentsView>() + private fun getVisibleRecentsView(displayId: Int) = + getContainerInterface(displayId).getVisibleRecentsView>() /** * Adds a command to be executed next, after all pending tasks are completed. Max commands that * can be queued is [.MAX_QUEUE_SIZE]. Requests after reaching that limit will be silently * dropped. + * + * @param type The type of the command + * @param onDisplays The display to run the command on */ @BinderThread - fun addCommand(type: CommandType): CommandInfo? { + @JvmOverloads + fun addCommand( + type: CommandType, + displayId: Int = DEFAULT_DISPLAY, + isLastOfBatch: Boolean = true, + ): CommandInfo? { if (commandQueue.size >= MAX_QUEUE_SIZE) { Log.d(TAG, "command not added: $type - queue is full ($commandQueue).") return null } - val command = CommandInfo(type) + val command = CommandInfo(type, displayId = displayId, isLastOfBatch = isLastOfBatch) commandQueue.add(command) Log.d(TAG, "command added: $command") @@ -129,6 +135,35 @@ constructor( return command } + @BinderThread + fun addCommandsForDisplays(type: CommandType, displayIds: IntArray): CommandInfo? { + if (displayIds.isEmpty()) return null + var lastCommand: CommandInfo? = null + displayIds.forEachIndexed({ i, displayId -> + lastCommand = addCommand(type, displayId, i == displayIds.size - 1) + }) + return lastCommand + } + + @BinderThread + fun addCommandsForAllDisplays(type: CommandType) = + addCommandsForDisplays( + type, + recentsDisplayModel.activeDisplayResources + .map { resource -> resource.displayId } + .toIntArray(), + ) + + @BinderThread + fun addCommandsForDisplaysExcept(type: CommandType, excludedDisplayId: Int) = + addCommandsForDisplays( + type, + recentsDisplayModel.activeDisplayResources + .map { resource -> resource.displayId } + .filter { displayId -> displayId != excludedDisplayId } + .toIntArray(), + ) + fun canStartHomeSafely(): Boolean = commandQueue.isEmpty() || commandQueue.first().type == HOME /** Clear pending or completed commands from the queue */ @@ -143,7 +178,7 @@ constructor( * completion (returns false). */ @UiThread - private fun processNextCommand() = + private fun processNextCommand(): Unit = traceSection("OverviewCommandHelper.processNextCommand") { val command: CommandInfo? = commandQueue.firstOrNull() if (command == null) { @@ -182,7 +217,7 @@ constructor( */ @VisibleForTesting fun executeCommand(command: CommandInfo, onCallbackResult: () -> Unit): Boolean { - val recentsView = visibleRecentsView + val recentsView = getVisibleRecentsView(command.displayId) Log.d(TAG, "executeCommand: $command - visibleRecentsView: $recentsView") return if (recentsView != null) { executeWhenRecentsIsVisible(command, recentsView, onCallbackResult) @@ -230,6 +265,7 @@ constructor( launchTask(recentsView, taskView, command, onCallbackResult) } } + TOGGLE -> { launchTask( recentsView, @@ -238,6 +274,7 @@ constructor( onCallbackResult, ) } + HOME -> { recentsView.startHome() true @@ -294,6 +331,7 @@ constructor( command: CommandInfo, onCallbackResult: () -> Unit, ): Boolean { + val containerInterface = getContainerInterface(command.displayId) val recentsViewContainer = containerInterface.getCreatedContainer() val recentsView: RecentsView<*, *>? = recentsViewContainer?.getOverviewPanel() val deviceProfile = recentsViewContainer?.getDeviceProfile() @@ -335,6 +373,7 @@ constructor( if (keyboardTaskFocusIndex == -1) return true } + KEYBOARD_INPUT -> if (uiController != null && deviceProfile?.isTablet == true) { if ( @@ -348,6 +387,7 @@ constructor( } else { keyboardTaskFocusIndex = 0 } + HOME -> { ActiveGestureProtoLogProxy.logExecuteHomeCommand() // Although IActivityTaskManager$Stub$Proxy.startActivity is a slow binder call, @@ -357,12 +397,14 @@ constructor( touchInteractionService.startActivity(overviewComponentObserver.homeIntent) return true } + SHOW -> // 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 -> {} } @@ -378,7 +420,7 @@ constructor( Log.d(TAG, "switching to Overview state - onAnimationStart: $command") super.onAnimationStart(animation) updateRecentsViewFocus(command) - logShowOverviewFrom(command.type) + logShowOverviewFrom(command) } override fun onAnimationEnd(animation: Animator) { @@ -402,7 +444,7 @@ constructor( val gestureState = touchInteractionService.createGestureState( - focusedDisplayId, + command.displayId, GestureState.DEFAULT_STATE, GestureState.TrackpadGestureType.NONE, ) @@ -432,7 +474,7 @@ constructor( } updateRecentsViewFocus(command) - logShowOverviewFrom(command.type) + logShowOverviewFrom(command) containerInterface.runOnInitBackgroundStateUI { Log.d(TAG, "recents animation started - onInitBackgroundStateUI: $command") interactionHandler.onGestureEnded( @@ -456,12 +498,13 @@ constructor( } } - val displayId = gestureState.displayId val taskAnimationManager = - recentsDisplayModel.getTaskAnimationManager(displayId) + recentsDisplayModel.getTaskAnimationManager(command.displayId) ?: run { - Log.e(TAG, "No TaskAnimationManager found for display $displayId") - ActiveGestureProtoLogProxy.logOnTaskAnimationManagerNotAvailable(displayId) + Log.e(TAG, "No TaskAnimationManager found for display ${command.displayId}") + ActiveGestureProtoLogProxy.logOnTaskAnimationManagerNotAvailable( + command.displayId + ) return false } if (taskAnimationManager.isRecentsAnimationRunning) { @@ -526,8 +569,13 @@ constructor( } private fun updateRecentsViewFocus(command: CommandInfo) { - val recentsView: RecentsView<*, *> = visibleRecentsView ?: return - if (command.type != KEYBOARD_INPUT && command.type != HIDE && command.type != SHOW) { + val recentsView: RecentsView<*, *> = getVisibleRecentsView(command.displayId) ?: return + if ( + command.type != KEYBOARD_INPUT && + command.type != HIDE && + command.type != SHOW && + command.type != TOGGLE + ) { return } @@ -547,7 +595,7 @@ constructor( } private fun onRecentsViewFocusUpdated(command: CommandInfo) { - val recentsView: RecentsView<*, *> = visibleRecentsView ?: return + val recentsView: RecentsView<*, *> = getVisibleRecentsView(command.displayId) ?: return if (command.type != HIDE || keyboardTaskFocusIndex == PagedView.INVALID_PAGE) { return } @@ -565,10 +613,11 @@ constructor( return true } - private fun logShowOverviewFrom(commandType: CommandType) { + private fun logShowOverviewFrom(command: CommandInfo) { + val containerInterface = getContainerInterface(command.displayId) val container = containerInterface.getCreatedContainer() ?: return val event = - when (commandType) { + when (command.type) { SHOW -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT HIDE -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH TOGGLE -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON @@ -601,6 +650,8 @@ constructor( var status: CommandStatus = CommandStatus.IDLE, val createTime: Long = SystemClock.elapsedRealtime(), private var animationCallbacks: RecentsAnimationCallbacks? = null, + val displayId: Int = DEFAULT_DISPLAY, + val isLastOfBatch: Boolean = true, ) { fun setAnimationCallbacks(recentsAnimationCallbacks: RecentsAnimationCallbacks) { this.animationCallbacks = recentsAnimationCallbacks diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt index 381ac6876a..11e0ee88b6 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt @@ -17,6 +17,7 @@ package com.android.quickstep import android.platform.test.flag.junit.SetFlagsRule +import android.view.Display.DEFAULT_DISPLAY import androidx.test.filters.SmallTest import com.android.launcher3.Flags import com.android.launcher3.util.LauncherMultivalentJUnit @@ -25,6 +26,7 @@ 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.fallback.window.RecentsDisplayModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay @@ -41,9 +43,9 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.doAnswer import org.mockito.Mockito.spy -import org.mockito.Mockito.`when` import org.mockito.kotlin.any import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @SmallTest @RunWith(LauncherMultivalentJUnit::class) @@ -57,18 +59,38 @@ class OverviewCommandHelperTest { private var pendingCallbacksWithDelays = mutableListOf() + private val recentsDisplayModel: RecentsDisplayModel = mock() + private val defaultDisplayResource: RecentsDisplayModel.RecentsDisplayResource = mock() + private val secondaryDisplayResource: RecentsDisplayModel.RecentsDisplayResource = mock() + private val executeCommandDisplayIds = mutableListOf() + + private fun setupDefaultDisplay() { + whenever(defaultDisplayResource.displayId).thenReturn(DEFAULT_DISPLAY) + whenever(recentsDisplayModel.activeDisplayResources) + .thenReturn(listOf(defaultDisplayResource)) + } + + private fun setupMultipleDisplays() { + whenever(defaultDisplayResource.displayId).thenReturn(DEFAULT_DISPLAY) + whenever(secondaryDisplayResource.displayId).thenReturn(1) + whenever(recentsDisplayModel.activeDisplayResources) + .thenReturn(listOf(defaultDisplayResource, secondaryDisplayResource)) + } + @Suppress("UNCHECKED_CAST") @Before fun setup() { setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_OVERVIEW_COMMAND_HELPER_TIMEOUT) + setupDefaultDisplay() + sut = spy( OverviewCommandHelper( touchInteractionService = mock(), overviewComponentObserver = mock(), dispatcherProvider = TestDispatcherProvider(dispatcher), - recentsDisplayModel = mock(), + recentsDisplayModel = recentsDisplayModel, focusState = mock(), taskbarManager = mock(), ) @@ -86,6 +108,8 @@ class OverviewCommandHelperTest { } } } + val commandInfo = invocation.arguments[0] as CommandInfo + executeCommandDisplayIds.add(commandInfo.displayId) delayInMillis == null // if no callback to execute, returns success } .`when`(sut) @@ -175,7 +199,61 @@ class OverviewCommandHelperTest { assertThat(commandInfo2.status).isEqualTo(CommandStatus.COMPLETED) } + @Test + fun whenAllDisplaysCommandIsAdded_singleCommandProcessedForDefaultDisplay() = + testScope.runTest { + executeCommandDisplayIds.clear() + // Add command to queue + val commandInfo: CommandInfo = sut.addCommandsForAllDisplays(CommandType.HOME)!! + assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) + runCurrent() + assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED) + assertThat(executeCommandDisplayIds).containsExactly(DEFAULT_DISPLAY) + } + + @Test + fun whenAllDisplaysCommandIsAdded_multipleCommandsProcessedForMultipleDisplays() = + testScope.runTest { + setupMultipleDisplays() + executeCommandDisplayIds.clear() + // Add command to queue + val commandInfo: CommandInfo = sut.addCommandsForAllDisplays(CommandType.HOME)!! + assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) + runCurrent() + assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED) + assertThat(executeCommandDisplayIds) + .containsExactly(DEFAULT_DISPLAY, EXTERNAL_DISPLAY_ID) + } + + @Test + fun whenAllExceptDisplayCommandIsAdded_otherDisplayProcessed() = + testScope.runTest { + setupMultipleDisplays() + executeCommandDisplayIds.clear() + // Add command to queue + val commandInfo: CommandInfo = + sut.addCommandsForDisplaysExcept(CommandType.HOME, DEFAULT_DISPLAY)!! + assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) + runCurrent() + assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED) + assertThat(executeCommandDisplayIds).containsExactly(EXTERNAL_DISPLAY_ID) + } + + @Test + fun whenSingleDisplayCommandIsAdded_thatDisplayIsProcessed() = + testScope.runTest { + executeCommandDisplayIds.clear() + val displayId = 5 + // Add command to queue + val commandInfo: CommandInfo = sut.addCommand(CommandType.HOME, displayId)!! + assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) + runCurrent() + assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED) + assertThat(executeCommandDisplayIds).containsExactly(displayId) + } + private companion object { const val QUEUE_TIMEOUT = 5001L + const val EXTERNAL_DISPLAY_ID = 1 } }