• 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.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