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.RecentsView; 41 import com.android.quickstep.views.TaskView; 42 import com.android.systemui.shared.recents.model.ThumbnailData; 43 import com.android.systemui.shared.system.InteractionJankMonitorWrapper; 44 45 import java.io.PrintWriter; 46 import java.util.ArrayList; 47 import java.util.HashMap; 48 49 /** 50 * Helper class to handle various atomic commands for switching between Overview. 51 */ 52 @TargetApi(Build.VERSION_CODES.P) 53 public class OverviewCommandHelper { 54 55 public static final int TYPE_SHOW = 1; 56 public static final int TYPE_KEYBOARD_INPUT = 2; 57 public static final int TYPE_HIDE = 3; 58 public static final int TYPE_TOGGLE = 4; 59 public static final int TYPE_HOME = 5; 60 61 /** 62 * Use case for needing a queue is double tapping recents button in 3 button nav. 63 * Size of 2 should be enough. We'll toss in one more because we're kind hearted. 64 */ 65 private final static int MAX_QUEUE_SIZE = 3; 66 67 private static final String TRANSITION_NAME = "Transition:toOverview"; 68 69 private final TouchInteractionService mService; 70 private final OverviewComponentObserver mOverviewComponentObserver; 71 private final TaskAnimationManager mTaskAnimationManager; 72 private final ArrayList<CommandInfo> mPendingCommands = new ArrayList<>(); 73 74 /** 75 * Index of the TaskView that should be focused when launching Overview. Persisted so that we 76 * do not lose the focus across multiple calls of 77 * {@link OverviewCommandHelper#executeCommand(CommandInfo)} for the same command 78 */ 79 private int mTaskFocusIndexOverride = -1; 80 OverviewCommandHelper(TouchInteractionService service, OverviewComponentObserver observer, TaskAnimationManager taskAnimationManager)81 public OverviewCommandHelper(TouchInteractionService service, 82 OverviewComponentObserver observer, 83 TaskAnimationManager taskAnimationManager) { 84 mService = service; 85 mOverviewComponentObserver = observer; 86 mTaskAnimationManager = taskAnimationManager; 87 } 88 89 /** 90 * Called when the command finishes execution. 91 */ scheduleNextTask(CommandInfo command)92 private void scheduleNextTask(CommandInfo command) { 93 if (!mPendingCommands.isEmpty() && mPendingCommands.get(0) == command) { 94 mPendingCommands.remove(0); 95 executeNext(); 96 } 97 } 98 99 /** 100 * Executes the next command from the queue. If the command finishes immediately (returns true), 101 * it continues to execute the next command, until the queue is empty of a command defer's its 102 * completion (returns false). 103 */ 104 @UiThread executeNext()105 private void executeNext() { 106 if (mPendingCommands.isEmpty()) { 107 return; 108 } 109 CommandInfo cmd = mPendingCommands.get(0); 110 if (executeCommand(cmd)) { 111 scheduleNextTask(cmd); 112 } 113 } 114 115 @UiThread addCommand(CommandInfo cmd)116 private void addCommand(CommandInfo cmd) { 117 boolean wasEmpty = mPendingCommands.isEmpty(); 118 mPendingCommands.add(cmd); 119 if (wasEmpty) { 120 executeNext(); 121 } 122 } 123 124 /** 125 * Adds a command to be executed next, after all pending tasks are completed. 126 * Max commands that can be queued is {@link #MAX_QUEUE_SIZE}. 127 * Requests after reaching that limit will be silently dropped. 128 */ 129 @BinderThread addCommand(int type)130 public void addCommand(int type) { 131 if (mPendingCommands.size() >= MAX_QUEUE_SIZE) { 132 return; 133 } 134 CommandInfo cmd = new CommandInfo(type); 135 MAIN_EXECUTOR.execute(() -> addCommand(cmd)); 136 } 137 138 @UiThread clearPendingCommands()139 public void clearPendingCommands() { 140 mPendingCommands.clear(); 141 } 142 143 @UiThread canStartHomeSafely()144 public boolean canStartHomeSafely() { 145 return mPendingCommands.isEmpty() || mPendingCommands.get(0).type == TYPE_HOME; 146 } 147 148 @Nullable getNextTask(RecentsView view)149 private TaskView getNextTask(RecentsView view) { 150 final TaskView runningTaskView = view.getRunningTaskView(); 151 152 if (runningTaskView == null) { 153 return view.getTaskViewAt(0); 154 } else { 155 final TaskView nextTask = view.getNextTaskView(); 156 return nextTask != null ? nextTask : runningTaskView; 157 } 158 } 159 launchTask(RecentsView recents, @Nullable TaskView taskView, CommandInfo cmd)160 private boolean launchTask(RecentsView recents, @Nullable TaskView taskView, CommandInfo cmd) { 161 RunnableList callbackList = null; 162 if (taskView != null) { 163 taskView.setEndQuickswitchCuj(true); 164 callbackList = taskView.launchTasks(); 165 } 166 167 if (callbackList != null) { 168 callbackList.add(() -> scheduleNextTask(cmd)); 169 return false; 170 } else { 171 recents.startHome(); 172 return true; 173 } 174 } 175 176 /** 177 * Executes the task and returns true if next task can be executed. If false, then the next 178 * task is deferred until {@link #scheduleNextTask} is called 179 */ executeCommand(CommandInfo cmd)180 private <T extends StatefulActivity<?>> boolean executeCommand(CommandInfo cmd) { 181 BaseActivityInterface<?, T> activityInterface = 182 mOverviewComponentObserver.getActivityInterface(); 183 RecentsView recents = activityInterface.getVisibleRecentsView(); 184 if (recents == null) { 185 T activity = activityInterface.getCreatedActivity(); 186 DeviceProfile dp = activity == null ? null : activity.getDeviceProfile(); 187 TaskbarUIController uiController = activityInterface.getTaskbarController(); 188 boolean allowQuickSwitch = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get() 189 && uiController != null 190 && dp != null 191 && (dp.isTablet || dp.isTwoPanels); 192 193 if (cmd.type == TYPE_HIDE) { 194 if (!allowQuickSwitch) { 195 return true; 196 } 197 mTaskFocusIndexOverride = uiController.launchFocusedTask(); 198 if (mTaskFocusIndexOverride == -1) { 199 return true; 200 } 201 } 202 if (cmd.type == TYPE_KEYBOARD_INPUT && allowQuickSwitch) { 203 uiController.openQuickSwitchView(); 204 return true; 205 } 206 if (cmd.type == TYPE_HOME) { 207 mService.startActivity(mOverviewComponentObserver.getHomeIntent()); 208 return true; 209 } 210 } else { 211 switch (cmd.type) { 212 case TYPE_SHOW: 213 // already visible 214 return true; 215 case TYPE_HIDE: { 216 mTaskFocusIndexOverride = -1; 217 int currentPage = recents.getNextPage(); 218 TaskView tv = (currentPage >= 0 && currentPage < recents.getTaskViewCount()) 219 ? (TaskView) recents.getPageAt(currentPage) 220 : null; 221 return launchTask(recents, tv, cmd); 222 } 223 case TYPE_TOGGLE: 224 return launchTask(recents, getNextTask(recents), cmd); 225 case TYPE_HOME: 226 recents.startHome(); 227 return true; 228 } 229 } 230 231 final Runnable completeCallback = () -> { 232 RecentsView rv = activityInterface.getVisibleRecentsView(); 233 if (rv != null && (cmd.type == TYPE_KEYBOARD_INPUT || cmd.type == TYPE_HIDE)) { 234 updateRecentsViewFocus(rv); 235 } 236 scheduleNextTask(cmd); 237 }; 238 if (activityInterface.switchToRecentsIfVisible(completeCallback)) { 239 // If successfully switched, wait until animation finishes 240 return false; 241 } 242 243 final T activity = activityInterface.getCreatedActivity(); 244 if (activity != null) { 245 InteractionJankMonitorWrapper.begin( 246 activity.getRootView(), 247 InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH); 248 } 249 250 GestureState gestureState = mService.createGestureState(GestureState.DEFAULT_STATE, 251 GestureState.TrackpadGestureType.NONE); 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())); 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