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