• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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