• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 
17 package com.android.wm.shell.recents;
18 
19 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
20 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
21 import static android.content.pm.PackageManager.FEATURE_PC;
22 import static android.view.Display.INVALID_DISPLAY;
23 
24 import static com.android.wm.shell.Flags.enableShellTopTaskTracking;
25 import static com.android.wm.shell.desktopmode.DesktopWallpaperActivity.isWallpaperTask;
26 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_OBSERVER;
27 
28 import android.Manifest;
29 import android.annotation.RequiresPermission;
30 import android.app.ActivityManager;
31 import android.app.ActivityManager.RecentTaskInfo;
32 import android.app.ActivityManager.RunningTaskInfo;
33 import android.app.ActivityTaskManager;
34 import android.app.IApplicationThread;
35 import android.app.KeyguardManager;
36 import android.app.PendingIntent;
37 import android.app.TaskInfo;
38 import android.content.ComponentName;
39 import android.content.Context;
40 import android.content.Intent;
41 import android.graphics.Color;
42 import android.graphics.Point;
43 import android.os.Bundle;
44 import android.os.RemoteException;
45 import android.util.Slog;
46 import android.util.SparseIntArray;
47 import android.window.DesktopExperienceFlags;
48 import android.window.DesktopModeFlags;
49 import android.window.WindowContainerToken;
50 
51 import androidx.annotation.BinderThread;
52 import androidx.annotation.NonNull;
53 import androidx.annotation.Nullable;
54 import androidx.annotation.VisibleForTesting;
55 
56 import com.android.internal.protolog.ProtoLog;
57 import com.android.wm.shell.common.ExternalInterfaceBinder;
58 import com.android.wm.shell.common.RemoteCallable;
59 import com.android.wm.shell.common.ShellExecutor;
60 import com.android.wm.shell.common.SingleInstanceRemoteListener;
61 import com.android.wm.shell.common.TaskStackListenerCallback;
62 import com.android.wm.shell.common.TaskStackListenerImpl;
63 import com.android.wm.shell.desktopmode.DesktopRepository;
64 import com.android.wm.shell.desktopmode.DesktopUserRepositories;
65 import com.android.wm.shell.protolog.ShellProtoLogGroup;
66 import com.android.wm.shell.shared.GroupedTaskInfo;
67 import com.android.wm.shell.shared.annotations.ExternalThread;
68 import com.android.wm.shell.shared.annotations.ShellMainThread;
69 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
70 import com.android.wm.shell.shared.split.SplitBounds;
71 import com.android.wm.shell.sysui.ShellCommandHandler;
72 import com.android.wm.shell.sysui.ShellController;
73 import com.android.wm.shell.sysui.ShellInit;
74 import com.android.wm.shell.sysui.UserChangeListener;
75 
76 import java.io.PrintWriter;
77 import java.util.ArrayList;
78 import java.util.HashMap;
79 import java.util.HashSet;
80 import java.util.List;
81 import java.util.Map;
82 import java.util.Optional;
83 import java.util.Set;
84 import java.util.concurrent.Executor;
85 import java.util.function.Consumer;
86 import java.util.stream.Collectors;
87 
88 /**
89  * Manages the recent task list from the system, caching it as necessary.
90  */
91 public class RecentTasksController implements TaskStackListenerCallback,
92         RemoteCallable<RecentTasksController>, DesktopRepository.ActiveTasksListener,
93         TaskStackTransitionObserver.TaskStackTransitionObserverListener, UserChangeListener {
94     private static final String TAG = RecentTasksController.class.getSimpleName();
95 
96     // When the multiple desktops feature is disabled, all freeform tasks are lumped together into
97     // a single `GroupedTaskInfo` whose type is `TYPE_DESK`, and its `mDeskId` doesn't matter, so
98     // we pick the below arbitrary value.
99     private static final int INVALID_DESK_ID = -1;
100 
101     private final Context mContext;
102     private final ShellController mShellController;
103     private final ShellCommandHandler mShellCommandHandler;
104     private final Optional<DesktopUserRepositories> mDesktopUserRepositories;
105 
106     private final ShellExecutor mMainExecutor;
107     private final TaskStackListenerImpl mTaskStackListener;
108     private final RecentTasksImpl mImpl = new RecentTasksImpl();
109     private final ActivityTaskManager mActivityTaskManager;
110     private final TaskStackTransitionObserver mTaskStackTransitionObserver;
111     private final RecentsShellCommandHandler mRecentsShellCommandHandler;
112     private RecentsTransitionHandler mTransitionHandler = null;
113     private IRecentTasksListener mListener;
114     private final boolean mPcFeatureEnabled;
115 
116     // Mapping of split task ids, mappings are symmetrical (ie. if t1 is the taskid of a task in a
117     // pair, then mSplitTasks[t1] = t2, and mSplitTasks[t2] = t1)
118     private final SparseIntArray mSplitTasks = new SparseIntArray();
119 
120     private int mUserId;
121     /**
122      * Maps taskId to {@link SplitBounds} for both taskIDs.
123      * Meaning there will be two taskId integers mapping to the same object.
124      * If there's any ordering to the pairing than we can probably just get away with only one
125      * taskID mapping to it, leaving both for consistency with {@link #mSplitTasks} for now.
126      */
127     private final Map<Integer, SplitBounds> mTaskSplitBoundsMap = new HashMap<>();
128 
129     /**
130      * Cached list of the visible tasks, sorted from top most to bottom most.
131      */
132     private final List<RunningTaskInfo> mVisibleTasks = new ArrayList<>();
133     private final Map<Integer, TaskInfo> mVisibleTasksMap = new HashMap<>();
134 
135     // Temporary vars used in `generateList()`
136     private final Map<Integer, TaskInfo> mTmpRemaining = new HashMap<>();
137     private final Map<Integer, Desk> mTmpDesks = new HashMap<>();
138 
139     /**
140      * Creates {@link RecentTasksController}, returns {@code null} if the feature is not
141      * supported.
142      */
143     @Nullable
create( Context context, ShellInit shellInit, ShellController shellController, ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, ActivityTaskManager activityTaskManager, Optional<DesktopUserRepositories> desktopUserRepositories, TaskStackTransitionObserver taskStackTransitionObserver, @ShellMainThread ShellExecutor mainExecutor )144     public static RecentTasksController create(
145             Context context,
146             ShellInit shellInit,
147             ShellController shellController,
148             ShellCommandHandler shellCommandHandler,
149             TaskStackListenerImpl taskStackListener,
150             ActivityTaskManager activityTaskManager,
151             Optional<DesktopUserRepositories> desktopUserRepositories,
152             TaskStackTransitionObserver taskStackTransitionObserver,
153             @ShellMainThread ShellExecutor mainExecutor
154     ) {
155         if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) {
156             return null;
157         }
158         return new RecentTasksController(context, shellInit, shellController, shellCommandHandler,
159                 taskStackListener, activityTaskManager, desktopUserRepositories,
160                 taskStackTransitionObserver, mainExecutor);
161     }
162 
RecentTasksController(Context context, ShellInit shellInit, ShellController shellController, ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, ActivityTaskManager activityTaskManager, Optional<DesktopUserRepositories> desktopUserRepositories, TaskStackTransitionObserver taskStackTransitionObserver, ShellExecutor mainExecutor)163     RecentTasksController(Context context,
164             ShellInit shellInit,
165             ShellController shellController,
166             ShellCommandHandler shellCommandHandler,
167             TaskStackListenerImpl taskStackListener,
168             ActivityTaskManager activityTaskManager,
169             Optional<DesktopUserRepositories> desktopUserRepositories,
170             TaskStackTransitionObserver taskStackTransitionObserver,
171             ShellExecutor mainExecutor) {
172         mContext = context;
173         mShellController = shellController;
174         mShellCommandHandler = shellCommandHandler;
175         mActivityTaskManager = activityTaskManager;
176         mPcFeatureEnabled = mContext.getPackageManager().hasSystemFeature(FEATURE_PC);
177         mTaskStackListener = taskStackListener;
178         mDesktopUserRepositories = desktopUserRepositories;
179         mTaskStackTransitionObserver = taskStackTransitionObserver;
180         mMainExecutor = mainExecutor;
181         mRecentsShellCommandHandler = new RecentsShellCommandHandler(this);
182         shellInit.addInitCallback(this::onInit, this);
183     }
184 
asRecentTasks()185     public RecentTasks asRecentTasks() {
186         return mImpl;
187     }
188 
createExternalInterface()189     private ExternalInterfaceBinder createExternalInterface() {
190         return new IRecentTasksImpl(this);
191     }
192 
193     @RequiresPermission(Manifest.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE)
onInit()194     void onInit() {
195         mShellController.addExternalInterface(IRecentTasks.DESCRIPTOR,
196                 this::createExternalInterface, this);
197         mShellCommandHandler.addDumpCallback(this::dump, this);
198         mShellCommandHandler.addCommandCallback("recents", mRecentsShellCommandHandler, this);
199         mUserId = ActivityManager.getCurrentUser();
200         mDesktopUserRepositories.ifPresent(
201                 desktopUserRepositories ->
202                         desktopUserRepositories.getCurrent().addActiveTaskListener(this));
203         mTaskStackListener.addListener(this);
204         mTaskStackTransitionObserver.addTaskStackTransitionObserverListener(this,
205                 mMainExecutor);
206         mContext.getSystemService(KeyguardManager.class).addKeyguardLockedStateListener(
207                 mMainExecutor, isKeyguardLocked -> notifyRecentTasksChanged());
208     }
209 
setTransitionHandler(RecentsTransitionHandler handler)210     void setTransitionHandler(RecentsTransitionHandler handler) {
211         mTransitionHandler = handler;
212     }
213 
214     /**
215      * Adds a split pair. This call does not validate the taskIds, only that they are not the same.
216      */
addSplitPair(int taskId1, int taskId2, SplitBounds splitBounds)217     public boolean addSplitPair(int taskId1, int taskId2, SplitBounds splitBounds) {
218         if (taskId1 == taskId2) {
219             return false;
220         }
221         if (mSplitTasks.get(taskId1, INVALID_TASK_ID) == taskId2
222                 && mTaskSplitBoundsMap.get(taskId1).equals(splitBounds)) {
223             // If the two tasks are already paired and the bounds are the same, then skip updating
224             return false;
225         }
226         // Remove any previous pairs
227         removeSplitPair(taskId1);
228         removeSplitPair(taskId2);
229         mTaskSplitBoundsMap.remove(taskId1);
230         mTaskSplitBoundsMap.remove(taskId2);
231 
232         mSplitTasks.put(taskId1, taskId2);
233         mSplitTasks.put(taskId2, taskId1);
234         mTaskSplitBoundsMap.put(taskId1, splitBounds);
235         mTaskSplitBoundsMap.put(taskId2, splitBounds);
236         notifyRecentTasksChanged();
237         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Add split pair: %d, %d, %s",
238                 taskId1, taskId2, splitBounds);
239         return true;
240     }
241 
242     /**
243      * Removes a split pair.
244      */
removeSplitPair(int taskId)245     public void removeSplitPair(int taskId) {
246         int pairedTaskId = mSplitTasks.get(taskId, INVALID_TASK_ID);
247         if (pairedTaskId != INVALID_TASK_ID) {
248             mSplitTasks.delete(taskId);
249             mSplitTasks.delete(pairedTaskId);
250             mTaskSplitBoundsMap.remove(taskId);
251             mTaskSplitBoundsMap.remove(pairedTaskId);
252             notifyRecentTasksChanged();
253             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Remove split pair: %d, %d",
254                     taskId, pairedTaskId);
255         }
256     }
257 
258     @Nullable
getSplitBoundsForTaskId(int taskId)259     public SplitBounds getSplitBoundsForTaskId(int taskId) {
260         if (taskId == INVALID_TASK_ID) {
261             return null;
262         }
263 
264         // We could do extra verification of requiring both taskIds of a pair and verifying that
265         // the same split bounds object is returned... but meh. Seems unnecessary.
266         SplitBounds splitBounds = mTaskSplitBoundsMap.get(taskId);
267         if (splitBounds != null) {
268             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
269                     "getSplitBoundsForTaskId: taskId=%d splitBoundsTasks=[%d, %d]", taskId,
270                     splitBounds.leftTopTaskId, splitBounds.rightBottomTaskId);
271         } else {
272             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
273                     "getSplitBoundsForTaskId: expected split bounds for taskId=%d but not found",
274                     taskId);
275         }
276         return splitBounds;
277     }
278 
279     @Override
getContext()280     public Context getContext() {
281         return mContext;
282     }
283 
284     @Override
getRemoteCallExecutor()285     public ShellExecutor getRemoteCallExecutor() {
286         return mMainExecutor;
287     }
288 
289     @Override
onTaskStackChanged()290     public void onTaskStackChanged() {
291         if (!enableShellTopTaskTracking()) {
292             // Skip notifying recent tasks changed whenever task stack changes
293             notifyRecentTasksChanged();
294         }
295     }
296 
297     @Override
onRecentTaskListUpdated()298     public void onRecentTaskListUpdated() {
299         // In some cases immediately after booting, the tasks in the system recent task list may be
300         // loaded, but not in the active task hierarchy in the system.  These tasks are displayed in
301         // overview, but removing them don't result in a onTaskStackChanged() nor a onTaskRemoved()
302         // callback (those are for changes to the active tasks), but the task list is still updated,
303         // so we should also invalidate the change id to ensure we load a new list instead of
304         // reusing a stale list.
305         notifyRecentTasksChanged();
306     }
307 
308     /**
309      * This method only gets notified when a task is removed from recents as a result of another
310      * task being added to recent tasks.
311      */
312     @Override
onRecentTaskRemovedForAddTask(int taskId)313     public void onRecentTaskRemovedForAddTask(int taskId) {
314         mDesktopUserRepositories.ifPresent(
315                 desktopUserRepositories -> desktopUserRepositories.getCurrent().removeTask(
316                         INVALID_DISPLAY, taskId));
317     }
318 
onTaskAdded(RunningTaskInfo taskInfo)319     public void onTaskAdded(RunningTaskInfo taskInfo) {
320         notifyRunningTaskAppeared(taskInfo);
321         if (!enableShellTopTaskTracking()) {
322             notifyRecentTasksChanged();
323         }
324     }
325 
onTaskRemoved(RunningTaskInfo taskInfo)326     public void onTaskRemoved(RunningTaskInfo taskInfo) {
327         // Remove any split pairs associated with this task
328         removeSplitPair(taskInfo.taskId);
329         notifyRunningTaskVanished(taskInfo);
330         if (!enableShellTopTaskTracking()) {
331             // Only notify recent tasks changed if we aren't already notifying the visible tasks
332             notifyRecentTasksChanged();
333         }
334     }
335 
336     /**
337      * Notify listeners that the running infos related to recent tasks was updated.
338      *
339      * This currently includes windowing mode and visibility.
340      */
onTaskRunningInfoChanged(RunningTaskInfo taskInfo)341     public void onTaskRunningInfoChanged(RunningTaskInfo taskInfo) {
342         notifyRecentTasksChanged();
343         notifyRunningTaskChanged(taskInfo);
344     }
345 
346     @Override
onActiveTasksChanged(int displayId)347     public void onActiveTasksChanged(int displayId) {
348         notifyRecentTasksChanged();
349     }
350 
351     @Override
onTaskMovedToFrontThroughTransition(RunningTaskInfo runningTaskInfo)352     public void onTaskMovedToFrontThroughTransition(RunningTaskInfo runningTaskInfo) {
353         notifyTaskMovedToFront(runningTaskInfo);
354     }
355 
356     @Override
onTaskChangedThroughTransition(@onNull ActivityManager.RunningTaskInfo taskInfo)357     public void onTaskChangedThroughTransition(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
358         notifyTaskInfoChanged(taskInfo);
359     }
360 
361     @Override
onVisibleTasksChanged(@onNull List<? extends RunningTaskInfo> visibleTasks)362     public void onVisibleTasksChanged(@NonNull List<? extends RunningTaskInfo> visibleTasks) {
363         mVisibleTasks.clear();
364         mVisibleTasks.addAll(visibleTasks);
365         mVisibleTasksMap.clear();
366         mVisibleTasksMap.putAll(mVisibleTasks.stream().collect(
367                 Collectors.toMap(TaskInfo::getTaskId, task -> task)));
368         // Notify with all the info and not just the running task info
369         notifyVisibleTasksChanged(mVisibleTasks);
370     }
371 
372     @VisibleForTesting
notifyRecentTasksChanged()373     void notifyRecentTasksChanged() {
374         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Notify recent tasks changed");
375         if (mListener == null) {
376             return;
377         }
378         try {
379             mListener.onRecentTasksChanged();
380         } catch (RemoteException e) {
381             Slog.w(TAG, "Failed call notifyRecentTasksChanged", e);
382         }
383     }
384 
385     /**
386      * Notify the running task listener that a task appeared on desktop environment.
387      */
notifyRunningTaskAppeared(RunningTaskInfo taskInfo)388     private void notifyRunningTaskAppeared(RunningTaskInfo taskInfo) {
389         if (mListener == null
390                 || !shouldEnableRunningTasksForDesktopMode()
391                 || taskInfo.realActivity == null
392                 || excludeTaskFromGeneratedList(taskInfo)) {
393             return;
394         }
395         try {
396             mListener.onRunningTaskAppeared(taskInfo);
397         } catch (RemoteException e) {
398             Slog.w(TAG, "Failed call onRunningTaskAppeared", e);
399         }
400     }
401 
402     /**
403      * Notify the running task listener that a task was changed on desktop environment.
404      */
notifyRunningTaskChanged(RunningTaskInfo taskInfo)405     private void notifyRunningTaskChanged(RunningTaskInfo taskInfo) {
406         if (mListener == null
407                 || !shouldEnableRunningTasksForDesktopMode()
408                 || taskInfo.realActivity == null
409                 || excludeTaskFromGeneratedList(taskInfo)) {
410             return;
411         }
412         try {
413             mListener.onRunningTaskChanged(taskInfo);
414         } catch (RemoteException e) {
415             Slog.w(TAG, "Failed call onRunningTaskChanged", e);
416         }
417     }
418 
419     /**
420      * Notify the running task listener that a task was removed on desktop environment.
421      */
notifyRunningTaskVanished(RunningTaskInfo taskInfo)422     private void notifyRunningTaskVanished(RunningTaskInfo taskInfo) {
423         if (mListener == null
424                 || !shouldEnableRunningTasksForDesktopMode()
425                 || taskInfo.realActivity == null
426                 || excludeTaskFromGeneratedList(taskInfo)) {
427             return;
428         }
429         try {
430             mListener.onRunningTaskVanished(taskInfo);
431         } catch (RemoteException e) {
432             Slog.w(TAG, "Failed call onRunningTaskVanished", e);
433         }
434     }
435 
436     /**
437      * Notify the recents task listener that a task moved to front via a transition.
438      */
notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo)439     private void notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
440         if (mListener == null
441                 || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue()
442                 || taskInfo.realActivity == null
443                 || enableShellTopTaskTracking()
444                 || excludeTaskFromGeneratedList(taskInfo)) {
445             return;
446         }
447         try {
448             mListener.onTaskMovedToFront(GroupedTaskInfo.forFullscreenTasks(taskInfo));
449         } catch (RemoteException e) {
450             Slog.w(TAG, "Failed call onTaskMovedToFront", e);
451         }
452     }
453 
454     /**
455      * Notify the recents task listener that a task changed via a transition.
456      */
notifyTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo)457     private void notifyTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
458         if (mListener == null
459                 || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue()
460                 || taskInfo.realActivity == null
461                 || enableShellTopTaskTracking()
462                 || excludeTaskFromGeneratedList(taskInfo)) {
463             return;
464         }
465         try {
466             mListener.onTaskInfoChanged(taskInfo);
467         } catch (RemoteException e) {
468             Slog.w(TAG, "Failed call onTaskInfoChanged", e);
469         }
470     }
471 
472     /**
473      * Notifies that the test of visible tasks have changed.
474      */
notifyVisibleTasksChanged(@onNull List<? extends RunningTaskInfo> visibleTasks)475     private void notifyVisibleTasksChanged(@NonNull List<? extends RunningTaskInfo> visibleTasks) {
476         if (mListener == null
477                 || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue()
478                 || !enableShellTopTaskTracking()) {
479             return;
480         }
481         try {
482             // Compute the visible recent tasks in order, and move the task to the top
483             mListener.onVisibleTasksChanged(generateList(visibleTasks, "visibleTasksChanged")
484                     .toArray(new GroupedTaskInfo[0]));
485         } catch (RemoteException e) {
486             Slog.w(TAG, "Failed call onVisibleTasksChanged", e);
487         }
488     }
489 
shouldEnableRunningTasksForDesktopMode()490     private boolean shouldEnableRunningTasksForDesktopMode() {
491         return mPcFeatureEnabled
492                 || (DesktopModeStatus.canEnterDesktopMode(mContext)
493                 && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS.isTrue());
494     }
495 
496     @VisibleForTesting
registerRecentTasksListener(IRecentTasksListener listener)497     void registerRecentTasksListener(IRecentTasksListener listener) {
498         mListener = listener;
499         if (enableShellTopTaskTracking()) {
500             ProtoLog.v(WM_SHELL_TASK_OBSERVER, "registerRecentTasksListener");
501             // Post a notification for the current set of visible tasks
502             mMainExecutor.executeDelayed(() -> notifyVisibleTasksChanged(mVisibleTasks), 0);
503         }
504     }
505 
506     @VisibleForTesting
unregisterRecentTasksListener()507     void unregisterRecentTasksListener() {
508         mListener = null;
509     }
510 
511     @VisibleForTesting
hasRecentTasksListener()512     boolean hasRecentTasksListener() {
513         return mListener != null;
514     }
515 
516     @VisibleForTesting
getRecentTasks(int maxNum, int flags, int userId)517     ArrayList<GroupedTaskInfo> getRecentTasks(int maxNum, int flags, int userId) {
518         // Note: the returned task list is ordered from the most-recent to least-recent order
519         return generateList(mActivityTaskManager.getRecentTasks(maxNum, flags, userId),
520                 "getRecentTasks");
521     }
522 
523     /**
524      * Returns whether the given task should be excluded from the generated list.
525      */
excludeTaskFromGeneratedList(TaskInfo taskInfo)526     private boolean excludeTaskFromGeneratedList(TaskInfo taskInfo) {
527         if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) {
528             // We don't current send pinned tasks as a part of recent or running tasks
529             return true;
530         }
531         if (isWallpaperTask(taskInfo)) {
532             // Don't add the fullscreen wallpaper task as an entry in grouped tasks
533             return true;
534         }
535         return false;
536     }
537 
538     /**
539      * Represents a desk whose ID is `mDeskId` inside the display with `mDisplayId` and contains
540      * the tasks in `mDeskTasks`. Some of these tasks are minimized and their IDs are contained
541      * in the `mMinimizedDeskTasks` set.
542      */
543     private static class Desk {
544         final int mDeskId;
545         final int mDisplayId;
546         boolean mHasVisibleTasks = false;
547         final ArrayList<TaskInfo> mDeskTasks = new ArrayList<>();
548         final Set<Integer> mMinimizedDeskTasks = new HashSet<>();
549 
Desk(int deskId, int displayId)550         Desk(int deskId, int displayId) {
551             mDeskId = deskId;
552             mDisplayId = displayId;
553         }
554 
addTask(TaskInfo taskInfo, boolean isMinimized, boolean isVisible)555         void addTask(TaskInfo taskInfo, boolean isMinimized, boolean isVisible) {
556             mDeskTasks.add(taskInfo);
557             if (isMinimized) {
558                 mMinimizedDeskTasks.add(taskInfo.taskId);
559             }
560             mHasVisibleTasks |= isVisible;
561         }
562 
hasTasks()563         boolean hasTasks() {
564             return !mDeskTasks.isEmpty();
565         }
566 
createDeskTaskInfo()567         GroupedTaskInfo createDeskTaskInfo() {
568             return GroupedTaskInfo.forDeskTasks(mDeskId, mDisplayId, mDeskTasks,
569                     mMinimizedDeskTasks);
570         }
571     }
572 
573     /**
574      * Clears the `mTmpDesks` map, and re-initializes it with the current state of desks from all
575      * displays, without adding any desk tasks. This is a preparation step so that tasks can be
576      * added to these desks in `generateList()`.
577      *
578      * This is needed since with the `ENABLE_MULTIPLE_DESKTOPS_BACKEND` flag, we want to include
579      * desk even if they're empty (i.e. have no tasks).
580      *
581      * @param multipleDesktopsEnabled whether the multiple desktops backend feature is enabled.
582      */
initializeDesksMap(boolean multipleDesktopsEnabled)583     private void initializeDesksMap(boolean multipleDesktopsEnabled) {
584         mTmpDesks.clear();
585 
586         if (DesktopModeStatus.canEnterDesktopMode(mContext)
587                 && mDesktopUserRepositories.isPresent()) {
588             if (multipleDesktopsEnabled) {
589                 for (var deskId : mDesktopUserRepositories.get().getCurrent().getAllDeskIds()) {
590                     getOrCreateDesk(deskId);
591                 }
592             } else {
593                 // When the multiple desks feature is disabled, we lump all freeform windows in a
594                 // single `GroupedTaskInfo` regardless of their display. The `deskId` in this case
595                 // doesn't matter and can be any arbitrary value.
596                 getOrCreateDesk(/* deskId = */ INVALID_DESK_ID);
597             }
598         }
599     }
600 
601     /**
602      * Returns the `Desk` whose ID is `deskId` from the `mTmpDesks` map if it exists, or it creates
603      * one and adds it to the map and then returns it.
604      */
getOrCreateDesk(int deskId)605     private Desk getOrCreateDesk(int deskId) {
606         var desk = mTmpDesks.get(deskId);
607         if (desk == null) {
608             desk = new Desk(deskId,
609                     mDesktopUserRepositories.get().getCurrent().getDisplayForDesk(deskId));
610             mTmpDesks.put(deskId, desk);
611         }
612         return desk;
613     }
614 
615     /**
616      * Generates a list of GroupedTaskInfos for the given raw list of tasks (either recents or
617      * running tasks).
618      *
619      * The general flow is:
620      *  - Collect the desktop tasks
621      *  - Collect the visible tasks (in order), including the desktop tasks if visible
622      *  - Construct the final list with the visible tasks, followed by the subsequent tasks
623      *      - if enableShellTopTaskTracking() is enabled, the visible tasks will be grouped into
624      *        a single mixed task
625      *      - if the desktop tasks are not visible, they will be appended to the end of the list
626      *
627      * TODO(346588978): Generate list in per-display order
628      *
629      * @param tasks The list of tasks ordered from most recent to least recent
630      */
631     @VisibleForTesting
generateList(@onNull List<T> tasks, String reason)632     <T extends TaskInfo> ArrayList<GroupedTaskInfo> generateList(@NonNull List<T> tasks,
633             String reason) {
634         if (tasks.isEmpty()) {
635             return new ArrayList<>();
636         }
637 
638         if (enableShellTopTaskTracking()) {
639             ProtoLog.v(WM_SHELL_TASK_OBSERVER, "RecentTasksController.generateList(%s)", reason);
640         }
641 
642         final boolean multipleDesktopsEnabled =
643                 DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue();
644         initializeDesksMap(multipleDesktopsEnabled);
645 
646         // When the multiple desktops feature is enabled, we include all desks even if they're
647         // empty.
648         final boolean shouldIncludeEmptyDesktops = multipleDesktopsEnabled;
649 
650         // Make a mapping of task id -> task info for the remaining tasks to be processed, this
651         // mapping is used to keep track of split tasks that may exist later in the task list that
652         // should be ignored because they've already been grouped
653         mTmpRemaining.clear();
654         mTmpRemaining.putAll(tasks.stream().collect(
655                 Collectors.toMap(TaskInfo::getTaskId, task -> task)));
656 
657         // The final grouped tasks
658         ArrayList<GroupedTaskInfo> groupedTasks = new ArrayList<>(tasks.size());
659         ArrayList<GroupedTaskInfo> visibleGroupedTasks = new ArrayList<>();
660 
661         // Phase 1: Extract the desktops and visible fullscreen/split tasks.
662         for (int i = 0; i < tasks.size(); i++) {
663             final TaskInfo taskInfo = tasks.get(i);
664             final int taskId = taskInfo.taskId;
665 
666             if (!mTmpRemaining.containsKey(taskInfo.taskId)) {
667                 // Skip if we've already processed it
668                 continue;
669             }
670 
671             if (excludeTaskFromGeneratedList(taskInfo)) {
672                 // Skip and update the list if we are excluding this task
673                 mTmpRemaining.remove(taskId);
674                 continue;
675             }
676 
677             // Desktop tasks
678             if (DesktopModeStatus.canEnterDesktopMode(mContext) &&
679                     mDesktopUserRepositories.isPresent()
680                     && mDesktopUserRepositories.get().getCurrent().isActiveTask(taskId)) {
681                 // If task has their app bounds set to null which happens after reboot, set the
682                 // app bounds to persisted lastFullscreenBounds. Also set the position in parent
683                 // to the top left of the bounds.
684                 if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()
685                         && taskInfo.configuration.windowConfiguration.getAppBounds() == null) {
686                     taskInfo.configuration.windowConfiguration.setAppBounds(
687                             taskInfo.lastNonFullscreenBounds);
688                     taskInfo.positionInParent = new Point(taskInfo.lastNonFullscreenBounds.left,
689                             taskInfo.lastNonFullscreenBounds.top);
690                 }
691                 // Lump all freeform tasks together as if they were all in a single desk whose ID is
692                 // `INVALID_DESK_ID` when the multiple desktops feature is disabled.
693                 final int deskId = multipleDesktopsEnabled
694                         ? mDesktopUserRepositories.get().getCurrent().getDeskIdForTask(taskId)
695                         : INVALID_DESK_ID;
696                 final Desk desk = getOrCreateDesk(deskId);
697                 desk.addTask(taskInfo,
698                         mDesktopUserRepositories.get().getCurrent().isMinimizedTask(taskId),
699                         mVisibleTasksMap.containsKey(taskId));
700                 mTmpRemaining.remove(taskId);
701                 continue;
702             }
703 
704             if (enableShellTopTaskTracking()) {
705                 // Visible tasks
706                 if (mVisibleTasksMap.containsKey(taskId)) {
707                     // Split tasks
708                     if (extractAndAddSplitGroupedTask(taskInfo, mTmpRemaining,
709                             visibleGroupedTasks)) {
710                         continue;
711                     }
712 
713                     // Fullscreen tasks
714                     visibleGroupedTasks.add(GroupedTaskInfo.forFullscreenTasks(taskInfo));
715                     mTmpRemaining.remove(taskId);
716                 }
717             } else {
718                 // Split tasks
719                 if (extractAndAddSplitGroupedTask(taskInfo, mTmpRemaining, groupedTasks)) {
720                     continue;
721                 }
722 
723                 // Fullscreen tasks
724                 groupedTasks.add(GroupedTaskInfo.forFullscreenTasks(taskInfo));
725             }
726         }
727 
728         if (enableShellTopTaskTracking()) {
729             // Phase 2: If there were desktop tasks and they are visible, add them to the visible
730             //          list as well (the actual order doesn't matter for Overview)
731             for (var desk : mTmpDesks.values()) {
732                 if (desk.mHasVisibleTasks) {
733                     visibleGroupedTasks.add(desk.createDeskTaskInfo());
734                 }
735             }
736 
737             if (!visibleGroupedTasks.isEmpty()) {
738                 // Phase 3: Combine the visible tasks into a single mixed grouped task, only if
739                 //          there are > 1 tasks to group, and add them to the final list
740                 if (visibleGroupedTasks.size() > 1) {
741                     groupedTasks.add(GroupedTaskInfo.forMixed(visibleGroupedTasks));
742                 } else {
743                     groupedTasks.addAll(visibleGroupedTasks);
744                 }
745             }
746             dumpGroupedTasks(groupedTasks, "Phase 3");
747 
748             // Phase 4: For the remaining non-visible split and fullscreen tasks, add grouped tasks
749             //          in order to the final list
750             for (int i = 0; i < tasks.size(); i++) {
751                 final TaskInfo taskInfo = tasks.get(i);
752                 if (!mTmpRemaining.containsKey(taskInfo.taskId)) {
753                     // Skip if we've already processed it
754                     continue;
755                 }
756 
757                 // Split tasks
758                 if (extractAndAddSplitGroupedTask(taskInfo, mTmpRemaining, groupedTasks)) {
759                     continue;
760                 }
761 
762                 // Fullscreen tasks
763                 groupedTasks.add(GroupedTaskInfo.forFullscreenTasks(taskInfo));
764             }
765             dumpGroupedTasks(groupedTasks, "Phase 4");
766 
767             // Phase 5: If there were desktop tasks and they are not visible (ie. weren't added
768             //          above), add them to the end of the final list (the actual order doesn't
769             //          matter for Overview)
770             for (var desk : mTmpDesks.values()) {
771                 if (!desk.mHasVisibleTasks && (desk.hasTasks() || shouldIncludeEmptyDesktops)) {
772                     groupedTasks.add(desk.createDeskTaskInfo());
773                 }
774             }
775             dumpGroupedTasks(groupedTasks, "Phase 5");
776         } else {
777             // Add the desktop tasks at the end of the list
778             for (var desk : mTmpDesks.values()) {
779                 if (desk.hasTasks() || shouldIncludeEmptyDesktops) {
780                     groupedTasks.add(desk.createDeskTaskInfo());
781                 }
782             }
783         }
784 
785         return groupedTasks;
786     }
787 
788     /**
789      * Only to be called from `generateList()`. If the given {@param taskInfo} has a paired task,
790      * then a split grouped task with the pair is added to {@param tasksOut}.
791      *
792      * @return whether a split task was extracted and added to the given list
793      */
extractAndAddSplitGroupedTask(@onNull TaskInfo taskInfo, @NonNull Map<Integer, TaskInfo> remainingTasks, @NonNull ArrayList<GroupedTaskInfo> tasksOut)794     private boolean extractAndAddSplitGroupedTask(@NonNull TaskInfo taskInfo,
795             @NonNull Map<Integer, TaskInfo> remainingTasks,
796             @NonNull ArrayList<GroupedTaskInfo> tasksOut) {
797         final int pairedTaskId = mSplitTasks.get(taskInfo.taskId, INVALID_TASK_ID);
798         if (pairedTaskId == INVALID_TASK_ID || !remainingTasks.containsKey(pairedTaskId)) {
799             return false;
800         }
801 
802         // Add both this task and its pair to the list, and mark the paired task to be
803         // skipped when it is encountered in the list
804         final TaskInfo pairedTaskInfo = remainingTasks.get(pairedTaskId);
805         remainingTasks.remove(taskInfo.taskId);
806         remainingTasks.remove(pairedTaskId);
807         tasksOut.add(GroupedTaskInfo.forSplitTasks(taskInfo, pairedTaskInfo,
808                 mTaskSplitBoundsMap.get(pairedTaskId)));
809         return true;
810     }
811 
812     /** Dumps the set of tasks to protolog */
dumpGroupedTasks(List<GroupedTaskInfo> groupedTasks, String reason)813     private void dumpGroupedTasks(List<GroupedTaskInfo> groupedTasks, String reason) {
814         if (!WM_SHELL_TASK_OBSERVER.isEnabled()) {
815             return;
816         }
817         ProtoLog.v(WM_SHELL_TASK_OBSERVER, "    Tasks (%s):", reason);
818         for (GroupedTaskInfo task : groupedTasks) {
819             ProtoLog.v(WM_SHELL_TASK_OBSERVER, "        %s", task);
820         }
821     }
822 
823     /**
824      * Returns the top running leaf task ignoring {@param ignoreTaskToken} if it is specified.
825      * NOTE: This path currently makes assumptions that ignoreTaskToken is for the top task.
826      */
827     @Nullable
getTopRunningTask( @ullable WindowContainerToken ignoreTaskToken)828     public RunningTaskInfo getTopRunningTask(
829             @Nullable WindowContainerToken ignoreTaskToken) {
830         final List<RunningTaskInfo> tasks = enableShellTopTaskTracking()
831                 ? mVisibleTasks
832                 : mActivityTaskManager.getTasks(2, false /* filterOnlyVisibleRecents */);
833         for (int i = 0; i < tasks.size(); i++) {
834             final RunningTaskInfo task = tasks.get(i);
835             if (task.token.equals(ignoreTaskToken)) {
836                 continue;
837             }
838             return task;
839         }
840         return null;
841     }
842 
843     /**
844      * Find the background task that match the given component.  Ignores tasks match
845      * {@param ignoreTaskToken} if it is non-null.
846      */
847     @Nullable
findTaskInBackground(ComponentName componentName, int userId, @Nullable WindowContainerToken ignoreTaskToken)848     public RecentTaskInfo findTaskInBackground(ComponentName componentName,
849             int userId, @Nullable WindowContainerToken ignoreTaskToken) {
850         if (componentName == null) {
851             return null;
852         }
853         List<RecentTaskInfo> tasks = mActivityTaskManager.getRecentTasks(
854                 Integer.MAX_VALUE, ActivityManager.RECENT_IGNORE_UNAVAILABLE,
855                 ActivityManager.getCurrentUser());
856         for (int i = 0; i < tasks.size(); i++) {
857             final RecentTaskInfo task = tasks.get(i);
858             if (task.isVisible) {
859                 continue;
860             }
861             if (task.token.equals(ignoreTaskToken)) {
862                 continue;
863             }
864             if (componentName.equals(task.baseIntent.getComponent()) && userId == task.userId) {
865                 return task;
866             }
867         }
868         return null;
869     }
870 
871     /**
872      * Find the background task (in the recent tasks list) that matches the given taskId.
873      */
874     @Nullable
findTaskInBackground(int taskId)875     public RecentTaskInfo findTaskInBackground(int taskId) {
876         List<RecentTaskInfo> tasks = mActivityTaskManager.getRecentTasks(
877                 Integer.MAX_VALUE, ActivityManager.RECENT_IGNORE_UNAVAILABLE,
878                 ActivityManager.getCurrentUser());
879         for (int i = 0; i < tasks.size(); i++) {
880             final RecentTaskInfo task = tasks.get(i);
881             if (task.isVisible) {
882                 continue;
883             }
884             if (taskId == task.taskId) {
885                 return task;
886             }
887         }
888         return null;
889     }
890 
891     /**
892      * Remove the background task that match the given taskId. This will remove the task regardless
893      * of whether it's active or recent.
894      */
removeBackgroundTask(int taskId)895     public boolean removeBackgroundTask(int taskId) {
896         return mActivityTaskManager.removeTask(taskId);
897     }
898 
899     /** Removes all recent tasks that are visible. */
removeAllVisibleRecentTasks()900     public void removeAllVisibleRecentTasks() throws RemoteException {
901         ActivityTaskManager.getService().removeAllVisibleRecentTasks();
902     }
903 
dump(@onNull PrintWriter pw, String prefix)904     public void dump(@NonNull PrintWriter pw, String prefix) {
905         final String innerPrefix = prefix + "  ";
906         pw.println(prefix + TAG);
907         pw.println(prefix + " mListener=" + mListener);
908         pw.println(prefix + "Tasks:");
909         ArrayList<GroupedTaskInfo> recentTasks = getRecentTasks(Integer.MAX_VALUE,
910                 ActivityManager.RECENT_IGNORE_UNAVAILABLE, ActivityManager.getCurrentUser());
911         for (int i = 0; i < recentTasks.size(); i++) {
912             pw.println(innerPrefix + recentTasks.get(i));
913         }
914     }
915 
916     /**
917      * The interface for calls from outside the Shell, within the host process.
918      */
919     @ExternalThread
920     private class RecentTasksImpl implements RecentTasks {
921         @Override
getRecentTasks(int maxNum, int flags, int userId, Executor executor, Consumer<List<GroupedTaskInfo>> callback)922         public void getRecentTasks(int maxNum, int flags, int userId, Executor executor,
923                 Consumer<List<GroupedTaskInfo>> callback) {
924             mMainExecutor.execute(() -> {
925                 List<GroupedTaskInfo> tasks =
926                         RecentTasksController.this.getRecentTasks(maxNum, flags, userId);
927                 executor.execute(() -> callback.accept(tasks));
928             });
929         }
930 
931         @Override
addAnimationStateListener(Executor executor, Consumer<Boolean> listener)932         public void addAnimationStateListener(Executor executor, Consumer<Boolean> listener) {
933             mMainExecutor.execute(() -> {
934                 if (mTransitionHandler == null) {
935                     return;
936                 }
937                 mTransitionHandler.addTransitionStateListener(new RecentsTransitionStateListener() {
938                     @Override
939                     public void onTransitionStateChanged(@RecentsTransitionState int state) {
940                         executor.execute(() -> {
941                             listener.accept(RecentsTransitionStateListener.isAnimating(state));
942                         });
943                     }
944                 });
945             });
946         }
947 
948         @Override
setTransitionBackgroundColor(@ullable Color color)949         public void setTransitionBackgroundColor(@Nullable Color color) {
950             mMainExecutor.execute(() -> {
951                 if (mTransitionHandler == null) {
952                     return;
953                 }
954                 mTransitionHandler.setTransitionBackgroundColor(color);
955             });
956         }
957     }
958 
959     @Override
onUserChanged(int newUserId, @NonNull Context userContext)960     public void onUserChanged(int newUserId, @NonNull Context userContext) {
961         if (mDesktopUserRepositories.isEmpty()) return;
962 
963         DesktopRepository previousUserRepository =
964                 mDesktopUserRepositories.get().getProfile(mUserId);
965         mUserId = newUserId;
966         DesktopRepository currentUserRepository =
967                 mDesktopUserRepositories.get().getProfile(newUserId);
968 
969         // No-op if both profile ids map to the same user.
970         if (previousUserRepository.getUserId() == currentUserRepository.getUserId()) return;
971         previousUserRepository.removeActiveTasksListener(this);
972         currentUserRepository.addActiveTaskListener(this);
973     }
974 
975     /**
976      * The interface for calls from outside the host process.
977      */
978     @BinderThread
979     private static class IRecentTasksImpl extends IRecentTasks.Stub
980             implements ExternalInterfaceBinder {
981         private RecentTasksController mController;
982         private final SingleInstanceRemoteListener<RecentTasksController,
983                 IRecentTasksListener> mListener;
984         private final IRecentTasksListener mRecentTasksListener = new IRecentTasksListener.Stub() {
985             @Override
986             public void onRecentTasksChanged() throws RemoteException {
987                 mListener.call(l -> l.onRecentTasksChanged());
988             }
989 
990             @Override
991             public void onRunningTaskAppeared(RunningTaskInfo taskInfo) {
992                 mListener.call(l -> l.onRunningTaskAppeared(taskInfo));
993             }
994 
995             @Override
996             public void onRunningTaskVanished(RunningTaskInfo taskInfo) {
997                 mListener.call(l -> l.onRunningTaskVanished(taskInfo));
998             }
999 
1000             @Override
1001             public void onRunningTaskChanged(RunningTaskInfo taskInfo) {
1002                 mListener.call(l -> l.onRunningTaskChanged(taskInfo));
1003             }
1004 
1005             @Override
1006             public void onTaskMovedToFront(GroupedTaskInfo taskToFront) {
1007                 mListener.call(l -> l.onTaskMovedToFront(taskToFront));
1008             }
1009 
1010             @Override
1011             public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
1012                 mListener.call(l -> l.onTaskInfoChanged(taskInfo));
1013             }
1014 
1015             @Override
1016             public void onVisibleTasksChanged(GroupedTaskInfo[] visibleTasks) {
1017                 mListener.call(l -> l.onVisibleTasksChanged(visibleTasks));
1018             }
1019         };
1020 
IRecentTasksImpl(RecentTasksController controller)1021         public IRecentTasksImpl(RecentTasksController controller) {
1022             mController = controller;
1023             mListener = new SingleInstanceRemoteListener<>(controller,
1024                     c -> c.registerRecentTasksListener(mRecentTasksListener),
1025                     c -> c.unregisterRecentTasksListener());
1026         }
1027 
1028         /**
1029          * Invalidates this instance, preventing future calls from updating the controller.
1030          */
1031         @Override
invalidate()1032         public void invalidate() {
1033             mController = null;
1034             // Unregister the listener to ensure any registered binder death recipients are unlinked
1035             mListener.unregister();
1036         }
1037 
1038         @Override
registerRecentTasksListener(IRecentTasksListener listener)1039         public void registerRecentTasksListener(IRecentTasksListener listener)
1040                 throws RemoteException {
1041             executeRemoteCallWithTaskPermission(mController, "registerRecentTasksListener",
1042                     (controller) -> mListener.register(listener));
1043         }
1044 
1045         @Override
unregisterRecentTasksListener(IRecentTasksListener listener)1046         public void unregisterRecentTasksListener(IRecentTasksListener listener)
1047                 throws RemoteException {
1048             executeRemoteCallWithTaskPermission(mController, "unregisterRecentTasksListener",
1049                     (controller) -> mListener.unregister());
1050         }
1051 
1052         @Override
getRecentTasks(int maxNum, int flags, int userId)1053         public GroupedTaskInfo[] getRecentTasks(int maxNum, int flags, int userId)
1054                 throws RemoteException {
1055             if (mController == null) {
1056                 // The controller is already invalidated -- just return an empty task list for now
1057                 return new GroupedTaskInfo[0];
1058             }
1059 
1060             final GroupedTaskInfo[][] out = new GroupedTaskInfo[][]{null};
1061             executeRemoteCallWithTaskPermission(mController, "getRecentTasks",
1062                     (controller) -> {
1063                         List<GroupedTaskInfo> tasks = controller.getRecentTasks(
1064                                 maxNum, flags, userId);
1065                         out[0] = tasks.toArray(new GroupedTaskInfo[0]);
1066                     },
1067                     true /* blocking */);
1068             return out[0];
1069         }
1070 
1071         @Override
getRunningTasks(int maxNum)1072         public RunningTaskInfo[] getRunningTasks(int maxNum) {
1073             final RunningTaskInfo[][] tasks =
1074                     new RunningTaskInfo[][]{null};
1075             executeRemoteCallWithTaskPermission(mController, "getRunningTasks",
1076                     (controller) -> tasks[0] = ActivityTaskManager.getInstance().getTasks(maxNum)
1077                             .toArray(new RunningTaskInfo[0]),
1078                     true /* blocking */);
1079             return tasks[0];
1080         }
1081 
1082         @Override
startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options, IApplicationThread appThread, IRecentsAnimationRunner listener)1083         public void startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options,
1084                 IApplicationThread appThread, IRecentsAnimationRunner listener) {
1085             if (mController.mTransitionHandler == null) {
1086                 Slog.e(TAG, "Used shell-transitions startRecentsTransition without"
1087                         + " shell-transitions");
1088                 return;
1089             }
1090             executeRemoteCallWithTaskPermission(mController, "startRecentsTransition",
1091                     (controller) -> controller.mTransitionHandler.startRecentsTransition(
1092                             intent, fillIn, options, appThread, listener));
1093         }
1094     }
1095 }
1096