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 com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
20 
21 import android.annotation.TargetApi;
22 import android.app.ActivityManager;
23 import android.os.Build;
24 import android.os.Process;
25 import android.os.RemoteException;
26 import android.util.SparseBooleanArray;
27 
28 import androidx.annotation.VisibleForTesting;
29 
30 import com.android.quickstep.util.GroupTask;
31 import com.android.launcher3.util.LooperExecutor;
32 import com.android.launcher3.util.SplitConfigurationOptions;
33 import com.android.systemui.shared.recents.model.Task;
34 import com.android.systemui.shared.system.KeyguardManagerCompat;
35 import com.android.wm.shell.recents.IRecentTasksListener;
36 import com.android.wm.shell.util.GroupedRecentTaskInfo;
37 import com.android.wm.shell.util.StagedSplitBounds;
38 
39 import java.io.PrintWriter;
40 import java.util.ArrayList;
41 import java.util.Collections;
42 import java.util.function.Consumer;
43 
44 /**
45  * Manages the recent task list from the system, caching it as necessary.
46  */
47 @TargetApi(Build.VERSION_CODES.R)
48 public class RecentTasksList {
49 
50     private static final TaskLoadResult INVALID_RESULT = new TaskLoadResult(-1, false, 0);
51 
52     private final KeyguardManagerCompat mKeyguardManager;
53     private final LooperExecutor mMainThreadExecutor;
54     private final SystemUiProxy mSysUiProxy;
55 
56     // The list change id, increments as the task list changes in the system
57     private int mChangeId;
58     // Whether we are currently updating the tasks in the background (up to when the result is
59     // posted back on the main thread)
60     private boolean mLoadingTasksInBackground;
61 
62     private TaskLoadResult mResultsBg = INVALID_RESULT;
63     private TaskLoadResult mResultsUi = INVALID_RESULT;
64 
RecentTasksList(LooperExecutor mainThreadExecutor, KeyguardManagerCompat keyguardManager, SystemUiProxy sysUiProxy)65     public RecentTasksList(LooperExecutor mainThreadExecutor,
66             KeyguardManagerCompat keyguardManager, SystemUiProxy sysUiProxy) {
67         mMainThreadExecutor = mainThreadExecutor;
68         mKeyguardManager = keyguardManager;
69         mChangeId = 1;
70         mSysUiProxy = sysUiProxy;
71         sysUiProxy.registerRecentTasksListener(new IRecentTasksListener.Stub() {
72             @Override
73             public void onRecentTasksChanged() throws RemoteException {
74                 mMainThreadExecutor.execute(RecentTasksList.this::onRecentTasksChanged);
75             }
76         });
77     }
78 
79     @VisibleForTesting
isLoadingTasksInBackground()80     public boolean isLoadingTasksInBackground() {
81         return mLoadingTasksInBackground;
82     }
83 
84     /**
85      * Fetches the task keys skipping any local cache.
86      */
getTaskKeys(int numTasks, Consumer<ArrayList<GroupTask>> callback)87     public void getTaskKeys(int numTasks, Consumer<ArrayList<GroupTask>> callback) {
88         // Kick off task loading in the background
89         UI_HELPER_EXECUTOR.execute(() -> {
90             ArrayList<GroupTask> tasks = loadTasksInBackground(numTasks, -1,
91                     true /* loadKeysOnly */);
92             mMainThreadExecutor.execute(() -> callback.accept(tasks));
93         });
94     }
95 
96     /**
97      * Asynchronously fetches the list of recent tasks, reusing cached list if available.
98      *
99      * @param loadKeysOnly Whether to load other associated task data, or just the key
100      * @param callback The callback to receive the list of recent tasks
101      * @return The change id of the current task list
102      */
getTasks(boolean loadKeysOnly, Consumer<ArrayList<GroupTask>> callback)103     public synchronized int getTasks(boolean loadKeysOnly,
104             Consumer<ArrayList<GroupTask>> callback) {
105         final int requestLoadId = mChangeId;
106         if (mResultsUi.isValidForRequest(requestLoadId, loadKeysOnly)) {
107             // The list is up to date, send the callback on the next frame,
108             // so that requestID can be returned first.
109             if (callback != null) {
110                 // Copy synchronously as the changeId might change by next frame
111                 ArrayList<GroupTask> result = copyOf(mResultsUi);
112                 mMainThreadExecutor.post(() -> {
113                     callback.accept(result);
114                 });
115             }
116 
117             return requestLoadId;
118         }
119 
120         // Kick off task loading in the background
121         mLoadingTasksInBackground = true;
122         UI_HELPER_EXECUTOR.execute(() -> {
123             if (!mResultsBg.isValidForRequest(requestLoadId, loadKeysOnly)) {
124                 mResultsBg = loadTasksInBackground(Integer.MAX_VALUE, requestLoadId, loadKeysOnly);
125             }
126             TaskLoadResult loadResult = mResultsBg;
127             mMainThreadExecutor.execute(() -> {
128                 mLoadingTasksInBackground = false;
129                 mResultsUi = loadResult;
130                 if (callback != null) {
131                     ArrayList<GroupTask> result = copyOf(mResultsUi);
132                     callback.accept(result);
133                 }
134             });
135         });
136 
137         return requestLoadId;
138     }
139 
140     /**
141      * @return Whether the provided {@param changeId} is the latest recent tasks list id.
142      */
isTaskListValid(int changeId)143     public synchronized boolean isTaskListValid(int changeId) {
144         return mChangeId == changeId;
145     }
146 
onRecentTasksChanged()147     public void onRecentTasksChanged() {
148         invalidateLoadedTasks();
149     }
150 
invalidateLoadedTasks()151     private synchronized void invalidateLoadedTasks() {
152         UI_HELPER_EXECUTOR.execute(() -> mResultsBg = INVALID_RESULT);
153         mResultsUi = INVALID_RESULT;
154         mChangeId++;
155     }
156 
157     /**
158      * Loads and creates a list of all the recent tasks.
159      */
160     @VisibleForTesting
loadTasksInBackground(int numTasks, int requestId, boolean loadKeysOnly)161     TaskLoadResult loadTasksInBackground(int numTasks, int requestId, boolean loadKeysOnly) {
162         int currentUserId = Process.myUserHandle().getIdentifier();
163         ArrayList<GroupedRecentTaskInfo> rawTasks =
164                 mSysUiProxy.getRecentTasks(numTasks, currentUserId);
165         // The raw tasks are given in most-recent to least-recent order, we need to reverse it
166         Collections.reverse(rawTasks);
167 
168         SparseBooleanArray tmpLockedUsers = new SparseBooleanArray() {
169             @Override
170             public boolean get(int key) {
171                 if (indexOfKey(key) < 0) {
172                     // Fill the cached locked state as we fetch
173                     put(key, mKeyguardManager.isDeviceLocked(key));
174                 }
175                 return super.get(key);
176             }
177         };
178 
179         TaskLoadResult allTasks = new TaskLoadResult(requestId, loadKeysOnly, rawTasks.size());
180         for (GroupedRecentTaskInfo rawTask : rawTasks) {
181             ActivityManager.RecentTaskInfo taskInfo1 = rawTask.mTaskInfo1;
182             ActivityManager.RecentTaskInfo taskInfo2 = rawTask.mTaskInfo2;
183             Task.TaskKey task1Key = new Task.TaskKey(taskInfo1);
184             Task task1 = loadKeysOnly
185                     ? new Task(task1Key)
186                     : Task.from(task1Key, taskInfo1,
187                             tmpLockedUsers.get(task1Key.userId) /* isLocked */);
188             task1.setLastSnapshotData(taskInfo1);
189             Task task2 = null;
190             if (taskInfo2 != null) {
191                 Task.TaskKey task2Key = new Task.TaskKey(taskInfo2);
192                 task2 = loadKeysOnly
193                         ? new Task(task2Key)
194                         : Task.from(task2Key, taskInfo2,
195                                 tmpLockedUsers.get(task2Key.userId) /* isLocked */);
196                 task2.setLastSnapshotData(taskInfo2);
197             }
198             final SplitConfigurationOptions.StagedSplitBounds launcherSplitBounds =
199                     convertSplitBounds(rawTask.mStagedSplitBounds);
200             allTasks.add(new GroupTask(task1, task2, launcherSplitBounds));
201         }
202 
203         return allTasks;
204     }
205 
convertSplitBounds( StagedSplitBounds shellSplitBounds)206     private SplitConfigurationOptions.StagedSplitBounds convertSplitBounds(
207             StagedSplitBounds shellSplitBounds) {
208         return shellSplitBounds == null ?
209                 null :
210                 new SplitConfigurationOptions.StagedSplitBounds(
211                         shellSplitBounds.leftTopBounds, shellSplitBounds.rightBottomBounds,
212                         shellSplitBounds.leftTopTaskId, shellSplitBounds.rightBottomTaskId);
213     }
214 
copyOf(ArrayList<GroupTask> tasks)215     private ArrayList<GroupTask> copyOf(ArrayList<GroupTask> tasks) {
216         ArrayList<GroupTask> newTasks = new ArrayList<>();
217         for (int i = 0; i < tasks.size(); i++) {
218             newTasks.add(new GroupTask(tasks.get(i)));
219         }
220         return newTasks;
221     }
222 
dump(String prefix, PrintWriter writer)223     public void dump(String prefix, PrintWriter writer) {
224         writer.println(prefix + "RecentTasksList:");
225         writer.println(prefix + "  mChangeId=" + mChangeId);
226         writer.println(prefix + "  mResultsUi=[id=" + mResultsUi.mRequestId + ", tasks=");
227         for (GroupTask task : mResultsUi) {
228             writer.println(prefix + "    t1=" + task.task1.key.id
229                     + " t2=" + (task.hasMultipleTasks() ? task.task2.key.id : "-1"));
230         }
231         writer.println(prefix + "  ]");
232         int currentUserId = Process.myUserHandle().getIdentifier();
233         ArrayList<GroupedRecentTaskInfo> rawTasks =
234                 mSysUiProxy.getRecentTasks(Integer.MAX_VALUE, currentUserId);
235         writer.println(prefix + "  rawTasks=[");
236         for (GroupedRecentTaskInfo task : rawTasks) {
237             writer.println(prefix + "    t1=" + task.mTaskInfo1.taskId
238                     + " t2=" + (task.mTaskInfo2 != null ? task.mTaskInfo2.taskId : "-1"));
239         }
240         writer.println(prefix + "  ]");
241     }
242 
243     private static class TaskLoadResult extends ArrayList<GroupTask> {
244 
245         final int mRequestId;
246 
247         // If the result was loaded with keysOnly  = true
248         final boolean mKeysOnly;
249 
TaskLoadResult(int requestId, boolean keysOnly, int size)250         TaskLoadResult(int requestId, boolean keysOnly, int size) {
251             super(size);
252             mRequestId = requestId;
253             mKeysOnly = keysOnly;
254         }
255 
isValidForRequest(int requestId, boolean loadKeysOnly)256         boolean isValidForRequest(int requestId, boolean loadKeysOnly) {
257             return mRequestId == requestId && (!mKeysOnly || loadKeysOnly);
258         }
259     }
260 }