1 /*
2  * Copyright (C) 2014 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.quickstep;
18 
19 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
20 
21 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
22 import static com.android.quickstep.util.SplitScreenUtils.convertShellSplitBoundsToLauncher;
23 import static com.android.window.flags.Flags.enableDesktopWindowingMode;
24 import static com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_FREEFORM;
25 
26 import android.app.ActivityManager;
27 import android.app.KeyguardManager;
28 import android.app.TaskInfo;
29 import android.content.ComponentName;
30 import android.os.Process;
31 import android.os.RemoteException;
32 import android.util.SparseBooleanArray;
33 
34 import androidx.annotation.Nullable;
35 import androidx.annotation.VisibleForTesting;
36 
37 import com.android.launcher3.util.LooperExecutor;
38 import com.android.launcher3.util.SplitConfigurationOptions;
39 import com.android.quickstep.util.DesktopTask;
40 import com.android.quickstep.util.GroupTask;
41 import com.android.systemui.shared.recents.model.Task;
42 import com.android.wm.shell.recents.IRecentTasksListener;
43 import com.android.wm.shell.util.GroupedRecentTaskInfo;
44 
45 import java.io.PrintWriter;
46 import java.util.ArrayList;
47 import java.util.Collections;
48 import java.util.List;
49 import java.util.function.Consumer;
50 import java.util.function.Predicate;
51 import java.util.stream.Collectors;
52 
53 /**
54  * Manages the recent task list from the system, caching it as necessary.
55  */
56 public class RecentTasksList {
57 
58     private static final TaskLoadResult INVALID_RESULT = new TaskLoadResult(-1, false, 0);
59 
60     private final KeyguardManager mKeyguardManager;
61     private final LooperExecutor mMainThreadExecutor;
62     private final SystemUiProxy mSysUiProxy;
63 
64     // The list change id, increments as the task list changes in the system
65     private int mChangeId;
66     // Whether we are currently updating the tasks in the background (up to when the result is
67     // posted back on the main thread)
68     private boolean mLoadingTasksInBackground;
69 
70     private TaskLoadResult mResultsBg = INVALID_RESULT;
71     private TaskLoadResult mResultsUi = INVALID_RESULT;
72 
73     private RecentsModel.RunningTasksListener mRunningTasksListener;
74     // Tasks are stored in order of least recently launched to most recently launched.
75     private ArrayList<ActivityManager.RunningTaskInfo> mRunningTasks;
76 
RecentTasksList(LooperExecutor mainThreadExecutor, KeyguardManager keyguardManager, SystemUiProxy sysUiProxy, TopTaskTracker topTaskTracker)77     public RecentTasksList(LooperExecutor mainThreadExecutor, KeyguardManager keyguardManager,
78             SystemUiProxy sysUiProxy, TopTaskTracker topTaskTracker) {
79         mMainThreadExecutor = mainThreadExecutor;
80         mKeyguardManager = keyguardManager;
81         mChangeId = 1;
82         mSysUiProxy = sysUiProxy;
83         sysUiProxy.registerRecentTasksListener(new IRecentTasksListener.Stub() {
84             @Override
85             public void onRecentTasksChanged() throws RemoteException {
86                 mMainThreadExecutor.execute(RecentTasksList.this::onRecentTasksChanged);
87             }
88 
89             @Override
90             public void onRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) {
91                 mMainThreadExecutor.execute(() -> {
92                     RecentTasksList.this.onRunningTaskAppeared(taskInfo);
93                 });
94             }
95 
96             @Override
97             public void onRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
98                 mMainThreadExecutor.execute(() -> {
99                     RecentTasksList.this.onRunningTaskVanished(taskInfo);
100                 });
101             }
102 
103             @Override
104             public void onRunningTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
105                 mMainThreadExecutor.execute(() -> {
106                     RecentTasksList.this.onRunningTaskChanged(taskInfo);
107                 });
108             }
109 
110             @Override
111             public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
112                 mMainThreadExecutor.execute(() -> {
113                     topTaskTracker.onTaskMovedToFront(taskInfo);
114                 });
115             }
116         });
117         // We may receive onRunningTaskAppeared events later for tasks which have already been
118         // included in the list returned by mSysUiProxy.getRunningTasks(), or may receive
119         // onRunningTaskVanished for tasks not included in the returned list. These cases will be
120         // addressed when the tasks are added to/removed from mRunningTasks.
121         initRunningTasks(mSysUiProxy.getRunningTasks(Integer.MAX_VALUE));
122     }
123 
124     @VisibleForTesting
isLoadingTasksInBackground()125     public boolean isLoadingTasksInBackground() {
126         return mLoadingTasksInBackground;
127     }
128 
129     /**
130      * Fetches the task keys skipping any local cache.
131      */
getTaskKeys(int numTasks, Consumer<ArrayList<GroupTask>> callback)132     public void getTaskKeys(int numTasks, Consumer<ArrayList<GroupTask>> callback) {
133         // Kick off task loading in the background
134         UI_HELPER_EXECUTOR.execute(() -> {
135             ArrayList<GroupTask> tasks = loadTasksInBackground(numTasks, -1,
136                     true /* loadKeysOnly */);
137             mMainThreadExecutor.execute(() -> callback.accept(tasks));
138         });
139     }
140 
141     /**
142      * Asynchronously fetches the list of recent tasks, reusing cached list if available.
143      *
144      * @param loadKeysOnly Whether to load other associated task data, or just the key
145      * @param callback     The callback to receive the list of recent tasks
146      * @return The change id of the current task list
147      */
getTasks(boolean loadKeysOnly, @Nullable Consumer<List<GroupTask>> callback, Predicate<GroupTask> filter)148     public synchronized int getTasks(boolean loadKeysOnly,
149             @Nullable Consumer<List<GroupTask>> callback, Predicate<GroupTask> filter) {
150         final int requestLoadId = mChangeId;
151         if (mResultsUi.isValidForRequest(requestLoadId, loadKeysOnly)) {
152             // The list is up to date, send the callback on the next frame,
153             // so that requestID can be returned first.
154             if (callback != null) {
155                 // Copy synchronously as the changeId might change by next frame
156                 // and filter GroupTasks
157                 ArrayList<GroupTask> result = mResultsUi.stream().filter(filter)
158                         .map(GroupTask::copy)
159                         .collect(Collectors.toCollection(ArrayList<GroupTask>::new));
160 
161                 mMainThreadExecutor.post(() -> {
162                     callback.accept(result);
163                 });
164             }
165 
166             return requestLoadId;
167         }
168 
169         // Kick off task loading in the background
170         mLoadingTasksInBackground = true;
171         UI_HELPER_EXECUTOR.execute(() -> {
172             if (!mResultsBg.isValidForRequest(requestLoadId, loadKeysOnly)) {
173                 mResultsBg = loadTasksInBackground(Integer.MAX_VALUE, requestLoadId, loadKeysOnly);
174             }
175             TaskLoadResult loadResult = mResultsBg;
176             mMainThreadExecutor.execute(() -> {
177                 mLoadingTasksInBackground = false;
178                 mResultsUi = loadResult;
179                 if (callback != null) {
180                     // filter the tasks if needed before passing them into the callback
181                     ArrayList<GroupTask> result = mResultsUi.stream().filter(filter)
182                             .map(GroupTask::copy)
183                             .collect(Collectors.toCollection(ArrayList<GroupTask>::new));
184 
185                     callback.accept(result);
186                 }
187             });
188         });
189 
190         return requestLoadId;
191     }
192 
193     /**
194      * @return Whether the provided {@param changeId} is the latest recent tasks list id.
195      */
isTaskListValid(int changeId)196     public synchronized boolean isTaskListValid(int changeId) {
197         return mChangeId == changeId;
198     }
199 
onRecentTasksChanged()200     public void onRecentTasksChanged() {
201         invalidateLoadedTasks();
202     }
203 
invalidateLoadedTasks()204     private synchronized void invalidateLoadedTasks() {
205         UI_HELPER_EXECUTOR.execute(() -> mResultsBg = INVALID_RESULT);
206         mResultsUi = INVALID_RESULT;
207         mChangeId++;
208     }
209 
210     /**
211      * Registers a listener for running tasks
212      */
registerRunningTasksListener(RecentsModel.RunningTasksListener listener)213     public void registerRunningTasksListener(RecentsModel.RunningTasksListener listener) {
214         mRunningTasksListener = listener;
215     }
216 
217     /**
218      * Removes the previously registered running tasks listener
219      */
unregisterRunningTasksListener()220     public void unregisterRunningTasksListener() {
221         mRunningTasksListener = null;
222     }
223 
initRunningTasks(ArrayList<ActivityManager.RunningTaskInfo> runningTasks)224     private void initRunningTasks(ArrayList<ActivityManager.RunningTaskInfo> runningTasks) {
225         // Tasks are retrieved in order of most recently launched/used to least recently launched.
226         mRunningTasks = new ArrayList<>(runningTasks);
227         Collections.reverse(mRunningTasks);
228     }
229 
230     /**
231      * Gets the set of running tasks.
232      */
getRunningTasks()233     public ArrayList<ActivityManager.RunningTaskInfo> getRunningTasks() {
234         return mRunningTasks;
235     }
236 
onRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo)237     private void onRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) {
238         // Make sure this task is not already in the list
239         for (ActivityManager.RunningTaskInfo existingTask : mRunningTasks) {
240             if (taskInfo.taskId == existingTask.taskId) {
241                 return;
242             }
243         }
244         mRunningTasks.add(taskInfo);
245         if (mRunningTasksListener != null) {
246             mRunningTasksListener.onRunningTasksChanged();
247         }
248     }
249 
onRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo)250     private void onRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
251         // Find the task from the list of running tasks, if it exists
252         for (ActivityManager.RunningTaskInfo existingTask : mRunningTasks) {
253             if (existingTask.taskId != taskInfo.taskId) continue;
254 
255             mRunningTasks.remove(existingTask);
256             if (mRunningTasksListener != null) {
257                 mRunningTasksListener.onRunningTasksChanged();
258             }
259             return;
260         }
261     }
262 
onRunningTaskChanged(ActivityManager.RunningTaskInfo taskInfo)263     private void onRunningTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
264         // Find the task from the list of running tasks, if it exists
265         for (ActivityManager.RunningTaskInfo existingTask : mRunningTasks) {
266             if (existingTask.taskId != taskInfo.taskId) continue;
267 
268             mRunningTasks.remove(existingTask);
269             mRunningTasks.add(taskInfo);
270             if (mRunningTasksListener != null) {
271                 mRunningTasksListener.onRunningTasksChanged();
272             }
273             return;
274         }
275     }
276 
277     /**
278      * Loads and creates a list of all the recent tasks.
279      */
280     @VisibleForTesting
loadTasksInBackground(int numTasks, int requestId, boolean loadKeysOnly)281     TaskLoadResult loadTasksInBackground(int numTasks, int requestId, boolean loadKeysOnly) {
282         int currentUserId = Process.myUserHandle().getIdentifier();
283         ArrayList<GroupedRecentTaskInfo> rawTasks =
284                 mSysUiProxy.getRecentTasks(numTasks, currentUserId);
285         // The raw tasks are given in most-recent to least-recent order, we need to reverse it
286         Collections.reverse(rawTasks);
287 
288         SparseBooleanArray tmpLockedUsers = new SparseBooleanArray() {
289             @Override
290             public boolean get(int key) {
291                 if (indexOfKey(key) < 0) {
292                     // Fill the cached locked state as we fetch
293                     put(key, mKeyguardManager.isDeviceLocked(key));
294                 }
295                 return super.get(key);
296             }
297         };
298 
299         TaskLoadResult allTasks = new TaskLoadResult(requestId, loadKeysOnly, rawTasks.size());
300 
301         int numVisibleTasks = 0;
302         for (GroupedRecentTaskInfo rawTask : rawTasks) {
303             if (rawTask.getType() == TYPE_FREEFORM) {
304                 // TYPE_FREEFORM tasks is only created when enableDesktopWindowingMode() is true,
305                 // leftover TYPE_FREEFORM tasks created when flag was on should be ignored.
306                 if (enableDesktopWindowingMode()) {
307                     GroupTask desktopTask = createDesktopTask(rawTask);
308                     allTasks.add(desktopTask);
309                 }
310                 continue;
311             }
312             ActivityManager.RecentTaskInfo taskInfo1 = rawTask.getTaskInfo1();
313             ActivityManager.RecentTaskInfo taskInfo2 = rawTask.getTaskInfo2();
314             Task.TaskKey task1Key = new Task.TaskKey(taskInfo1);
315             Task task1 = loadKeysOnly
316                     ? new Task(task1Key)
317                     : Task.from(task1Key, taskInfo1,
318                             tmpLockedUsers.get(task1Key.userId) /* isLocked */);
319             task1.setLastSnapshotData(taskInfo1);
320             Task task2 = null;
321             if (taskInfo2 != null) {
322                 // Is split task
323                 Task.TaskKey task2Key = new Task.TaskKey(taskInfo2);
324                 task2 = loadKeysOnly
325                         ? new Task(task2Key)
326                         : Task.from(task2Key, taskInfo2,
327                                 tmpLockedUsers.get(task2Key.userId) /* isLocked */);
328                 task2.setLastSnapshotData(taskInfo2);
329             } else {
330                 // Is fullscreen task
331                 if (numVisibleTasks > 0) {
332                     boolean isExcluded = (taskInfo1.baseIntent.getFlags()
333                             & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
334                     if (taskInfo1.isTopActivityTransparent && isExcluded) {
335                         // If there are already visible tasks, then ignore the excluded tasks and
336                         // don't add them to the returned list
337                         continue;
338                     }
339                 }
340             }
341             if (taskInfo1.isVisible) {
342                 numVisibleTasks++;
343             }
344             final SplitConfigurationOptions.SplitBounds launcherSplitBounds =
345                     convertShellSplitBoundsToLauncher(rawTask.getSplitBounds());
346             allTasks.add(new GroupTask(task1, task2, launcherSplitBounds));
347         }
348 
349         return allTasks;
350     }
351 
createDesktopTask(GroupedRecentTaskInfo recentTaskInfo)352     private DesktopTask createDesktopTask(GroupedRecentTaskInfo recentTaskInfo) {
353         ArrayList<Task> tasks = new ArrayList<>(recentTaskInfo.getTaskInfoList().size());
354         for (ActivityManager.RecentTaskInfo taskInfo : recentTaskInfo.getTaskInfoList()) {
355             Task.TaskKey key = new Task.TaskKey(taskInfo);
356             Task task = Task.from(key, taskInfo, false);
357             task.setLastSnapshotData(taskInfo);
358             task.positionInParent = taskInfo.positionInParent;
359             task.appBounds = taskInfo.configuration.windowConfiguration.getAppBounds();
360             tasks.add(task);
361         }
362         return new DesktopTask(tasks);
363     }
364 
copyOf(ArrayList<GroupTask> tasks)365     private ArrayList<GroupTask> copyOf(ArrayList<GroupTask> tasks) {
366         ArrayList<GroupTask> newTasks = new ArrayList<>();
367         for (int i = 0; i < tasks.size(); i++) {
368             newTasks.add(tasks.get(i).copy());
369         }
370         return newTasks;
371     }
372 
dump(String prefix, PrintWriter writer)373     public void dump(String prefix, PrintWriter writer) {
374         writer.println(prefix + "RecentTasksList:");
375         writer.println(prefix + "  mChangeId=" + mChangeId);
376         writer.println(prefix + "  mResultsUi=[id=" + mResultsUi.mRequestId + ", tasks=");
377         for (GroupTask task : mResultsUi) {
378             Task task1 = task.task1;
379             Task task2 = task.task2;
380             ComponentName cn1 = task1.getTopComponent();
381             ComponentName cn2 = task2 != null ? task2.getTopComponent() : null;
382             writer.println(prefix + "    t1: (id=" + task1.key.id
383                     + "; package=" + (cn1 != null ? cn1.getPackageName() + ")" : "no package)")
384                     + " t2: (id=" + (task2 != null ? task2.key.id : "-1")
385                     + "; package=" + (cn2 != null ? cn2.getPackageName() + ")" : "no package)"));
386         }
387         writer.println(prefix + "  ]");
388         int currentUserId = Process.myUserHandle().getIdentifier();
389         ArrayList<GroupedRecentTaskInfo> rawTasks =
390                 mSysUiProxy.getRecentTasks(Integer.MAX_VALUE, currentUserId);
391         writer.println(prefix + "  rawTasks=[");
392         for (GroupedRecentTaskInfo task : rawTasks) {
393             TaskInfo taskInfo1 = task.getTaskInfo1();
394             TaskInfo taskInfo2 = task.getTaskInfo2();
395             ComponentName cn1 = taskInfo1.topActivity;
396             ComponentName cn2 = taskInfo2 != null ? taskInfo2.topActivity : null;
397             writer.println(prefix + "    t1: (id=" + taskInfo1.taskId
398                     + "; package=" + (cn1 != null ? cn1.getPackageName() + ")" : "no package)")
399                     + " t2: (id=" + (taskInfo2 != null ? taskInfo2.taskId : "-1")
400                     + "; package=" + (cn2 != null ? cn2.getPackageName() + ")" : "no package)"));
401         }
402         writer.println(prefix + "  ]");
403     }
404 
405     @VisibleForTesting
406     static class TaskLoadResult extends ArrayList<GroupTask> {
407 
408         final int mRequestId;
409 
410         // If the result was loaded with keysOnly  = true
411         final boolean mKeysOnly;
412 
TaskLoadResult(int requestId, boolean keysOnly, int size)413         TaskLoadResult(int requestId, boolean keysOnly, int size) {
414             super(size);
415             mRequestId = requestId;
416             mKeysOnly = keysOnly;
417         }
418 
isValidForRequest(int requestId, boolean loadKeysOnly)419         boolean isValidForRequest(int requestId, boolean loadKeysOnly) {
420             return mRequestId == requestId && (!mKeysOnly || loadKeysOnly);
421         }
422     }
423 }
424