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