• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.quickstep
17 
18 import android.animation.Animator
19 import android.animation.AnimatorListenerAdapter
20 import android.content.Intent
21 import android.graphics.PointF
22 import android.os.SystemClock
23 import android.os.Trace
24 import android.util.Log
25 import android.view.Display.DEFAULT_DISPLAY
26 import android.view.View
27 import android.window.TransitionInfo
28 import androidx.annotation.BinderThread
29 import androidx.annotation.UiThread
30 import androidx.annotation.VisibleForTesting
31 import com.android.app.tracing.traceSection
32 import com.android.internal.jank.Cuj
33 import com.android.launcher3.Flags.enableAltTabKqsOnConnectedDisplays
34 import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
35 import com.android.launcher3.Flags.enableOverviewCommandHelperTimeout
36 import com.android.launcher3.PagedView
37 import com.android.launcher3.logger.LauncherAtom
38 import com.android.launcher3.logging.StatsLogManager
39 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON
40 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH
41 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT
42 import com.android.launcher3.taskbar.TaskbarManager
43 import com.android.launcher3.taskbar.TaskbarUIController
44 import com.android.launcher3.util.Executors
45 import com.android.launcher3.util.RunnableList
46 import com.android.launcher3.util.coroutines.DispatcherProvider
47 import com.android.launcher3.util.coroutines.ProductionDispatchers
48 import com.android.quickstep.OverviewCommandHelper.CommandInfo.CommandStatus
49 import com.android.quickstep.OverviewCommandHelper.CommandType.HIDE
50 import com.android.quickstep.OverviewCommandHelper.CommandType.HOME
51 import com.android.quickstep.OverviewCommandHelper.CommandType.KEYBOARD_INPUT
52 import com.android.quickstep.OverviewCommandHelper.CommandType.SHOW
53 import com.android.quickstep.OverviewCommandHelper.CommandType.TOGGLE
54 import com.android.quickstep.fallback.window.RecentsDisplayModel
55 import com.android.quickstep.fallback.window.RecentsWindowFlags.Companion.enableOverviewInWindow
56 import com.android.quickstep.util.ActiveGestureLog
57 import com.android.quickstep.util.ActiveGestureProtoLogProxy
58 import com.android.quickstep.views.RecentsView
59 import com.android.quickstep.views.TaskView
60 import com.android.systemui.shared.recents.model.ThumbnailData
61 import com.android.systemui.shared.system.InteractionJankMonitorWrapper
62 import java.io.PrintWriter
63 import java.util.concurrent.ConcurrentLinkedDeque
64 import kotlin.coroutines.resume
65 import kotlinx.coroutines.CoroutineScope
66 import kotlinx.coroutines.SupervisorJob
67 import kotlinx.coroutines.ensureActive
68 import kotlinx.coroutines.launch
69 import kotlinx.coroutines.suspendCancellableCoroutine
70 import kotlinx.coroutines.withTimeout
71 
72 /** Helper class to handle various atomic commands for switching between Overview. */
73 class OverviewCommandHelper
74 @JvmOverloads
75 constructor(
76     private val touchInteractionService: TouchInteractionService,
77     private val overviewComponentObserver: OverviewComponentObserver,
78     private val dispatcherProvider: DispatcherProvider = ProductionDispatchers,
79     private val recentsDisplayModel: RecentsDisplayModel,
80     private val focusState: FocusState,
81     private val taskbarManager: TaskbarManager,
82 ) {
83     private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcherProvider.background)
84 
85     private val commandQueue = ConcurrentLinkedDeque<CommandInfo>()
86 
87     /**
88      * Index of the TaskView that should be focused when launching Overview. Persisted so that we do
89      * not lose the focus across multiple calls of [OverviewCommandHelper.executeCommand] for the
90      * same command
91      */
92     private var keyboardTaskFocusIndex = -1
93 
94     private fun getContainerInterface(displayId: Int) =
95         overviewComponentObserver.getContainerInterface(displayId)
96 
97     private fun getVisibleRecentsView(displayId: Int) =
98         getContainerInterface(displayId).getVisibleRecentsView<RecentsView<*, *>>()
99 
100     /**
101      * Adds a command to be executed next, after all pending tasks are completed. Max commands that
102      * can be queued is [.MAX_QUEUE_SIZE]. Requests after reaching that limit will be silently
103      * dropped.
104      *
105      * @param type The type of the command
106      * @param onDisplays The display to run the command on
107      */
108     @BinderThread
109     @JvmOverloads
110     fun addCommand(
111         type: CommandType,
112         displayId: Int = DEFAULT_DISPLAY,
113         isLastOfBatch: Boolean = true,
114     ): CommandInfo? {
115         if (commandQueue.size >= MAX_QUEUE_SIZE) {
116             Log.d(TAG, "command not added: $type - queue is full ($commandQueue).")
117             return null
118         }
119 
120         val command = CommandInfo(type, displayId = displayId, isLastOfBatch = isLastOfBatch)
121         commandQueue.add(command)
122         Log.d(TAG, "command added: $command")
123 
124         if (commandQueue.size == 1) {
125             Log.d(TAG, "execute: $command - queue size: ${commandQueue.size}")
126             if (enableOverviewCommandHelperTimeout()) {
127                 coroutineScope.launch(dispatcherProvider.main) { processNextCommand() }
128             } else {
129                 Executors.MAIN_EXECUTOR.execute { processNextCommand() }
130             }
131         } else {
132             Log.d(TAG, "not executed: $command - queue size: ${commandQueue.size}")
133         }
134 
135         return command
136     }
137 
138     @BinderThread
139     fun addCommandsForDisplays(type: CommandType, displayIds: IntArray): CommandInfo? {
140         if (displayIds.isEmpty()) return null
141         var lastCommand: CommandInfo? = null
142         displayIds.forEachIndexed({ i, displayId ->
143             lastCommand = addCommand(type, displayId, i == displayIds.size - 1)
144         })
145         return lastCommand
146     }
147 
148     @BinderThread
149     fun addCommandsForAllDisplays(type: CommandType) =
150         addCommandsForDisplays(
151             type,
152             recentsDisplayModel.activeDisplayResources
153                 .map { resource -> resource.displayId }
154                 .toIntArray(),
155         )
156 
157     @BinderThread
158     fun addCommandsForDisplaysExcept(type: CommandType, excludedDisplayId: Int) =
159         addCommandsForDisplays(
160             type,
161             recentsDisplayModel.activeDisplayResources
162                 .map { resource -> resource.displayId }
163                 .filter { displayId -> displayId != excludedDisplayId }
164                 .toIntArray(),
165         )
166 
167     fun canStartHomeSafely(): Boolean = commandQueue.isEmpty() || commandQueue.first().type == HOME
168 
169     /** Clear pending or completed commands from the queue */
170     fun clearPendingCommands() {
171         Log.d(TAG, "clearing pending commands: $commandQueue")
172         commandQueue.removeAll { it.status != CommandStatus.PROCESSING }
173     }
174 
175     /**
176      * Executes the next command from the queue. If the command finishes immediately (returns true),
177      * it continues to execute the next command, until the queue is empty of a command defer's its
178      * completion (returns false).
179      */
180     @UiThread
181     private fun processNextCommand(): Unit =
182         traceSection("OverviewCommandHelper.processNextCommand") {
183             val command: CommandInfo? = commandQueue.firstOrNull()
184             if (command == null) {
185                 Log.d(TAG, "no pending commands to be executed.")
186                 return@traceSection
187             }
188 
189             command.status = CommandStatus.PROCESSING
190             Log.d(TAG, "executing command: $command")
191 
192             if (enableOverviewCommandHelperTimeout()) {
193                 coroutineScope.launch(dispatcherProvider.main) {
194                     traceSection("OverviewCommandHelper.executeCommandWithTimeout") {
195                         withTimeout(QUEUE_WAIT_DURATION_IN_MS) {
196                             executeCommandSuspended(command)
197                             ensureActive()
198                             onCommandFinished(command)
199                         }
200                     }
201                 }
202             } else {
203                 val result =
204                     executeCommand(command, onCallbackResult = { onCommandFinished(command) })
205                 Log.d(TAG, "command executed: $command with result: $result")
206                 if (result) {
207                     onCommandFinished(command)
208                 } else {
209                     Log.d(TAG, "waiting for command callback: $command")
210                 }
211             }
212         }
213 
214     /**
215      * Executes the task and returns true if next task can be executed. If false, then the next task
216      * is deferred until [.scheduleNextTask] is called
217      */
218     @VisibleForTesting
219     fun executeCommand(command: CommandInfo, onCallbackResult: () -> Unit): Boolean {
220         val recentsView = getVisibleRecentsView(command.displayId)
221         Log.d(TAG, "executeCommand: $command - visibleRecentsView: $recentsView")
222         return if (recentsView != null) {
223             executeWhenRecentsIsVisible(command, recentsView, onCallbackResult)
224         } else {
225             executeWhenRecentsIsNotVisible(command, onCallbackResult)
226         }
227     }
228 
229     /**
230      * Executes the task and returns true if next task can be executed. If false, then the next task
231      * is deferred until [.scheduleNextTask] is called
232      */
233     private suspend fun executeCommandSuspended(command: CommandInfo) =
234         suspendCancellableCoroutine { continuation ->
235             fun processResult(isCompleted: Boolean) {
236                 Log.d(TAG, "command executed: $command with result: $isCompleted")
237                 if (isCompleted) {
238                     continuation.resume(Unit)
239                 } else {
240                     Log.d(TAG, "waiting for command callback: $command")
241                 }
242             }
243 
244             val result = executeCommand(command, onCallbackResult = { processResult(true) })
245             processResult(result)
246 
247             continuation.invokeOnCancellation { cancelCommand(command, it) }
248         }
249 
250     private fun executeWhenRecentsIsVisible(
251         command: CommandInfo,
252         recentsView: RecentsView<*, *>,
253         onCallbackResult: () -> Unit,
254     ): Boolean =
255         when (command.type) {
256             SHOW -> true // already visible
257             KEYBOARD_INPUT,
258             HIDE -> {
259                 if (recentsView.isHandlingTouch) {
260                     true
261                 } else {
262                     keyboardTaskFocusIndex = PagedView.INVALID_PAGE
263                     val currentPage = recentsView.nextPage
264                     val taskView = recentsView.getTaskViewAt(currentPage)
265                     launchTask(recentsView, taskView, command, onCallbackResult)
266                 }
267             }
268 
269             TOGGLE -> {
270                 launchTask(
271                     recentsView,
272                     getNextToggledTaskView(recentsView),
273                     command,
274                     onCallbackResult,
275                 )
276             }
277 
278             HOME -> {
279                 recentsView.startHome()
280                 true
281             }
282         }
283 
284     private fun getNextToggledTaskView(recentsView: RecentsView<*, *>): TaskView? {
285         // When running task view is null we return last large taskView - typically focusView when
286         // grid only is not enabled else last desktop task view.
287         return if (recentsView.runningTaskView == null) {
288             recentsView.lastLargeTaskView ?: recentsView.getFirstTaskView()
289         } else {
290             if (
291                 enableLargeDesktopWindowingTile() &&
292                     recentsView.getTaskViewCount() == recentsView.largeTilesCount &&
293                     recentsView.runningTaskView === recentsView.lastLargeTaskView
294             ) {
295                 // Enables the toggle when only large tiles are in recents view.
296                 // We return previous because unlike small tiles, large tiles are always
297                 // on the right hand side.
298                 recentsView.previousTaskView ?: recentsView.runningTaskView
299             } else {
300                 recentsView.nextTaskView ?: recentsView.runningTaskView
301             }
302         }
303     }
304 
305     private fun launchTask(
306         recents: RecentsView<*, *>,
307         taskView: TaskView?,
308         command: CommandInfo,
309         onCallbackResult: () -> Unit,
310     ): Boolean {
311         var callbackList: RunnableList? = null
312         if (taskView != null) {
313             taskView.isEndQuickSwitchCuj = true
314             callbackList = taskView.launchWithAnimation()
315         }
316 
317         if (callbackList != null) {
318             callbackList.add {
319                 Log.d(TAG, "launching task callback: $command")
320                 onCallbackResult()
321             }
322             Log.d(TAG, "launching task - waiting for callback: $command")
323             return false
324         } else {
325             recents.startHome()
326             return true
327         }
328     }
329 
330     private fun executeWhenRecentsIsNotVisible(
331         command: CommandInfo,
332         onCallbackResult: () -> Unit,
333     ): Boolean {
334         val containerInterface = getContainerInterface(command.displayId)
335         val recentsViewContainer = containerInterface.getCreatedContainer()
336         val recentsView: RecentsView<*, *>? = recentsViewContainer?.getOverviewPanel()
337         val deviceProfile = recentsViewContainer?.getDeviceProfile()
338         val uiController = containerInterface.getTaskbarController()
339 
340         val focusedDisplayId = focusState.focusedDisplayId
341         val focusedDisplayUIController: TaskbarUIController? =
342             if (enableOverviewInWindow) {
343                 Log.d(
344                     TAG,
345                     "Querying RecentsDisplayModel for TaskbarUIController for display: $focusedDisplayId",
346                 )
347                 recentsDisplayModel.getRecentsWindowManager(focusedDisplayId)?.taskbarUIController
348             } else {
349                 Log.d(
350                     TAG,
351                     "Querying TaskbarManager for TaskbarUIController for display: $focusedDisplayId",
352                 )
353                 // TODO(b/395061396): Remove this path when overview in widow is enabled.
354                 taskbarManager.getUIControllerForDisplay(focusedDisplayId)
355             }
356         Log.d(
357             TAG,
358             "TaskbarUIController for display $focusedDisplayId was" +
359                 "${if (focusedDisplayUIController == null) " not" else ""} found",
360         )
361 
362         when (command.type) {
363             HIDE -> {
364                 if (uiController == null || deviceProfile?.isTablet == false) return true
365                 keyboardTaskFocusIndex =
366                     if (
367                         enableAltTabKqsOnConnectedDisplays() && focusedDisplayUIController != null
368                     ) {
369                         focusedDisplayUIController.launchFocusedTask()
370                     } else {
371                         uiController.launchFocusedTask()
372                     }
373 
374                 if (keyboardTaskFocusIndex == -1) return true
375             }
376 
377             KEYBOARD_INPUT ->
378                 if (uiController != null && deviceProfile?.isTablet == true) {
379                     if (
380                         enableAltTabKqsOnConnectedDisplays() && focusedDisplayUIController != null
381                     ) {
382                         focusedDisplayUIController.openQuickSwitchView()
383                     } else {
384                         uiController.openQuickSwitchView()
385                     }
386                     return true
387                 } else {
388                     keyboardTaskFocusIndex = 0
389                 }
390 
391             HOME -> {
392                 ActiveGestureProtoLogProxy.logExecuteHomeCommand()
393                 // Although IActivityTaskManager$Stub$Proxy.startActivity is a slow binder call,
394                 // we should still call it on main thread because launcher is waiting for
395                 // ActivityTaskManager to resume it. Also calling startActivity() on bg thread
396                 // could potentially delay resuming launcher. See b/348668521 for more details.
397                 touchInteractionService.startActivity(overviewComponentObserver.homeIntent)
398                 return true
399             }
400 
401             SHOW ->
402                 // When Recents is not currently visible, the command's type is SHOW
403                 // when overview is triggered via the keyboard overview button or Action+Tab
404                 // keys (Not Alt+Tab which is KQS). The overview button on-screen in 3-button
405                 // nav is TYPE_TOGGLE.
406                 keyboardTaskFocusIndex = 0
407 
408             TOGGLE -> {}
409         }
410 
411         recentsView?.setKeyboardTaskFocusIndex(
412             recentsView.indexOfChild(recentsView.taskViews.elementAtOrNull(keyboardTaskFocusIndex))
413                 ?: -1
414         )
415 
416         // Handle recents view focus when launching from home
417         val animatorListener: Animator.AnimatorListener =
418             object : AnimatorListenerAdapter() {
419                 override fun onAnimationStart(animation: Animator) {
420                     Log.d(TAG, "switching to Overview state - onAnimationStart: $command")
421                     super.onAnimationStart(animation)
422                     updateRecentsViewFocus(command)
423                     logShowOverviewFrom(command)
424                 }
425 
426                 override fun onAnimationEnd(animation: Animator) {
427                     Log.d(TAG, "switching to Overview state - onAnimationEnd: $command")
428                     super.onAnimationEnd(animation)
429                     onRecentsViewFocusUpdated(command)
430                     onCallbackResult()
431                 }
432             }
433         if (containerInterface.switchToRecentsIfVisible(animatorListener)) {
434             Log.d(TAG, "switching to Overview state - waiting: $command")
435             // If successfully switched, wait until animation finishes
436             return false
437         }
438 
439         if (!enableOverviewInWindow) {
440             containerInterface.getCreatedContainer()?.rootView?.let { view ->
441                 InteractionJankMonitorWrapper.begin(view, Cuj.CUJ_LAUNCHER_QUICK_SWITCH)
442             }
443         }
444 
445         val gestureState =
446             touchInteractionService.createGestureState(
447                 command.displayId,
448                 GestureState.DEFAULT_STATE,
449                 GestureState.TrackpadGestureType.NONE,
450             )
451         gestureState.isHandlingAtomicEvent = true
452         val interactionHandler =
453             touchInteractionService
454                 // TODO(b/404757863): use command.displayId instead of focusedDisplayId.
455                 .getSwipeUpHandlerFactory(focusedDisplayId)
456                 .newHandler(gestureState, command.createTime)
457         interactionHandler.setGestureEndCallback {
458             onTransitionComplete(command, interactionHandler, onCallbackResult)
459         }
460         interactionHandler.initWhenReady("OverviewCommandHelper: command.type=${command.type}")
461 
462         val recentAnimListener: RecentsAnimationCallbacks.RecentsAnimationListener =
463             object : RecentsAnimationCallbacks.RecentsAnimationListener {
464                 override fun onRecentsAnimationStart(
465                     controller: RecentsAnimationController,
466                     targets: RecentsAnimationTargets,
467                     transitionInfo: TransitionInfo?,
468                 ) {
469                     Log.d(TAG, "recents animation started: $command")
470                     if (enableOverviewInWindow) {
471                         containerInterface.getCreatedContainer()?.rootView?.let { view ->
472                             InteractionJankMonitorWrapper.begin(view, Cuj.CUJ_LAUNCHER_QUICK_SWITCH)
473                         }
474                     }
475 
476                     updateRecentsViewFocus(command)
477                     logShowOverviewFrom(command)
478                     containerInterface.runOnInitBackgroundStateUI {
479                         Log.d(TAG, "recents animation started - onInitBackgroundStateUI: $command")
480                         interactionHandler.onGestureEnded(
481                             0f,
482                             PointF(),
483                             /* horizontalTouchSlopPassed= */ false,
484                         )
485                     }
486                     command.removeListener(this)
487                 }
488 
489                 override fun onRecentsAnimationCanceled(
490                     thumbnailDatas: HashMap<Int, ThumbnailData>
491                 ) {
492                     Log.d(TAG, "recents animation canceled: $command")
493                     interactionHandler.onGestureCancelled()
494                     command.removeListener(this)
495 
496                     containerInterface.getCreatedContainer() ?: return
497                     recentsView?.onRecentsAnimationComplete()
498                 }
499             }
500 
501         val taskAnimationManager =
502             recentsDisplayModel.getTaskAnimationManager(command.displayId)
503                 ?: run {
504                     Log.e(TAG, "No TaskAnimationManager found for display ${command.displayId}")
505                     ActiveGestureProtoLogProxy.logOnTaskAnimationManagerNotAvailable(
506                         command.displayId
507                     )
508                     return false
509                 }
510         if (taskAnimationManager.isRecentsAnimationRunning) {
511             command.setAnimationCallbacks(
512                 taskAnimationManager.continueRecentsAnimation(gestureState)
513             )
514             command.addListener(interactionHandler)
515             taskAnimationManager.notifyRecentsAnimationState(interactionHandler)
516             interactionHandler.onGestureStarted(true /*isLikelyToStartNewTask*/)
517 
518             command.addListener(recentAnimListener)
519             taskAnimationManager.notifyRecentsAnimationState(recentAnimListener)
520         } else {
521             val intent =
522                 Intent(interactionHandler.getLaunchIntent())
523                     .putExtra(ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID, gestureState.gestureId)
524             command.setAnimationCallbacks(
525                 taskAnimationManager.startRecentsAnimation(gestureState, intent, interactionHandler)
526             )
527             interactionHandler.onGestureStarted(false /*isLikelyToStartNewTask*/)
528             command.addListener(recentAnimListener)
529         }
530         Trace.beginAsyncSection(TRANSITION_NAME, 0)
531         Log.d(TAG, "switching via recents animation - onGestureStarted: $command")
532         return false
533     }
534 
535     private fun onTransitionComplete(
536         command: CommandInfo,
537         handler: AbsSwipeUpHandler<*, *, *>,
538         onCommandResult: () -> Unit,
539     ) {
540         Log.d(TAG, "switching via recents animation - onTransitionComplete: $command")
541         command.removeListener(handler)
542         Trace.endAsyncSection(TRANSITION_NAME, 0)
543         onRecentsViewFocusUpdated(command)
544         onCommandResult()
545     }
546 
547     /** Called when the command finishes execution. */
548     private fun onCommandFinished(command: CommandInfo) {
549         command.status = CommandStatus.COMPLETED
550         if (commandQueue.firstOrNull() !== command) {
551             Log.d(
552                 TAG,
553                 "next task not scheduled. First pending command type " +
554                     "is ${commandQueue.firstOrNull()} - command type is: $command",
555             )
556             return
557         }
558 
559         Log.d(TAG, "command executed successfully! $command")
560         commandQueue.remove(command)
561         processNextCommand()
562     }
563 
564     private fun cancelCommand(command: CommandInfo, throwable: Throwable?) {
565         command.status = CommandStatus.CANCELED
566         Log.e(TAG, "command cancelled: $command - $throwable")
567         commandQueue.remove(command)
568         processNextCommand()
569     }
570 
571     private fun updateRecentsViewFocus(command: CommandInfo) {
572         val recentsView: RecentsView<*, *> = getVisibleRecentsView(command.displayId) ?: return
573         if (command.type != KEYBOARD_INPUT && command.type != HIDE && command.type != SHOW) {
574             return
575         }
576 
577         // When the overview is launched via alt tab (command type is TYPE_KEYBOARD_INPUT),
578         // the touch mode somehow is not change to false by the Android framework.
579         // The subsequent tab to go through tasks in overview can only be dispatched to
580         // focuses views, while focus can only be requested in
581         // {@link View#requestFocusNoSearch(int, Rect)} when touch mode is false. To note,
582         // here we launch overview with live tile.
583         recentsView.viewRootImpl.touchModeChanged(false)
584         // Ensure that recents view has focus so that it receives the followup key inputs
585         // Stops requesting focused after first view gets focused.
586         recentsView.getTaskViewAt(keyboardTaskFocusIndex).requestFocus() ||
587             recentsView.nextTaskView.requestFocus() ||
588             recentsView.firstTaskView.requestFocus() ||
589             recentsView.requestFocus()
590     }
591 
592     private fun onRecentsViewFocusUpdated(command: CommandInfo) {
593         val recentsView: RecentsView<*, *> = getVisibleRecentsView(command.displayId) ?: return
594         if (command.type != HIDE || keyboardTaskFocusIndex == PagedView.INVALID_PAGE) {
595             return
596         }
597         recentsView.setKeyboardTaskFocusIndex(PagedView.INVALID_PAGE)
598         recentsView.currentPage = keyboardTaskFocusIndex
599         keyboardTaskFocusIndex = PagedView.INVALID_PAGE
600     }
601 
602     private fun View?.requestFocus(): Boolean {
603         if (this == null) return false
604         post {
605             requestFocus()
606             requestAccessibilityFocus()
607         }
608         return true
609     }
610 
611     private fun logShowOverviewFrom(command: CommandInfo) {
612         val containerInterface = getContainerInterface(command.displayId)
613         val container = containerInterface.getCreatedContainer() ?: return
614         val event =
615             when (command.type) {
616                 SHOW -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT
617                 HIDE -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH
618                 TOGGLE -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON
619                 else -> return
620             }
621         StatsLogManager.newInstance(container.asContext())
622             .logger()
623             .withContainerInfo(
624                 LauncherAtom.ContainerInfo.newBuilder()
625                     .setTaskSwitcherContainer(
626                         LauncherAtom.TaskSwitcherContainer.getDefaultInstance()
627                     )
628                     .build()
629             )
630             .log(event)
631     }
632 
633     fun dump(pw: PrintWriter) {
634         pw.println("OverviewCommandHelper:")
635         pw.println("  pendingCommands=${commandQueue.size}")
636         if (commandQueue.isNotEmpty()) {
637             pw.println("    pendingCommandType=${commandQueue.first().type}")
638         }
639         pw.println("  keyboardTaskFocusIndex=$keyboardTaskFocusIndex")
640     }
641 
642     @VisibleForTesting
643     data class CommandInfo(
644         val type: CommandType,
645         var status: CommandStatus = CommandStatus.IDLE,
646         val createTime: Long = SystemClock.elapsedRealtime(),
647         private var animationCallbacks: RecentsAnimationCallbacks? = null,
648         val displayId: Int = DEFAULT_DISPLAY,
649         val isLastOfBatch: Boolean = true,
650     ) {
651         fun setAnimationCallbacks(recentsAnimationCallbacks: RecentsAnimationCallbacks) {
652             this.animationCallbacks = recentsAnimationCallbacks
653         }
654 
655         fun addListener(listener: RecentsAnimationCallbacks.RecentsAnimationListener) {
656             animationCallbacks?.addListener(listener)
657         }
658 
659         fun removeListener(listener: RecentsAnimationCallbacks.RecentsAnimationListener?) {
660             animationCallbacks?.removeListener(listener)
661         }
662 
663         enum class CommandStatus {
664             IDLE,
665             PROCESSING,
666             COMPLETED,
667             CANCELED,
668         }
669     }
670 
671     enum class CommandType {
672         SHOW,
673         KEYBOARD_INPUT,
674         HIDE,
675         TOGGLE, // Navigate to Overview
676         HOME, // Navigate to Home
677     }
678 
679     companion object {
680         private const val TAG = "OverviewCommandHelper"
681         private const val TRANSITION_NAME = "Transition:toOverview"
682 
683         /**
684          * Use case for needing a queue is double tapping recents button in 3 button nav. Size of 2
685          * should be enough. We'll toss in one more because we're kind hearted.
686          */
687         private const val MAX_QUEUE_SIZE = 3
688         private const val QUEUE_WAIT_DURATION_IN_MS = 5000L
689     }
690 }
691