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.util.Executors.MAIN_EXECUTOR; 19 import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID; 20 21 import android.annotation.TargetApi; 22 import android.content.Intent; 23 import android.graphics.PointF; 24 import android.os.Build; 25 import android.os.SystemClock; 26 import android.os.Trace; 27 import android.view.View; 28 29 import androidx.annotation.BinderThread; 30 import androidx.annotation.NonNull; 31 import androidx.annotation.Nullable; 32 import androidx.annotation.UiThread; 33 34 import com.android.launcher3.DeviceProfile; 35 import com.android.launcher3.config.FeatureFlags; 36 import com.android.launcher3.statemanager.StatefulActivity; 37 import com.android.launcher3.taskbar.TaskbarUIController; 38 import com.android.launcher3.util.RunnableList; 39 import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener; 40 import com.android.quickstep.views.DesktopTaskView; 41 import com.android.quickstep.views.RecentsView; 42 import com.android.quickstep.views.TaskView; 43 import com.android.systemui.shared.recents.model.ThumbnailData; 44 import com.android.systemui.shared.system.InteractionJankMonitorWrapper; 45 46 import java.io.PrintWriter; 47 import java.util.ArrayList; 48 import java.util.HashMap; 49 50 /** 51 * Helper class to handle various atomic commands for switching between Overview. 52 */ 53 @TargetApi(Build.VERSION_CODES.P) 54 public class OverviewCommandHelper { 55 56 public static final int TYPE_SHOW = 1; 57 public static final int TYPE_KEYBOARD_INPUT = 2; 58 public static final int TYPE_HIDE = 3; 59 public static final int TYPE_TOGGLE = 4; 60 public static final int TYPE_HOME = 5; 61 62 /** 63 * Use case for needing a queue is double tapping recents button in 3 button nav. 64 * Size of 2 should be enough. We'll toss in one more because we're kind hearted. 65 */ 66 private final static int MAX_QUEUE_SIZE = 3; 67 68 private static final String TRANSITION_NAME = "Transition:toOverview"; 69 70 private final TouchInteractionService mService; 71 private final OverviewComponentObserver mOverviewComponentObserver; 72 private final TaskAnimationManager mTaskAnimationManager; 73 private final ArrayList<CommandInfo> mPendingCommands = new ArrayList<>(); 74 75 /** 76 * Index of the TaskView that should be focused when launching Overview. Persisted so that we 77 * do not lose the focus across multiple calls of 78 * {@link OverviewCommandHelper#executeCommand(CommandInfo)} for the same command 79 */ 80 private int mTaskFocusIndexOverride = -1; 81 OverviewCommandHelper(TouchInteractionService service, OverviewComponentObserver observer, TaskAnimationManager taskAnimationManager)82 public OverviewCommandHelper(TouchInteractionService service, 83 OverviewComponentObserver observer, 84 TaskAnimationManager taskAnimationManager) { 85 mService = service; 86 mOverviewComponentObserver = observer; 87 mTaskAnimationManager = taskAnimationManager; 88 } 89 90 /** 91 * Called when the command finishes execution. 92 */ scheduleNextTask(CommandInfo command)93 private void scheduleNextTask(CommandInfo command) { 94 if (!mPendingCommands.isEmpty() && mPendingCommands.get(0) == command) { 95 mPendingCommands.remove(0); 96 executeNext(); 97 } 98 } 99 100 /** 101 * Executes the next command from the queue. If the command finishes immediately (returns true), 102 * it continues to execute the next command, until the queue is empty of a command defer's its 103 * completion (returns false). 104 */ 105 @UiThread executeNext()106 private void executeNext() { 107 if (mPendingCommands.isEmpty()) { 108 return; 109 } 110 CommandInfo cmd = mPendingCommands.get(0); 111 if (executeCommand(cmd)) { 112 scheduleNextTask(cmd); 113 } 114 } 115 116 @UiThread addCommand(CommandInfo cmd)117 private void addCommand(CommandInfo cmd) { 118 boolean wasEmpty = mPendingCommands.isEmpty(); 119 mPendingCommands.add(cmd); 120 if (wasEmpty) { 121 executeNext(); 122 } 123 } 124 125 /** 126 * Adds a command to be executed next, after all pending tasks are completed. 127 * Max commands that can be queued is {@link #MAX_QUEUE_SIZE}. 128 * Requests after reaching that limit will be silently dropped. 129 */ 130 @BinderThread addCommand(int type)131 public void addCommand(int type) { 132 if (mPendingCommands.size() >= MAX_QUEUE_SIZE) { 133 return; 134 } 135 CommandInfo cmd = new CommandInfo(type); 136 MAIN_EXECUTOR.execute(() -> addCommand(cmd)); 137 } 138 139 @UiThread clearPendingCommands()140 public void clearPendingCommands() { 141 mPendingCommands.clear(); 142 } 143 144 @Nullable getNextTask(RecentsView view)145 private TaskView getNextTask(RecentsView view) { 146 final TaskView runningTaskView = view.getRunningTaskView(); 147 148 if (runningTaskView == null) { 149 return view.getTaskViewAt(0); 150 } else { 151 final TaskView nextTask = view.getNextTaskView(); 152 return nextTask != null ? nextTask : runningTaskView; 153 } 154 } 155 launchTask(RecentsView recents, @Nullable TaskView taskView, CommandInfo cmd)156 private boolean launchTask(RecentsView recents, @Nullable TaskView taskView, CommandInfo cmd) { 157 RunnableList callbackList = null; 158 if (taskView != null) { 159 taskView.setEndQuickswitchCuj(true); 160 callbackList = taskView.launchTasks(); 161 } 162 163 if (callbackList != null) { 164 callbackList.add(() -> scheduleNextTask(cmd)); 165 return false; 166 } else { 167 recents.startHome(); 168 return true; 169 } 170 } 171 172 /** 173 * Executes the task and returns true if next task can be executed. If false, then the next 174 * task is deferred until {@link #scheduleNextTask} is called 175 */ executeCommand(CommandInfo cmd)176 private <T extends StatefulActivity<?>> boolean executeCommand(CommandInfo cmd) { 177 BaseActivityInterface<?, T> activityInterface = 178 mOverviewComponentObserver.getActivityInterface(); 179 RecentsView recents = activityInterface.getVisibleRecentsView(); 180 if (recents == null) { 181 T activity = activityInterface.getCreatedActivity(); 182 DeviceProfile dp = activity == null ? null : activity.getDeviceProfile(); 183 TaskbarUIController uiController = activityInterface.getTaskbarController(); 184 boolean allowQuickSwitch = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get() 185 && uiController != null 186 && dp != null 187 && (dp.isTablet || dp.isTwoPanels); 188 189 if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) { 190 // TODO(b/268075592): add support for quickswitch to/from desktop 191 allowQuickSwitch = false; 192 } 193 194 if (cmd.type == TYPE_HIDE) { 195 if (!allowQuickSwitch) { 196 return true; 197 } 198 mTaskFocusIndexOverride = uiController.launchFocusedTask(); 199 if (mTaskFocusIndexOverride == -1) { 200 return true; 201 } 202 } 203 if (cmd.type == TYPE_KEYBOARD_INPUT && allowQuickSwitch) { 204 uiController.openQuickSwitchView(); 205 return true; 206 } 207 if (cmd.type == TYPE_HOME) { 208 mService.startActivity(mOverviewComponentObserver.getHomeIntent()); 209 return true; 210 } 211 } else { 212 switch (cmd.type) { 213 case TYPE_SHOW: 214 // already visible 215 return true; 216 case TYPE_HIDE: { 217 mTaskFocusIndexOverride = -1; 218 int currentPage = recents.getNextPage(); 219 TaskView tv = (currentPage >= 0 && currentPage < recents.getTaskViewCount()) 220 ? (TaskView) recents.getPageAt(currentPage) 221 : null; 222 return launchTask(recents, tv, cmd); 223 } 224 case TYPE_TOGGLE: 225 return launchTask(recents, getNextTask(recents), cmd); 226 case TYPE_HOME: 227 recents.startHome(); 228 return true; 229 } 230 } 231 232 final Runnable completeCallback = () -> { 233 RecentsView rv = activityInterface.getVisibleRecentsView(); 234 if (rv != null && (cmd.type == TYPE_KEYBOARD_INPUT || cmd.type == TYPE_HIDE)) { 235 updateRecentsViewFocus(rv); 236 } 237 scheduleNextTask(cmd); 238 }; 239 if (activityInterface.switchToRecentsIfVisible(completeCallback)) { 240 // If successfully switched, wait until animation finishes 241 return false; 242 } 243 244 final T activity = activityInterface.getCreatedActivity(); 245 if (activity != null) { 246 InteractionJankMonitorWrapper.begin( 247 activity.getRootView(), 248 InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH); 249 } 250 251 GestureState gestureState = mService.createGestureState(GestureState.DEFAULT_STATE); 252 gestureState.setHandlingAtomicEvent(true); 253 AbsSwipeUpHandler interactionHandler = mService.getSwipeUpHandlerFactory() 254 .newHandler(gestureState, cmd.createTime); 255 interactionHandler.setGestureEndCallback( 256 () -> onTransitionComplete(cmd, interactionHandler)); 257 interactionHandler.initWhenReady(); 258 259 RecentsAnimationListener recentAnimListener = new RecentsAnimationListener() { 260 @Override 261 public void onRecentsAnimationStart(RecentsAnimationController controller, 262 RecentsAnimationTargets targets) { 263 activityInterface.runOnInitBackgroundStateUI(() -> 264 interactionHandler.onGestureEnded(0, new PointF(), new PointF())); 265 cmd.removeListener(this); 266 } 267 268 @Override 269 public void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) { 270 interactionHandler.onGestureCancelled(); 271 cmd.removeListener(this); 272 273 T createdActivity = activityInterface.getCreatedActivity(); 274 if (createdActivity == null) { 275 return; 276 } 277 RecentsView createdRecents = createdActivity.getOverviewPanel(); 278 if (createdRecents != null) { 279 createdRecents.onRecentsAnimationComplete(); 280 } 281 } 282 }; 283 284 RecentsView<?, ?> visibleRecentsView = activityInterface.getVisibleRecentsView(); 285 if (visibleRecentsView != null) { 286 visibleRecentsView.moveRunningTaskToFront(); 287 } 288 if (mTaskAnimationManager.isRecentsAnimationRunning()) { 289 cmd.mActiveCallbacks = mTaskAnimationManager.continueRecentsAnimation(gestureState); 290 cmd.mActiveCallbacks.addListener(interactionHandler); 291 mTaskAnimationManager.notifyRecentsAnimationState(interactionHandler); 292 interactionHandler.onGestureStarted(true /*isLikelyToStartNewTask*/); 293 294 cmd.mActiveCallbacks.addListener(recentAnimListener); 295 mTaskAnimationManager.notifyRecentsAnimationState(recentAnimListener); 296 } else { 297 Intent intent = new Intent(interactionHandler.getLaunchIntent()); 298 intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, gestureState.getGestureId()); 299 cmd.mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation( 300 gestureState, intent, interactionHandler); 301 interactionHandler.onGestureStarted(false /*isLikelyToStartNewTask*/); 302 cmd.mActiveCallbacks.addListener(recentAnimListener); 303 } 304 305 Trace.beginAsyncSection(TRANSITION_NAME, 0); 306 return false; 307 } 308 onTransitionComplete(CommandInfo cmd, AbsSwipeUpHandler handler)309 private void onTransitionComplete(CommandInfo cmd, AbsSwipeUpHandler handler) { 310 cmd.removeListener(handler); 311 Trace.endAsyncSection(TRANSITION_NAME, 0); 312 313 RecentsView rv = 314 mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView(); 315 if (rv != null && (cmd.type == TYPE_KEYBOARD_INPUT || cmd.type == TYPE_HIDE)) { 316 updateRecentsViewFocus(rv); 317 } 318 scheduleNextTask(cmd); 319 } 320 updateRecentsViewFocus(@onNull RecentsView rv)321 private void updateRecentsViewFocus(@NonNull RecentsView rv) { 322 // When the overview is launched via alt tab (cmd type is TYPE_KEYBOARD_INPUT), 323 // the touch mode somehow is not change to false by the Android framework. 324 // The subsequent tab to go through tasks in overview can only be dispatched to 325 // focuses views, while focus can only be requested in 326 // {@link View#requestFocusNoSearch(int, Rect)} when touch mode is false. To note, 327 // here we launch overview with live tile. 328 rv.getViewRootImpl().touchModeChanged(false); 329 // Ensure that recents view has focus so that it receives the followup key inputs 330 TaskView taskView = rv.getTaskViewAt(mTaskFocusIndexOverride); 331 if (taskView != null) { 332 requestFocus(taskView); 333 return; 334 } 335 taskView = rv.getNextTaskView(); 336 if (taskView != null) { 337 requestFocus(taskView); 338 return; 339 } 340 taskView = rv.getTaskViewAt(0); 341 if (taskView != null) { 342 requestFocus(taskView); 343 return; 344 } 345 requestFocus(rv); 346 } 347 requestFocus(@onNull View view)348 private void requestFocus(@NonNull View view) { 349 view.post(() -> { 350 view.requestFocus(); 351 view.requestAccessibilityFocus(); 352 }); 353 } 354 dump(PrintWriter pw)355 public void dump(PrintWriter pw) { 356 pw.println("OverviewCommandHelper:"); 357 pw.println(" mPendingCommands=" + mPendingCommands.size()); 358 if (!mPendingCommands.isEmpty()) { 359 pw.println(" pendingCommandType=" + mPendingCommands.get(0).type); 360 } 361 pw.println(" mTaskFocusIndexOverride=" + mTaskFocusIndexOverride); 362 } 363 364 private static class CommandInfo { 365 public final long createTime = SystemClock.elapsedRealtime(); 366 public final int type; 367 RecentsAnimationCallbacks mActiveCallbacks; 368 CommandInfo(int type)369 CommandInfo(int type) { 370 this.type = type; 371 } 372 removeListener(RecentsAnimationListener listener)373 void removeListener(RecentsAnimationListener listener) { 374 if (mActiveCallbacks != null) { 375 mActiveCallbacks.removeListener(listener); 376 } 377 } 378 } 379 } 380