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 
21 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
22 
23 import android.app.ActivityManager;
24 import android.app.ActivityTaskManager;
25 import android.app.TaskInfo;
26 import android.content.Context;
27 import android.os.RemoteException;
28 import android.util.Slog;
29 import android.util.SparseArray;
30 import android.util.SparseIntArray;
31 
32 import androidx.annotation.BinderThread;
33 import androidx.annotation.NonNull;
34 import androidx.annotation.Nullable;
35 import androidx.annotation.VisibleForTesting;
36 
37 import com.android.wm.shell.common.RemoteCallable;
38 import com.android.wm.shell.common.ShellExecutor;
39 import com.android.wm.shell.common.SingleInstanceRemoteListener;
40 import com.android.wm.shell.common.TaskStackListenerCallback;
41 import com.android.wm.shell.common.TaskStackListenerImpl;
42 import com.android.wm.shell.common.annotations.ExternalThread;
43 import com.android.wm.shell.common.annotations.ShellMainThread;
44 import com.android.wm.shell.util.GroupedRecentTaskInfo;
45 import com.android.wm.shell.util.StagedSplitBounds;
46 
47 import java.io.PrintWriter;
48 import java.util.ArrayList;
49 import java.util.HashMap;
50 import java.util.List;
51 import java.util.Map;
52 
53 /**
54  * Manages the recent task list from the system, caching it as necessary.
55  */
56 public class RecentTasksController implements TaskStackListenerCallback,
57         RemoteCallable<RecentTasksController> {
58     private static final String TAG = RecentTasksController.class.getSimpleName();
59 
60     private final Context mContext;
61     private final ShellExecutor mMainExecutor;
62     private final TaskStackListenerImpl mTaskStackListener;
63     private final RecentTasks mImpl = new RecentTasksImpl();
64 
65     private final ArrayList<Runnable> mCallbacks = new ArrayList<>();
66     // Mapping of split task ids, mappings are symmetrical (ie. if t1 is the taskid of a task in a
67     // pair, then mSplitTasks[t1] = t2, and mSplitTasks[t2] = t1)
68     private final SparseIntArray mSplitTasks = new SparseIntArray();
69     /**
70      * Maps taskId to {@link StagedSplitBounds} for both taskIDs.
71      * Meaning there will be two taskId integers mapping to the same object.
72      * If there's any ordering to the pairing than we can probably just get away with only one
73      * taskID mapping to it, leaving both for consistency with {@link #mSplitTasks} for now.
74      */
75     private final Map<Integer, StagedSplitBounds> mTaskSplitBoundsMap = new HashMap<>();
76 
77     /**
78      * Creates {@link RecentTasksController}, returns {@code null} if the feature is not
79      * supported.
80      */
81     @Nullable
create( Context context, TaskStackListenerImpl taskStackListener, @ShellMainThread ShellExecutor mainExecutor )82     public static RecentTasksController create(
83             Context context,
84             TaskStackListenerImpl taskStackListener,
85             @ShellMainThread ShellExecutor mainExecutor
86     ) {
87         if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) {
88             return null;
89         }
90         return new RecentTasksController(context, taskStackListener, mainExecutor);
91     }
92 
RecentTasksController(Context context, TaskStackListenerImpl taskStackListener, ShellExecutor mainExecutor)93     RecentTasksController(Context context, TaskStackListenerImpl taskStackListener,
94             ShellExecutor mainExecutor) {
95         mContext = context;
96         mTaskStackListener = taskStackListener;
97         mMainExecutor = mainExecutor;
98     }
99 
asRecentTasks()100     public RecentTasks asRecentTasks() {
101         return mImpl;
102     }
103 
init()104     public void init() {
105         mTaskStackListener.addListener(this);
106     }
107 
108     /**
109      * Adds a split pair. This call does not validate the taskIds, only that they are not the same.
110      */
addSplitPair(int taskId1, int taskId2, StagedSplitBounds splitBounds)111     public void addSplitPair(int taskId1, int taskId2, StagedSplitBounds splitBounds) {
112         if (taskId1 == taskId2) {
113             return;
114         }
115         if (mSplitTasks.get(taskId1, INVALID_TASK_ID) == taskId2
116                 && mTaskSplitBoundsMap.get(taskId1).equals(splitBounds)) {
117             // If the two tasks are already paired and the bounds are the same, then skip updating
118             return;
119         }
120         // Remove any previous pairs
121         removeSplitPair(taskId1);
122         removeSplitPair(taskId2);
123         mTaskSplitBoundsMap.remove(taskId1);
124         mTaskSplitBoundsMap.remove(taskId2);
125 
126         mSplitTasks.put(taskId1, taskId2);
127         mSplitTasks.put(taskId2, taskId1);
128         mTaskSplitBoundsMap.put(taskId1, splitBounds);
129         mTaskSplitBoundsMap.put(taskId2, splitBounds);
130         notifyRecentTasksChanged();
131     }
132 
133     /**
134      * Removes a split pair.
135      */
removeSplitPair(int taskId)136     public void removeSplitPair(int taskId) {
137         int pairedTaskId = mSplitTasks.get(taskId, INVALID_TASK_ID);
138         if (pairedTaskId != INVALID_TASK_ID) {
139             mSplitTasks.delete(taskId);
140             mSplitTasks.delete(pairedTaskId);
141             mTaskSplitBoundsMap.remove(taskId);
142             mTaskSplitBoundsMap.remove(pairedTaskId);
143             notifyRecentTasksChanged();
144         }
145     }
146 
147     @Override
getContext()148     public Context getContext() {
149         return mContext;
150     }
151 
152     @Override
getRemoteCallExecutor()153     public ShellExecutor getRemoteCallExecutor() {
154         return mMainExecutor;
155     }
156 
157     @Override
onTaskStackChanged()158     public void onTaskStackChanged() {
159         notifyRecentTasksChanged();
160     }
161 
162     @Override
onRecentTaskListUpdated()163     public void onRecentTaskListUpdated() {
164         // In some cases immediately after booting, the tasks in the system recent task list may be
165         // loaded, but not in the active task hierarchy in the system.  These tasks are displayed in
166         // overview, but removing them don't result in a onTaskStackChanged() nor a onTaskRemoved()
167         // callback (those are for changes to the active tasks), but the task list is still updated,
168         // so we should also invalidate the change id to ensure we load a new list instead of
169         // reusing a stale list.
170         notifyRecentTasksChanged();
171     }
172 
onTaskRemoved(TaskInfo taskInfo)173     public void onTaskRemoved(TaskInfo taskInfo) {
174         // Remove any split pairs associated with this task
175         removeSplitPair(taskInfo.taskId);
176         notifyRecentTasksChanged();
177     }
178 
onTaskWindowingModeChanged(TaskInfo taskInfo)179     public void onTaskWindowingModeChanged(TaskInfo taskInfo) {
180         notifyRecentTasksChanged();
181     }
182 
183     @VisibleForTesting
notifyRecentTasksChanged()184     void notifyRecentTasksChanged() {
185         for (int i = 0; i < mCallbacks.size(); i++) {
186             mCallbacks.get(i).run();
187         }
188     }
189 
registerRecentTasksListener(Runnable listener)190     private void registerRecentTasksListener(Runnable listener) {
191         if (!mCallbacks.contains(listener)) {
192             mCallbacks.add(listener);
193         }
194     }
195 
unregisterRecentTasksListener(Runnable listener)196     private void unregisterRecentTasksListener(Runnable listener) {
197         mCallbacks.remove(listener);
198     }
199 
200     @VisibleForTesting
getRawRecentTasks(int maxNum, int flags, int userId)201     List<ActivityManager.RecentTaskInfo> getRawRecentTasks(int maxNum, int flags, int userId) {
202         return ActivityTaskManager.getInstance().getRecentTasks(maxNum, flags, userId);
203     }
204 
205     @VisibleForTesting
getRecentTasks(int maxNum, int flags, int userId)206     ArrayList<GroupedRecentTaskInfo> getRecentTasks(int maxNum, int flags, int userId) {
207         // Note: the returned task list is from the most-recent to least-recent order
208         final List<ActivityManager.RecentTaskInfo> rawList = getRawRecentTasks(maxNum, flags,
209                 userId);
210 
211         // Make a mapping of task id -> task info
212         final SparseArray<ActivityManager.RecentTaskInfo> rawMapping = new SparseArray<>();
213         for (int i = 0; i < rawList.size(); i++) {
214             final ActivityManager.RecentTaskInfo taskInfo = rawList.get(i);
215             rawMapping.put(taskInfo.taskId, taskInfo);
216         }
217 
218         // Pull out the pairs as we iterate back in the list
219         ArrayList<GroupedRecentTaskInfo> recentTasks = new ArrayList<>();
220         for (int i = 0; i < rawList.size(); i++) {
221             final ActivityManager.RecentTaskInfo taskInfo = rawList.get(i);
222             if (!rawMapping.contains(taskInfo.taskId)) {
223                 // If it's not in the mapping, then it was already paired with another task
224                 continue;
225             }
226 
227             final int pairedTaskId = mSplitTasks.get(taskInfo.taskId);
228             if (pairedTaskId != INVALID_TASK_ID && rawMapping.contains(pairedTaskId)) {
229                 final ActivityManager.RecentTaskInfo pairedTaskInfo = rawMapping.get(pairedTaskId);
230                 rawMapping.remove(pairedTaskId);
231                 recentTasks.add(new GroupedRecentTaskInfo(taskInfo, pairedTaskInfo,
232                         mTaskSplitBoundsMap.get(pairedTaskId)));
233             } else {
234                 recentTasks.add(new GroupedRecentTaskInfo(taskInfo));
235             }
236         }
237         return recentTasks;
238     }
239 
dump(@onNull PrintWriter pw, String prefix)240     public void dump(@NonNull PrintWriter pw, String prefix) {
241         final String innerPrefix = prefix + "  ";
242         pw.println(prefix + TAG);
243         ArrayList<GroupedRecentTaskInfo> recentTasks = getRecentTasks(Integer.MAX_VALUE,
244                 ActivityManager.RECENT_IGNORE_UNAVAILABLE, ActivityManager.getCurrentUser());
245         for (int i = 0; i < recentTasks.size(); i++) {
246             pw.println(innerPrefix + recentTasks.get(i));
247         }
248     }
249 
250     /**
251      * The interface for calls from outside the Shell, within the host process.
252      */
253     @ExternalThread
254     private class RecentTasksImpl implements RecentTasks {
255         private IRecentTasksImpl mIRecentTasks;
256 
257         @Override
createExternalInterface()258         public IRecentTasks createExternalInterface() {
259             if (mIRecentTasks != null) {
260                 mIRecentTasks.invalidate();
261             }
262             mIRecentTasks = new IRecentTasksImpl(RecentTasksController.this);
263             return mIRecentTasks;
264         }
265     }
266 
267 
268     /**
269      * The interface for calls from outside the host process.
270      */
271     @BinderThread
272     private static class IRecentTasksImpl extends IRecentTasks.Stub {
273         private RecentTasksController mController;
274         private final SingleInstanceRemoteListener<RecentTasksController,
275                 IRecentTasksListener> mListener;
276         private final Runnable mRecentTasksListener =
277                 new Runnable() {
278                     @Override
279                     public void run() {
280                         mListener.call(l -> l.onRecentTasksChanged());
281                     }
282                 };
283 
IRecentTasksImpl(RecentTasksController controller)284         public IRecentTasksImpl(RecentTasksController controller) {
285             mController = controller;
286             mListener = new SingleInstanceRemoteListener<>(controller,
287                     c -> c.registerRecentTasksListener(mRecentTasksListener),
288                     c -> c.unregisterRecentTasksListener(mRecentTasksListener));
289         }
290 
291         /**
292          * Invalidates this instance, preventing future calls from updating the controller.
293          */
invalidate()294         void invalidate() {
295             mController = null;
296         }
297 
298         @Override
registerRecentTasksListener(IRecentTasksListener listener)299         public void registerRecentTasksListener(IRecentTasksListener listener)
300                 throws RemoteException {
301             executeRemoteCallWithTaskPermission(mController, "registerRecentTasksListener",
302                     (controller) -> mListener.register(listener));
303         }
304 
305         @Override
unregisterRecentTasksListener(IRecentTasksListener listener)306         public void unregisterRecentTasksListener(IRecentTasksListener listener)
307                 throws RemoteException {
308             executeRemoteCallWithTaskPermission(mController, "unregisterRecentTasksListener",
309                     (controller) -> mListener.unregister());
310         }
311 
312         @Override
getRecentTasks(int maxNum, int flags, int userId)313         public GroupedRecentTaskInfo[] getRecentTasks(int maxNum, int flags, int userId)
314                 throws RemoteException {
315             if (mController == null) {
316                 // The controller is already invalidated -- just return an empty task list for now
317                 return new GroupedRecentTaskInfo[0];
318             }
319 
320             final GroupedRecentTaskInfo[][] out = new GroupedRecentTaskInfo[][]{null};
321             executeRemoteCallWithTaskPermission(mController, "getRecentTasks",
322                     (controller) -> out[0] = controller.getRecentTasks(maxNum, flags, userId)
323                             .toArray(new GroupedRecentTaskInfo[0]),
324                     true /* blocking */);
325             return out[0];
326         }
327     }
328 }