1 /* 2 * 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 static com.android.launcher3.PagedView.INVALID_PAGE; 19 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON; 20 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH; 21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT; 22 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 23 import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID; 24 25 import android.animation.Animator; 26 import android.animation.AnimatorListenerAdapter; 27 import android.content.Intent; 28 import android.graphics.PointF; 29 import android.os.SystemClock; 30 import android.os.Trace; 31 import android.util.Log; 32 import android.view.View; 33 34 import androidx.annotation.BinderThread; 35 import androidx.annotation.NonNull; 36 import androidx.annotation.Nullable; 37 import androidx.annotation.UiThread; 38 39 import com.android.internal.jank.Cuj; 40 import com.android.launcher3.DeviceProfile; 41 import com.android.launcher3.config.FeatureFlags; 42 import com.android.launcher3.logger.LauncherAtom; 43 import com.android.launcher3.logging.StatsLogManager; 44 import com.android.launcher3.statemanager.StatefulActivity; 45 import com.android.launcher3.taskbar.TaskbarUIController; 46 import com.android.launcher3.util.RunnableList; 47 import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener; 48 import com.android.quickstep.util.ActiveGestureLog; 49 import com.android.quickstep.views.RecentsView; 50 import com.android.quickstep.views.RecentsViewContainer; 51 import com.android.quickstep.views.TaskView; 52 import com.android.systemui.shared.recents.model.ThumbnailData; 53 import com.android.systemui.shared.system.InteractionJankMonitorWrapper; 54 55 import java.io.PrintWriter; 56 import java.util.ArrayList; 57 import java.util.HashMap; 58 59 /** 60 * Helper class to handle various atomic commands for switching between Overview. 61 */ 62 public class OverviewCommandHelper { 63 private static final String TAG = "OverviewCommandHelper"; 64 65 public static final int TYPE_SHOW = 1; 66 public static final int TYPE_KEYBOARD_INPUT = 2; 67 public static final int TYPE_HIDE = 3; 68 public static final int TYPE_TOGGLE = 4; 69 public static final int TYPE_HOME = 5; 70 71 /** 72 * Use case for needing a queue is double tapping recents button in 3 button nav. 73 * Size of 2 should be enough. We'll toss in one more because we're kind hearted. 74 */ 75 private final static int MAX_QUEUE_SIZE = 3; 76 77 private static final String TRANSITION_NAME = "Transition:toOverview"; 78 79 private final TouchInteractionService mService; 80 private final OverviewComponentObserver mOverviewComponentObserver; 81 private final TaskAnimationManager mTaskAnimationManager; 82 private final ArrayList<CommandInfo> mPendingCommands = new ArrayList<>(); 83 84 /** 85 * Index of the TaskView that should be focused when launching Overview. Persisted so that we 86 * do not lose the focus across multiple calls of 87 * {@link OverviewCommandHelper#executeCommand(CommandInfo)} for the same command 88 */ 89 private int mKeyboardTaskFocusIndex = -1; 90 91 /** 92 * Whether we should incoming toggle commands while a previous toggle command is still ongoing. 93 * This serves as a rate-limiter to prevent overlapping animations that can clobber each other 94 * and prevent clean-up callbacks from running. This thus prevents a recurring set of bugs with 95 * janky recents animations and unresponsive home and overview buttons. 96 */ 97 private boolean mWaitForToggleCommandComplete = false; 98 OverviewCommandHelper(TouchInteractionService service, OverviewComponentObserver observer, TaskAnimationManager taskAnimationManager)99 public OverviewCommandHelper(TouchInteractionService service, 100 OverviewComponentObserver observer, 101 TaskAnimationManager taskAnimationManager) { 102 mService = service; 103 mOverviewComponentObserver = observer; 104 mTaskAnimationManager = taskAnimationManager; 105 } 106 107 /** 108 * Called when the command finishes execution. 109 */ scheduleNextTask(CommandInfo command)110 private void scheduleNextTask(CommandInfo command) { 111 if (mPendingCommands.isEmpty()) { 112 Log.d(TAG, "no pending commands to schedule"); 113 return; 114 } 115 if (mPendingCommands.get(0) != command) { 116 Log.d(TAG, "next task not scheduled." 117 + " mPendingCommands[0] type is " + mPendingCommands.get(0) 118 + " - command type is: " + command); 119 return; 120 } 121 Log.d(TAG, "scheduleNextTask called: " + command); 122 mPendingCommands.remove(0); 123 executeNext(); 124 } 125 126 /** 127 * Executes the next command from the queue. If the command finishes immediately (returns true), 128 * it continues to execute the next command, until the queue is empty of a command defer's its 129 * completion (returns false). 130 */ 131 @UiThread executeNext()132 private void executeNext() { 133 if (mPendingCommands.isEmpty()) { 134 Log.d(TAG, "executeNext - mPendingCommands is empty"); 135 return; 136 } 137 CommandInfo cmd = mPendingCommands.get(0); 138 139 boolean result = executeCommand(cmd); 140 Log.d(TAG, "executeNext cmd type: " + cmd + ", result: " + result); 141 if (result) { 142 scheduleNextTask(cmd); 143 } 144 } 145 146 @UiThread addCommand(CommandInfo cmd)147 private void addCommand(CommandInfo cmd) { 148 boolean wasEmpty = mPendingCommands.isEmpty(); 149 mPendingCommands.add(cmd); 150 if (wasEmpty) { 151 executeNext(); 152 } 153 } 154 155 /** 156 * Adds a command to be executed next, after all pending tasks are completed. 157 * Max commands that can be queued is {@link #MAX_QUEUE_SIZE}. 158 * Requests after reaching that limit will be silently dropped. 159 */ 160 @BinderThread addCommand(int type)161 public void addCommand(int type) { 162 if (mPendingCommands.size() >= MAX_QUEUE_SIZE) { 163 Log.d(TAG, "the pending command queue is full (" + mPendingCommands.size() + "). " 164 + "command not added: " + type); 165 return; 166 } 167 Log.d(TAG, "adding command type: " + type); 168 CommandInfo cmd = new CommandInfo(type); 169 MAIN_EXECUTOR.execute(() -> addCommand(cmd)); 170 } 171 172 @UiThread clearPendingCommands()173 public void clearPendingCommands() { 174 Log.d(TAG, "clearing pending commands - size: " + mPendingCommands.size()); 175 mPendingCommands.clear(); 176 } 177 178 @UiThread canStartHomeSafely()179 public boolean canStartHomeSafely() { 180 return mPendingCommands.isEmpty() || mPendingCommands.get(0).type == TYPE_HOME; 181 } 182 183 @Nullable getNextTask(RecentsView view)184 private TaskView getNextTask(RecentsView view) { 185 final TaskView runningTaskView = view.getRunningTaskView(); 186 187 if (runningTaskView == null) { 188 return view.getTaskViewAt(0); 189 } else { 190 final TaskView nextTask = view.getNextTaskView(); 191 return nextTask != null ? nextTask : runningTaskView; 192 } 193 } 194 launchTask(RecentsView recents, @Nullable TaskView taskView, CommandInfo cmd)195 private boolean launchTask(RecentsView recents, @Nullable TaskView taskView, CommandInfo cmd) { 196 RunnableList callbackList = null; 197 if (taskView != null) { 198 mWaitForToggleCommandComplete = true; 199 taskView.setEndQuickSwitchCuj(true); 200 callbackList = taskView.launchTasks(); 201 } 202 203 if (callbackList != null) { 204 callbackList.add(() -> { 205 Log.d(TAG, "launching task callback: " + cmd); 206 scheduleNextTask(cmd); 207 mWaitForToggleCommandComplete = false; 208 }); 209 Log.d(TAG, "launching task - waiting for callback: " + cmd); 210 return false; 211 } else { 212 recents.startHome(); 213 mWaitForToggleCommandComplete = false; 214 return true; 215 } 216 } 217 218 /** 219 * Executes the task and returns true if next task can be executed. If false, then the next 220 * task is deferred until {@link #scheduleNextTask} is called 221 */ executeCommand( CommandInfo cmd)222 private <T extends StatefulActivity<?> & RecentsViewContainer> boolean executeCommand( 223 CommandInfo cmd) { 224 if (mWaitForToggleCommandComplete && cmd.type == TYPE_TOGGLE) { 225 Log.d(TAG, "executeCommand: " + cmd 226 + " - waiting for toggle command complete"); 227 return true; 228 } 229 BaseActivityInterface<?, T> activityInterface = 230 mOverviewComponentObserver.getActivityInterface(); 231 232 RecentsView<?, ?> visibleRecentsView = activityInterface.getVisibleRecentsView(); 233 RecentsView<?, ?> createdRecentsView; 234 235 Log.d(TAG, "executeCommand: " + cmd 236 + " - visibleRecentsView: " + visibleRecentsView); 237 if (visibleRecentsView == null) { 238 T activity = activityInterface.getCreatedContainer(); 239 createdRecentsView = activity == null ? null : activity.getOverviewPanel(); 240 DeviceProfile dp = activity == null ? null : activity.getDeviceProfile(); 241 TaskbarUIController uiController = activityInterface.getTaskbarController(); 242 boolean allowQuickSwitch = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get() 243 && uiController != null 244 && dp != null 245 && (dp.isTablet || dp.isTwoPanels); 246 247 switch (cmd.type) { 248 case TYPE_HIDE: 249 if (!allowQuickSwitch) { 250 return true; 251 } 252 mKeyboardTaskFocusIndex = uiController.launchFocusedTask(); 253 if (mKeyboardTaskFocusIndex == -1) { 254 return true; 255 } 256 break; 257 case TYPE_KEYBOARD_INPUT: 258 if (allowQuickSwitch) { 259 uiController.openQuickSwitchView(); 260 return true; 261 } else { 262 mKeyboardTaskFocusIndex = 0; 263 break; 264 } 265 case TYPE_HOME: 266 ActiveGestureLog.INSTANCE.addLog( 267 "OverviewCommandHelper.executeCommand(TYPE_HOME)"); 268 mService.startActivity(mOverviewComponentObserver.getHomeIntent()); 269 return true; 270 case TYPE_SHOW: 271 // When Recents is not currently visible, the command's type is TYPE_SHOW 272 // when overview is triggered via the keyboard overview button or Action+Tab 273 // keys (Not Alt+Tab which is KQS). The overview button on-screen in 3-button 274 // nav is TYPE_TOGGLE. 275 mKeyboardTaskFocusIndex = 0; 276 break; 277 default: 278 // continue below to handle displaying Recents. 279 } 280 } else { 281 createdRecentsView = visibleRecentsView; 282 switch (cmd.type) { 283 case TYPE_SHOW: 284 // already visible 285 return true; 286 case TYPE_KEYBOARD_INPUT: { 287 if (visibleRecentsView.isHandlingTouch()) { 288 return true; 289 } 290 } 291 case TYPE_HIDE: { 292 if (visibleRecentsView.isHandlingTouch()) { 293 return true; 294 } 295 mKeyboardTaskFocusIndex = INVALID_PAGE; 296 int currentPage = visibleRecentsView.getNextPage(); 297 TaskView tv = (currentPage >= 0 298 && currentPage < visibleRecentsView.getTaskViewCount()) 299 ? (TaskView) visibleRecentsView.getPageAt(currentPage) 300 : null; 301 return launchTask(visibleRecentsView, tv, cmd); 302 } 303 case TYPE_TOGGLE: 304 return launchTask(visibleRecentsView, getNextTask(visibleRecentsView), cmd); 305 case TYPE_HOME: 306 visibleRecentsView.startHome(); 307 return true; 308 } 309 } 310 311 if (createdRecentsView != null) { 312 createdRecentsView.setKeyboardTaskFocusIndex(mKeyboardTaskFocusIndex); 313 } 314 // Handle recents view focus when launching from home 315 Animator.AnimatorListener animatorListener = new AnimatorListenerAdapter() { 316 317 @Override 318 public void onAnimationStart(Animator animation) { 319 super.onAnimationStart(animation); 320 updateRecentsViewFocus(cmd); 321 logShowOverviewFrom(cmd.type); 322 } 323 324 @Override 325 public void onAnimationEnd(Animator animation) { 326 Log.d(TAG, "switching to Overview state - onAnimationEnd: " + cmd); 327 super.onAnimationEnd(animation); 328 onRecentsViewFocusUpdated(cmd); 329 scheduleNextTask(cmd); 330 } 331 }; 332 if (activityInterface.switchToRecentsIfVisible(animatorListener)) { 333 Log.d(TAG, "switching to Overview state - waiting: " + cmd); 334 // If successfully switched, wait until animation finishes 335 return false; 336 } 337 338 final T activity = activityInterface.getCreatedContainer(); 339 if (activity != null) { 340 InteractionJankMonitorWrapper.begin( 341 activity.getRootView(), 342 Cuj.CUJ_LAUNCHER_QUICK_SWITCH); 343 } 344 345 GestureState gestureState = mService.createGestureState(GestureState.DEFAULT_STATE, 346 GestureState.TrackpadGestureType.NONE); 347 gestureState.setHandlingAtomicEvent(true); 348 AbsSwipeUpHandler interactionHandler = mService.getSwipeUpHandlerFactory() 349 .newHandler(gestureState, cmd.createTime); 350 interactionHandler.setGestureEndCallback( 351 () -> onTransitionComplete(cmd, interactionHandler)); 352 interactionHandler.initWhenReady("OverviewCommandHelper: cmd.type=" + cmd.type); 353 354 RecentsAnimationListener recentAnimListener = new RecentsAnimationListener() { 355 @Override 356 public void onRecentsAnimationStart(RecentsAnimationController controller, 357 RecentsAnimationTargets targets) { 358 updateRecentsViewFocus(cmd); 359 logShowOverviewFrom(cmd.type); 360 activityInterface.runOnInitBackgroundStateUI(() -> 361 interactionHandler.onGestureEnded(0, new PointF())); 362 cmd.removeListener(this); 363 } 364 365 @Override 366 public void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) { 367 interactionHandler.onGestureCancelled(); 368 cmd.removeListener(this); 369 370 T createdActivity = activityInterface.getCreatedContainer(); 371 if (createdActivity == null) { 372 return; 373 } 374 if (createdRecentsView != null) { 375 createdRecentsView.onRecentsAnimationComplete(); 376 } 377 } 378 }; 379 380 if (visibleRecentsView != null) { 381 visibleRecentsView.moveRunningTaskToFront(); 382 } 383 if (mTaskAnimationManager.isRecentsAnimationRunning()) { 384 cmd.mActiveCallbacks = mTaskAnimationManager.continueRecentsAnimation(gestureState); 385 cmd.mActiveCallbacks.addListener(interactionHandler); 386 mTaskAnimationManager.notifyRecentsAnimationState(interactionHandler); 387 interactionHandler.onGestureStarted(true /*isLikelyToStartNewTask*/); 388 389 cmd.mActiveCallbacks.addListener(recentAnimListener); 390 mTaskAnimationManager.notifyRecentsAnimationState(recentAnimListener); 391 } else { 392 Intent intent = new Intent(interactionHandler.getLaunchIntent()); 393 intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, gestureState.getGestureId()); 394 cmd.mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation( 395 gestureState, intent, interactionHandler); 396 interactionHandler.onGestureStarted(false /*isLikelyToStartNewTask*/); 397 cmd.mActiveCallbacks.addListener(recentAnimListener); 398 } 399 Trace.beginAsyncSection(TRANSITION_NAME, 0); 400 Log.d(TAG, "switching via recents animation - onGestureStarted: " + cmd); 401 return false; 402 } 403 onTransitionComplete(CommandInfo cmd, AbsSwipeUpHandler handler)404 private void onTransitionComplete(CommandInfo cmd, AbsSwipeUpHandler handler) { 405 Log.d(TAG, "switching via recents animation - onTransitionComplete: " + cmd); 406 cmd.removeListener(handler); 407 Trace.endAsyncSection(TRANSITION_NAME, 0); 408 onRecentsViewFocusUpdated(cmd); 409 scheduleNextTask(cmd); 410 } 411 updateRecentsViewFocus(CommandInfo cmd)412 private void updateRecentsViewFocus(CommandInfo cmd) { 413 RecentsView recentsView = 414 mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView(); 415 if (recentsView == null || (cmd.type != TYPE_KEYBOARD_INPUT && cmd.type != TYPE_HIDE 416 && cmd.type != TYPE_SHOW)) { 417 return; 418 } 419 // When the overview is launched via alt tab (cmd type is TYPE_KEYBOARD_INPUT), 420 // the touch mode somehow is not change to false by the Android framework. 421 // The subsequent tab to go through tasks in overview can only be dispatched to 422 // focuses views, while focus can only be requested in 423 // {@link View#requestFocusNoSearch(int, Rect)} when touch mode is false. To note, 424 // here we launch overview with live tile. 425 recentsView.getViewRootImpl().touchModeChanged(false); 426 // Ensure that recents view has focus so that it receives the followup key inputs 427 if (requestFocus(recentsView.getTaskViewAt(mKeyboardTaskFocusIndex))) { 428 return; 429 } 430 if (requestFocus(recentsView.getNextTaskView())) { 431 return; 432 } 433 if (requestFocus(recentsView.getTaskViewAt(0))) { 434 return; 435 } 436 requestFocus(recentsView); 437 } 438 onRecentsViewFocusUpdated(CommandInfo cmd)439 private void onRecentsViewFocusUpdated(CommandInfo cmd) { 440 RecentsView recentsView = 441 mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView(); 442 if (recentsView == null 443 || cmd.type != TYPE_HIDE 444 || mKeyboardTaskFocusIndex == INVALID_PAGE) { 445 return; 446 } 447 recentsView.setKeyboardTaskFocusIndex(INVALID_PAGE); 448 recentsView.setCurrentPage(mKeyboardTaskFocusIndex); 449 mKeyboardTaskFocusIndex = INVALID_PAGE; 450 } 451 requestFocus(@ullable View taskView)452 private boolean requestFocus(@Nullable View taskView) { 453 if (taskView == null) { 454 return false; 455 } 456 taskView.post(() -> { 457 taskView.requestFocus(); 458 taskView.requestAccessibilityFocus(); 459 }); 460 return true; 461 } 462 463 private <T extends StatefulActivity<?> & RecentsViewContainer> logShowOverviewFrom(int cmdType)464 void logShowOverviewFrom(int cmdType) { 465 BaseActivityInterface<?, T> activityInterface = 466 mOverviewComponentObserver.getActivityInterface(); 467 var container = activityInterface.getCreatedContainer(); 468 if (container != null) { 469 StatsLogManager.LauncherEvent event; 470 switch (cmdType) { 471 case TYPE_SHOW -> event = LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT; 472 case TYPE_HIDE -> 473 event = LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH; 474 case TYPE_TOGGLE -> event = LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON; 475 default -> { 476 return; 477 } 478 } 479 480 StatsLogManager.newInstance(container.asContext()) 481 .logger() 482 .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder() 483 .setTaskSwitcherContainer( 484 LauncherAtom.TaskSwitcherContainer.getDefaultInstance()) 485 .build()) 486 .log(event); 487 } 488 } 489 dump(PrintWriter pw)490 public void dump(PrintWriter pw) { 491 pw.println("OverviewCommandHelper:"); 492 pw.println(" mPendingCommands=" + mPendingCommands.size()); 493 if (!mPendingCommands.isEmpty()) { 494 pw.println(" pendingCommandType=" + mPendingCommands.get(0).type); 495 } 496 pw.println(" mKeyboardTaskFocusIndex=" + mKeyboardTaskFocusIndex); 497 pw.println(" mWaitForToggleCommandComplete=" + mWaitForToggleCommandComplete); 498 } 499 500 private static class CommandInfo { 501 public final long createTime = SystemClock.elapsedRealtime(); 502 public final int type; 503 RecentsAnimationCallbacks mActiveCallbacks; 504 CommandInfo(int type)505 CommandInfo(int type) { 506 this.type = type; 507 } 508 removeListener(RecentsAnimationListener listener)509 void removeListener(RecentsAnimationListener listener) { 510 if (mActiveCallbacks != null) { 511 mActiveCallbacks.removeListener(listener); 512 } 513 } 514 515 @NonNull 516 @Override toString()517 public String toString() { 518 return "CommandInfo(" 519 + "type=" + type + ", " 520 + "createTime=" + createTime + ", " 521 + "mActiveCallbacks=" + mActiveCallbacks 522 + ")"; 523 } 524 } 525 } 526