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.content.pm.PackageManager.FEATURE_PC;
21 
22 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
23 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS;
24 
25 import android.app.ActivityManager;
26 import android.app.ActivityTaskManager;
27 import android.app.IApplicationThread;
28 import android.app.PendingIntent;
29 import android.app.TaskInfo;
30 import android.content.ComponentName;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.os.Bundle;
34 import android.os.RemoteException;
35 import android.util.Slog;
36 import android.util.SparseArray;
37 import android.util.SparseIntArray;
38 import android.view.IRecentsAnimationRunner;
39 
40 import androidx.annotation.BinderThread;
41 import androidx.annotation.NonNull;
42 import androidx.annotation.Nullable;
43 import androidx.annotation.VisibleForTesting;
44 
45 import com.android.internal.protolog.common.ProtoLog;
46 import com.android.wm.shell.common.ExternalInterfaceBinder;
47 import com.android.wm.shell.common.RemoteCallable;
48 import com.android.wm.shell.common.ShellExecutor;
49 import com.android.wm.shell.common.SingleInstanceRemoteListener;
50 import com.android.wm.shell.common.TaskStackListenerCallback;
51 import com.android.wm.shell.common.TaskStackListenerImpl;
52 import com.android.wm.shell.common.annotations.ExternalThread;
53 import com.android.wm.shell.common.annotations.ShellMainThread;
54 import com.android.wm.shell.desktopmode.DesktopModeStatus;
55 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
56 import com.android.wm.shell.protolog.ShellProtoLogGroup;
57 import com.android.wm.shell.sysui.ShellCommandHandler;
58 import com.android.wm.shell.sysui.ShellController;
59 import com.android.wm.shell.sysui.ShellInit;
60 import com.android.wm.shell.util.GroupedRecentTaskInfo;
61 import com.android.wm.shell.util.SplitBounds;
62 
63 import java.io.PrintWriter;
64 import java.util.ArrayList;
65 import java.util.HashMap;
66 import java.util.List;
67 import java.util.Map;
68 import java.util.Optional;
69 import java.util.concurrent.Executor;
70 import java.util.function.Consumer;
71 
72 /**
73  * Manages the recent task list from the system, caching it as necessary.
74  */
75 public class RecentTasksController implements TaskStackListenerCallback,
76         RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener {
77     private static final String TAG = RecentTasksController.class.getSimpleName();
78 
79     private final Context mContext;
80     private final ShellController mShellController;
81     private final ShellCommandHandler mShellCommandHandler;
82     private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository;
83     private final ShellExecutor mMainExecutor;
84     private final TaskStackListenerImpl mTaskStackListener;
85     private final RecentTasksImpl mImpl = new RecentTasksImpl();
86     private final ActivityTaskManager mActivityTaskManager;
87     private RecentsTransitionHandler mTransitionHandler = null;
88     private IRecentTasksListener mListener;
89     private final boolean mIsDesktopMode;
90 
91     // Mapping of split task ids, mappings are symmetrical (ie. if t1 is the taskid of a task in a
92     // pair, then mSplitTasks[t1] = t2, and mSplitTasks[t2] = t1)
93     private final SparseIntArray mSplitTasks = new SparseIntArray();
94     /**
95      * Maps taskId to {@link SplitBounds} for both taskIDs.
96      * Meaning there will be two taskId integers mapping to the same object.
97      * If there's any ordering to the pairing than we can probably just get away with only one
98      * taskID mapping to it, leaving both for consistency with {@link #mSplitTasks} for now.
99      */
100     private final Map<Integer, SplitBounds> mTaskSplitBoundsMap = new HashMap<>();
101 
102     /**
103      * Creates {@link RecentTasksController}, returns {@code null} if the feature is not
104      * supported.
105      */
106     @Nullable
create( Context context, ShellInit shellInit, ShellController shellController, ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, ActivityTaskManager activityTaskManager, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, @ShellMainThread ShellExecutor mainExecutor )107     public static RecentTasksController create(
108             Context context,
109             ShellInit shellInit,
110             ShellController shellController,
111             ShellCommandHandler shellCommandHandler,
112             TaskStackListenerImpl taskStackListener,
113             ActivityTaskManager activityTaskManager,
114             Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
115             @ShellMainThread ShellExecutor mainExecutor
116     ) {
117         if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) {
118             return null;
119         }
120         return new RecentTasksController(context, shellInit, shellController, shellCommandHandler,
121                 taskStackListener, activityTaskManager, desktopModeTaskRepository, mainExecutor);
122     }
123 
RecentTasksController(Context context, ShellInit shellInit, ShellController shellController, ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, ActivityTaskManager activityTaskManager, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, ShellExecutor mainExecutor)124     RecentTasksController(Context context,
125             ShellInit shellInit,
126             ShellController shellController,
127             ShellCommandHandler shellCommandHandler,
128             TaskStackListenerImpl taskStackListener,
129             ActivityTaskManager activityTaskManager,
130             Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
131             ShellExecutor mainExecutor) {
132         mContext = context;
133         mShellController = shellController;
134         mShellCommandHandler = shellCommandHandler;
135         mActivityTaskManager = activityTaskManager;
136         mIsDesktopMode = mContext.getPackageManager().hasSystemFeature(FEATURE_PC);
137         mTaskStackListener = taskStackListener;
138         mDesktopModeTaskRepository = desktopModeTaskRepository;
139         mMainExecutor = mainExecutor;
140         shellInit.addInitCallback(this::onInit, this);
141     }
142 
asRecentTasks()143     public RecentTasks asRecentTasks() {
144         return mImpl;
145     }
146 
createExternalInterface()147     private ExternalInterfaceBinder createExternalInterface() {
148         return new IRecentTasksImpl(this);
149     }
150 
onInit()151     private void onInit() {
152         mShellController.addExternalInterface(KEY_EXTRA_SHELL_RECENT_TASKS,
153                 this::createExternalInterface, this);
154         mShellCommandHandler.addDumpCallback(this::dump, this);
155         mTaskStackListener.addListener(this);
156         mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTaskListener(this));
157     }
158 
setTransitionHandler(RecentsTransitionHandler handler)159     void setTransitionHandler(RecentsTransitionHandler handler) {
160         mTransitionHandler = handler;
161     }
162 
163     /**
164      * Adds a split pair. This call does not validate the taskIds, only that they are not the same.
165      */
addSplitPair(int taskId1, int taskId2, SplitBounds splitBounds)166     public void addSplitPair(int taskId1, int taskId2, SplitBounds splitBounds) {
167         if (taskId1 == taskId2) {
168             return;
169         }
170         if (mSplitTasks.get(taskId1, INVALID_TASK_ID) == taskId2
171                 && mTaskSplitBoundsMap.get(taskId1).equals(splitBounds)) {
172             // If the two tasks are already paired and the bounds are the same, then skip updating
173             return;
174         }
175         // Remove any previous pairs
176         removeSplitPair(taskId1);
177         removeSplitPair(taskId2);
178         mTaskSplitBoundsMap.remove(taskId1);
179         mTaskSplitBoundsMap.remove(taskId2);
180 
181         mSplitTasks.put(taskId1, taskId2);
182         mSplitTasks.put(taskId2, taskId1);
183         mTaskSplitBoundsMap.put(taskId1, splitBounds);
184         mTaskSplitBoundsMap.put(taskId2, splitBounds);
185         notifyRecentTasksChanged();
186         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Add split pair: %d, %d, %s",
187                 taskId1, taskId2, splitBounds);
188     }
189 
190     /**
191      * Removes a split pair.
192      */
removeSplitPair(int taskId)193     public void removeSplitPair(int taskId) {
194         int pairedTaskId = mSplitTasks.get(taskId, INVALID_TASK_ID);
195         if (pairedTaskId != INVALID_TASK_ID) {
196             mSplitTasks.delete(taskId);
197             mSplitTasks.delete(pairedTaskId);
198             mTaskSplitBoundsMap.remove(taskId);
199             mTaskSplitBoundsMap.remove(pairedTaskId);
200             notifyRecentTasksChanged();
201             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Remove split pair: %d, %d",
202                     taskId, pairedTaskId);
203         }
204     }
205 
206     @Override
getContext()207     public Context getContext() {
208         return mContext;
209     }
210 
211     @Override
getRemoteCallExecutor()212     public ShellExecutor getRemoteCallExecutor() {
213         return mMainExecutor;
214     }
215 
216     @Override
onTaskStackChanged()217     public void onTaskStackChanged() {
218         notifyRecentTasksChanged();
219     }
220 
221     @Override
onRecentTaskListUpdated()222     public void onRecentTaskListUpdated() {
223         // In some cases immediately after booting, the tasks in the system recent task list may be
224         // loaded, but not in the active task hierarchy in the system.  These tasks are displayed in
225         // overview, but removing them don't result in a onTaskStackChanged() nor a onTaskRemoved()
226         // callback (those are for changes to the active tasks), but the task list is still updated,
227         // so we should also invalidate the change id to ensure we load a new list instead of
228         // reusing a stale list.
229         notifyRecentTasksChanged();
230     }
231 
onTaskAdded(ActivityManager.RunningTaskInfo taskInfo)232     public void onTaskAdded(ActivityManager.RunningTaskInfo taskInfo) {
233         notifyRunningTaskAppeared(taskInfo);
234     }
235 
onTaskRemoved(ActivityManager.RunningTaskInfo taskInfo)236     public void onTaskRemoved(ActivityManager.RunningTaskInfo taskInfo) {
237         // Remove any split pairs associated with this task
238         removeSplitPair(taskInfo.taskId);
239         notifyRecentTasksChanged();
240         notifyRunningTaskVanished(taskInfo);
241     }
242 
onTaskWindowingModeChanged(TaskInfo taskInfo)243     public void onTaskWindowingModeChanged(TaskInfo taskInfo) {
244         notifyRecentTasksChanged();
245     }
246 
247     @Override
onActiveTasksChanged(int displayId)248     public void onActiveTasksChanged(int displayId) {
249         notifyRecentTasksChanged();
250     }
251 
252     @VisibleForTesting
notifyRecentTasksChanged()253     void notifyRecentTasksChanged() {
254         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Notify recent tasks changed");
255         if (mListener == null) {
256             return;
257         }
258         try {
259             mListener.onRecentTasksChanged();
260         } catch (RemoteException e) {
261             Slog.w(TAG, "Failed call notifyRecentTasksChanged", e);
262         }
263     }
264 
265     /**
266      * Notify the running task listener that a task appeared on desktop environment.
267      */
notifyRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo)268     private void notifyRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) {
269         if (mListener == null || !mIsDesktopMode || taskInfo.realActivity == null) {
270             return;
271         }
272         try {
273             mListener.onRunningTaskAppeared(taskInfo);
274         } catch (RemoteException e) {
275             Slog.w(TAG, "Failed call onRunningTaskAppeared", e);
276         }
277     }
278 
279     /**
280      * Notify the running task listener that a task was removed on desktop environment.
281      */
notifyRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo)282     private void notifyRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
283         if (mListener == null || !mIsDesktopMode || taskInfo.realActivity == null) {
284             return;
285         }
286         try {
287             mListener.onRunningTaskVanished(taskInfo);
288         } catch (RemoteException e) {
289             Slog.w(TAG, "Failed call onRunningTaskVanished", e);
290         }
291     }
292 
293     @VisibleForTesting
registerRecentTasksListener(IRecentTasksListener listener)294     void registerRecentTasksListener(IRecentTasksListener listener) {
295         mListener = listener;
296     }
297 
298     @VisibleForTesting
unregisterRecentTasksListener()299     void unregisterRecentTasksListener() {
300         mListener = null;
301     }
302 
303     @VisibleForTesting
hasRecentTasksListener()304     boolean hasRecentTasksListener() {
305         return mListener != null;
306     }
307 
308     @VisibleForTesting
getRecentTasks(int maxNum, int flags, int userId)309     ArrayList<GroupedRecentTaskInfo> getRecentTasks(int maxNum, int flags, int userId) {
310         // Note: the returned task list is from the most-recent to least-recent order
311         final List<ActivityManager.RecentTaskInfo> rawList = mActivityTaskManager.getRecentTasks(
312                 maxNum, flags, userId);
313 
314         // Make a mapping of task id -> task info
315         final SparseArray<ActivityManager.RecentTaskInfo> rawMapping = new SparseArray<>();
316         for (int i = 0; i < rawList.size(); i++) {
317             final ActivityManager.RecentTaskInfo taskInfo = rawList.get(i);
318             rawMapping.put(taskInfo.taskId, taskInfo);
319         }
320 
321         ArrayList<ActivityManager.RecentTaskInfo> freeformTasks = new ArrayList<>();
322 
323         // Pull out the pairs as we iterate back in the list
324         ArrayList<GroupedRecentTaskInfo> recentTasks = new ArrayList<>();
325         for (int i = 0; i < rawList.size(); i++) {
326             final ActivityManager.RecentTaskInfo taskInfo = rawList.get(i);
327             if (!rawMapping.contains(taskInfo.taskId)) {
328                 // If it's not in the mapping, then it was already paired with another task
329                 continue;
330             }
331 
332             if (DesktopModeStatus.isProto2Enabled() && mDesktopModeTaskRepository.isPresent()
333                     && mDesktopModeTaskRepository.get().isActiveTask(taskInfo.taskId)) {
334                 // Freeform tasks will be added as a separate entry
335                 freeformTasks.add(taskInfo);
336                 continue;
337             }
338 
339             final int pairedTaskId = mSplitTasks.get(taskInfo.taskId);
340             if (pairedTaskId != INVALID_TASK_ID && rawMapping.contains(
341                     pairedTaskId)) {
342                 final ActivityManager.RecentTaskInfo pairedTaskInfo = rawMapping.get(pairedTaskId);
343                 rawMapping.remove(pairedTaskId);
344                 recentTasks.add(GroupedRecentTaskInfo.forSplitTasks(taskInfo, pairedTaskInfo,
345                         mTaskSplitBoundsMap.get(pairedTaskId)));
346             } else {
347                 recentTasks.add(GroupedRecentTaskInfo.forSingleTask(taskInfo));
348             }
349         }
350 
351         // Add a special entry for freeform tasks
352         if (!freeformTasks.isEmpty()) {
353             recentTasks.add(0, GroupedRecentTaskInfo.forFreeformTasks(
354                     freeformTasks.toArray(new ActivityManager.RecentTaskInfo[0])));
355         }
356 
357         return recentTasks;
358     }
359 
360     /**
361      * Returns the top running leaf task.
362      */
363     @Nullable
getTopRunningTask()364     public ActivityManager.RunningTaskInfo getTopRunningTask() {
365         List<ActivityManager.RunningTaskInfo> tasks = mActivityTaskManager.getTasks(1,
366                 false /* filterOnlyVisibleRecents */);
367         return tasks.isEmpty() ? null : tasks.get(0);
368     }
369 
370     /**
371      * Find the background task that match the given component.
372      */
373     @Nullable
findTaskInBackground(ComponentName componentName, int userId)374     public ActivityManager.RecentTaskInfo findTaskInBackground(ComponentName componentName,
375             int userId) {
376         if (componentName == null) {
377             return null;
378         }
379         List<ActivityManager.RecentTaskInfo> tasks = mActivityTaskManager.getRecentTasks(
380                 Integer.MAX_VALUE, ActivityManager.RECENT_IGNORE_UNAVAILABLE,
381                 ActivityManager.getCurrentUser());
382         for (int i = 0; i < tasks.size(); i++) {
383             final ActivityManager.RecentTaskInfo task = tasks.get(i);
384             if (task.isVisible) {
385                 continue;
386             }
387             if (componentName.equals(task.baseIntent.getComponent()) && userId == task.userId) {
388                 return task;
389             }
390         }
391         return null;
392     }
393 
dump(@onNull PrintWriter pw, String prefix)394     public void dump(@NonNull PrintWriter pw, String prefix) {
395         final String innerPrefix = prefix + "  ";
396         pw.println(prefix + TAG);
397         pw.println(prefix + " mListener=" + mListener);
398         pw.println(prefix + "Tasks:");
399         ArrayList<GroupedRecentTaskInfo> recentTasks = getRecentTasks(Integer.MAX_VALUE,
400                 ActivityManager.RECENT_IGNORE_UNAVAILABLE, ActivityManager.getCurrentUser());
401         for (int i = 0; i < recentTasks.size(); i++) {
402             pw.println(innerPrefix + recentTasks.get(i));
403         }
404     }
405 
406     /**
407      * The interface for calls from outside the Shell, within the host process.
408      */
409     @ExternalThread
410     private class RecentTasksImpl implements RecentTasks {
411         @Override
getRecentTasks(int maxNum, int flags, int userId, Executor executor, Consumer<List<GroupedRecentTaskInfo>> callback)412         public void getRecentTasks(int maxNum, int flags, int userId, Executor executor,
413                 Consumer<List<GroupedRecentTaskInfo>> callback) {
414             mMainExecutor.execute(() -> {
415                 List<GroupedRecentTaskInfo> tasks =
416                         RecentTasksController.this.getRecentTasks(maxNum, flags, userId);
417                 executor.execute(() -> callback.accept(tasks));
418             });
419         }
420     }
421 
422 
423     /**
424      * The interface for calls from outside the host process.
425      */
426     @BinderThread
427     private static class IRecentTasksImpl extends IRecentTasks.Stub
428             implements ExternalInterfaceBinder {
429         private RecentTasksController mController;
430         private final SingleInstanceRemoteListener<RecentTasksController,
431                 IRecentTasksListener> mListener;
432         private final IRecentTasksListener mRecentTasksListener = new IRecentTasksListener.Stub() {
433             @Override
434             public void onRecentTasksChanged() throws RemoteException {
435                 mListener.call(l -> l.onRecentTasksChanged());
436             }
437 
438             @Override
439             public void onRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) {
440                 mListener.call(l -> l.onRunningTaskAppeared(taskInfo));
441             }
442 
443             @Override
444             public void onRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
445                 mListener.call(l -> l.onRunningTaskVanished(taskInfo));
446             }
447         };
448 
IRecentTasksImpl(RecentTasksController controller)449         public IRecentTasksImpl(RecentTasksController controller) {
450             mController = controller;
451             mListener = new SingleInstanceRemoteListener<>(controller,
452                     c -> c.registerRecentTasksListener(mRecentTasksListener),
453                     c -> c.unregisterRecentTasksListener());
454         }
455 
456         /**
457          * Invalidates this instance, preventing future calls from updating the controller.
458          */
459         @Override
invalidate()460         public void invalidate() {
461             mController = null;
462             // Unregister the listener to ensure any registered binder death recipients are unlinked
463             mListener.unregister();
464         }
465 
466         @Override
registerRecentTasksListener(IRecentTasksListener listener)467         public void registerRecentTasksListener(IRecentTasksListener listener)
468                 throws RemoteException {
469             executeRemoteCallWithTaskPermission(mController, "registerRecentTasksListener",
470                     (controller) -> mListener.register(listener));
471         }
472 
473         @Override
unregisterRecentTasksListener(IRecentTasksListener listener)474         public void unregisterRecentTasksListener(IRecentTasksListener listener)
475                 throws RemoteException {
476             executeRemoteCallWithTaskPermission(mController, "unregisterRecentTasksListener",
477                     (controller) -> mListener.unregister());
478         }
479 
480         @Override
getRecentTasks(int maxNum, int flags, int userId)481         public GroupedRecentTaskInfo[] getRecentTasks(int maxNum, int flags, int userId)
482                 throws RemoteException {
483             if (mController == null) {
484                 // The controller is already invalidated -- just return an empty task list for now
485                 return new GroupedRecentTaskInfo[0];
486             }
487 
488             final GroupedRecentTaskInfo[][] out = new GroupedRecentTaskInfo[][]{null};
489             executeRemoteCallWithTaskPermission(mController, "getRecentTasks",
490                     (controller) -> out[0] = controller.getRecentTasks(maxNum, flags, userId)
491                             .toArray(new GroupedRecentTaskInfo[0]),
492                     true /* blocking */);
493             return out[0];
494         }
495 
496         @Override
getRunningTasks(int maxNum)497         public ActivityManager.RunningTaskInfo[] getRunningTasks(int maxNum) {
498             final ActivityManager.RunningTaskInfo[][] tasks =
499                     new ActivityManager.RunningTaskInfo[][] {null};
500             executeRemoteCallWithTaskPermission(mController, "getRunningTasks",
501                     (controller) -> tasks[0] = ActivityTaskManager.getInstance().getTasks(maxNum)
502                             .toArray(new ActivityManager.RunningTaskInfo[0]),
503                     true /* blocking */);
504             return tasks[0];
505         }
506 
507         @Override
startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options, IApplicationThread appThread, IRecentsAnimationRunner listener)508         public void startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options,
509                 IApplicationThread appThread, IRecentsAnimationRunner listener) {
510             if (mController.mTransitionHandler == null) {
511                 Slog.e(TAG, "Used shell-transitions startRecentsTransition without"
512                         + " shell-transitions");
513                 return;
514             }
515             executeRemoteCallWithTaskPermission(mController, "startRecentsTransition",
516                     (controller) -> controller.mTransitionHandler.startRecentsTransition(
517                             intent, fillIn, options, appThread, listener));
518         }
519     }
520 }
521