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