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