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