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 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; 21 import static android.content.pm.PackageManager.FEATURE_PC; 22 import static android.view.Display.INVALID_DISPLAY; 23 24 import static com.android.wm.shell.Flags.enableShellTopTaskTracking; 25 import static com.android.wm.shell.desktopmode.DesktopWallpaperActivity.isWallpaperTask; 26 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_OBSERVER; 27 28 import android.Manifest; 29 import android.annotation.RequiresPermission; 30 import android.app.ActivityManager; 31 import android.app.ActivityManager.RecentTaskInfo; 32 import android.app.ActivityManager.RunningTaskInfo; 33 import android.app.ActivityTaskManager; 34 import android.app.IApplicationThread; 35 import android.app.KeyguardManager; 36 import android.app.PendingIntent; 37 import android.app.TaskInfo; 38 import android.content.ComponentName; 39 import android.content.Context; 40 import android.content.Intent; 41 import android.graphics.Color; 42 import android.graphics.Point; 43 import android.os.Bundle; 44 import android.os.RemoteException; 45 import android.util.Slog; 46 import android.util.SparseIntArray; 47 import android.window.DesktopExperienceFlags; 48 import android.window.DesktopModeFlags; 49 import android.window.WindowContainerToken; 50 51 import androidx.annotation.BinderThread; 52 import androidx.annotation.NonNull; 53 import androidx.annotation.Nullable; 54 import androidx.annotation.VisibleForTesting; 55 56 import com.android.internal.protolog.ProtoLog; 57 import com.android.wm.shell.common.ExternalInterfaceBinder; 58 import com.android.wm.shell.common.RemoteCallable; 59 import com.android.wm.shell.common.ShellExecutor; 60 import com.android.wm.shell.common.SingleInstanceRemoteListener; 61 import com.android.wm.shell.common.TaskStackListenerCallback; 62 import com.android.wm.shell.common.TaskStackListenerImpl; 63 import com.android.wm.shell.desktopmode.DesktopRepository; 64 import com.android.wm.shell.desktopmode.DesktopUserRepositories; 65 import com.android.wm.shell.protolog.ShellProtoLogGroup; 66 import com.android.wm.shell.shared.GroupedTaskInfo; 67 import com.android.wm.shell.shared.annotations.ExternalThread; 68 import com.android.wm.shell.shared.annotations.ShellMainThread; 69 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; 70 import com.android.wm.shell.shared.split.SplitBounds; 71 import com.android.wm.shell.sysui.ShellCommandHandler; 72 import com.android.wm.shell.sysui.ShellController; 73 import com.android.wm.shell.sysui.ShellInit; 74 import com.android.wm.shell.sysui.UserChangeListener; 75 76 import java.io.PrintWriter; 77 import java.util.ArrayList; 78 import java.util.HashMap; 79 import java.util.HashSet; 80 import java.util.List; 81 import java.util.Map; 82 import java.util.Optional; 83 import java.util.Set; 84 import java.util.concurrent.Executor; 85 import java.util.function.Consumer; 86 import java.util.stream.Collectors; 87 88 /** 89 * Manages the recent task list from the system, caching it as necessary. 90 */ 91 public class RecentTasksController implements TaskStackListenerCallback, 92 RemoteCallable<RecentTasksController>, DesktopRepository.ActiveTasksListener, 93 TaskStackTransitionObserver.TaskStackTransitionObserverListener, UserChangeListener { 94 private static final String TAG = RecentTasksController.class.getSimpleName(); 95 96 // When the multiple desktops feature is disabled, all freeform tasks are lumped together into 97 // a single `GroupedTaskInfo` whose type is `TYPE_DESK`, and its `mDeskId` doesn't matter, so 98 // we pick the below arbitrary value. 99 private static final int INVALID_DESK_ID = -1; 100 101 private final Context mContext; 102 private final ShellController mShellController; 103 private final ShellCommandHandler mShellCommandHandler; 104 private final Optional<DesktopUserRepositories> mDesktopUserRepositories; 105 106 private final ShellExecutor mMainExecutor; 107 private final TaskStackListenerImpl mTaskStackListener; 108 private final RecentTasksImpl mImpl = new RecentTasksImpl(); 109 private final ActivityTaskManager mActivityTaskManager; 110 private final TaskStackTransitionObserver mTaskStackTransitionObserver; 111 private final RecentsShellCommandHandler mRecentsShellCommandHandler; 112 private RecentsTransitionHandler mTransitionHandler = null; 113 private IRecentTasksListener mListener; 114 private final boolean mPcFeatureEnabled; 115 116 // Mapping of split task ids, mappings are symmetrical (ie. if t1 is the taskid of a task in a 117 // pair, then mSplitTasks[t1] = t2, and mSplitTasks[t2] = t1) 118 private final SparseIntArray mSplitTasks = new SparseIntArray(); 119 120 private int mUserId; 121 /** 122 * Maps taskId to {@link SplitBounds} for both taskIDs. 123 * Meaning there will be two taskId integers mapping to the same object. 124 * If there's any ordering to the pairing than we can probably just get away with only one 125 * taskID mapping to it, leaving both for consistency with {@link #mSplitTasks} for now. 126 */ 127 private final Map<Integer, SplitBounds> mTaskSplitBoundsMap = new HashMap<>(); 128 129 /** 130 * Cached list of the visible tasks, sorted from top most to bottom most. 131 */ 132 private final List<RunningTaskInfo> mVisibleTasks = new ArrayList<>(); 133 private final Map<Integer, TaskInfo> mVisibleTasksMap = new HashMap<>(); 134 135 // Temporary vars used in `generateList()` 136 private final Map<Integer, TaskInfo> mTmpRemaining = new HashMap<>(); 137 private final Map<Integer, Desk> mTmpDesks = new HashMap<>(); 138 139 /** 140 * Creates {@link RecentTasksController}, returns {@code null} if the feature is not 141 * supported. 142 */ 143 @Nullable create( Context context, ShellInit shellInit, ShellController shellController, ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, ActivityTaskManager activityTaskManager, Optional<DesktopUserRepositories> desktopUserRepositories, TaskStackTransitionObserver taskStackTransitionObserver, @ShellMainThread ShellExecutor mainExecutor )144 public static RecentTasksController create( 145 Context context, 146 ShellInit shellInit, 147 ShellController shellController, 148 ShellCommandHandler shellCommandHandler, 149 TaskStackListenerImpl taskStackListener, 150 ActivityTaskManager activityTaskManager, 151 Optional<DesktopUserRepositories> desktopUserRepositories, 152 TaskStackTransitionObserver taskStackTransitionObserver, 153 @ShellMainThread ShellExecutor mainExecutor 154 ) { 155 if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) { 156 return null; 157 } 158 return new RecentTasksController(context, shellInit, shellController, shellCommandHandler, 159 taskStackListener, activityTaskManager, desktopUserRepositories, 160 taskStackTransitionObserver, mainExecutor); 161 } 162 RecentTasksController(Context context, ShellInit shellInit, ShellController shellController, ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, ActivityTaskManager activityTaskManager, Optional<DesktopUserRepositories> desktopUserRepositories, TaskStackTransitionObserver taskStackTransitionObserver, ShellExecutor mainExecutor)163 RecentTasksController(Context context, 164 ShellInit shellInit, 165 ShellController shellController, 166 ShellCommandHandler shellCommandHandler, 167 TaskStackListenerImpl taskStackListener, 168 ActivityTaskManager activityTaskManager, 169 Optional<DesktopUserRepositories> desktopUserRepositories, 170 TaskStackTransitionObserver taskStackTransitionObserver, 171 ShellExecutor mainExecutor) { 172 mContext = context; 173 mShellController = shellController; 174 mShellCommandHandler = shellCommandHandler; 175 mActivityTaskManager = activityTaskManager; 176 mPcFeatureEnabled = mContext.getPackageManager().hasSystemFeature(FEATURE_PC); 177 mTaskStackListener = taskStackListener; 178 mDesktopUserRepositories = desktopUserRepositories; 179 mTaskStackTransitionObserver = taskStackTransitionObserver; 180 mMainExecutor = mainExecutor; 181 mRecentsShellCommandHandler = new RecentsShellCommandHandler(this); 182 shellInit.addInitCallback(this::onInit, this); 183 } 184 asRecentTasks()185 public RecentTasks asRecentTasks() { 186 return mImpl; 187 } 188 createExternalInterface()189 private ExternalInterfaceBinder createExternalInterface() { 190 return new IRecentTasksImpl(this); 191 } 192 193 @RequiresPermission(Manifest.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE) onInit()194 void onInit() { 195 mShellController.addExternalInterface(IRecentTasks.DESCRIPTOR, 196 this::createExternalInterface, this); 197 mShellCommandHandler.addDumpCallback(this::dump, this); 198 mShellCommandHandler.addCommandCallback("recents", mRecentsShellCommandHandler, this); 199 mUserId = ActivityManager.getCurrentUser(); 200 mDesktopUserRepositories.ifPresent( 201 desktopUserRepositories -> 202 desktopUserRepositories.getCurrent().addActiveTaskListener(this)); 203 mTaskStackListener.addListener(this); 204 mTaskStackTransitionObserver.addTaskStackTransitionObserverListener(this, 205 mMainExecutor); 206 mContext.getSystemService(KeyguardManager.class).addKeyguardLockedStateListener( 207 mMainExecutor, isKeyguardLocked -> notifyRecentTasksChanged()); 208 } 209 setTransitionHandler(RecentsTransitionHandler handler)210 void setTransitionHandler(RecentsTransitionHandler handler) { 211 mTransitionHandler = handler; 212 } 213 214 /** 215 * Adds a split pair. This call does not validate the taskIds, only that they are not the same. 216 */ addSplitPair(int taskId1, int taskId2, SplitBounds splitBounds)217 public boolean addSplitPair(int taskId1, int taskId2, SplitBounds splitBounds) { 218 if (taskId1 == taskId2) { 219 return false; 220 } 221 if (mSplitTasks.get(taskId1, INVALID_TASK_ID) == taskId2 222 && mTaskSplitBoundsMap.get(taskId1).equals(splitBounds)) { 223 // If the two tasks are already paired and the bounds are the same, then skip updating 224 return false; 225 } 226 // Remove any previous pairs 227 removeSplitPair(taskId1); 228 removeSplitPair(taskId2); 229 mTaskSplitBoundsMap.remove(taskId1); 230 mTaskSplitBoundsMap.remove(taskId2); 231 232 mSplitTasks.put(taskId1, taskId2); 233 mSplitTasks.put(taskId2, taskId1); 234 mTaskSplitBoundsMap.put(taskId1, splitBounds); 235 mTaskSplitBoundsMap.put(taskId2, splitBounds); 236 notifyRecentTasksChanged(); 237 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Add split pair: %d, %d, %s", 238 taskId1, taskId2, splitBounds); 239 return true; 240 } 241 242 /** 243 * Removes a split pair. 244 */ removeSplitPair(int taskId)245 public void removeSplitPair(int taskId) { 246 int pairedTaskId = mSplitTasks.get(taskId, INVALID_TASK_ID); 247 if (pairedTaskId != INVALID_TASK_ID) { 248 mSplitTasks.delete(taskId); 249 mSplitTasks.delete(pairedTaskId); 250 mTaskSplitBoundsMap.remove(taskId); 251 mTaskSplitBoundsMap.remove(pairedTaskId); 252 notifyRecentTasksChanged(); 253 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Remove split pair: %d, %d", 254 taskId, pairedTaskId); 255 } 256 } 257 258 @Nullable getSplitBoundsForTaskId(int taskId)259 public SplitBounds getSplitBoundsForTaskId(int taskId) { 260 if (taskId == INVALID_TASK_ID) { 261 return null; 262 } 263 264 // We could do extra verification of requiring both taskIds of a pair and verifying that 265 // the same split bounds object is returned... but meh. Seems unnecessary. 266 SplitBounds splitBounds = mTaskSplitBoundsMap.get(taskId); 267 if (splitBounds != null) { 268 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, 269 "getSplitBoundsForTaskId: taskId=%d splitBoundsTasks=[%d, %d]", taskId, 270 splitBounds.leftTopTaskId, splitBounds.rightBottomTaskId); 271 } else { 272 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, 273 "getSplitBoundsForTaskId: expected split bounds for taskId=%d but not found", 274 taskId); 275 } 276 return splitBounds; 277 } 278 279 @Override getContext()280 public Context getContext() { 281 return mContext; 282 } 283 284 @Override getRemoteCallExecutor()285 public ShellExecutor getRemoteCallExecutor() { 286 return mMainExecutor; 287 } 288 289 @Override onTaskStackChanged()290 public void onTaskStackChanged() { 291 if (!enableShellTopTaskTracking()) { 292 // Skip notifying recent tasks changed whenever task stack changes 293 notifyRecentTasksChanged(); 294 } 295 } 296 297 @Override onRecentTaskListUpdated()298 public void onRecentTaskListUpdated() { 299 // In some cases immediately after booting, the tasks in the system recent task list may be 300 // loaded, but not in the active task hierarchy in the system. These tasks are displayed in 301 // overview, but removing them don't result in a onTaskStackChanged() nor a onTaskRemoved() 302 // callback (those are for changes to the active tasks), but the task list is still updated, 303 // so we should also invalidate the change id to ensure we load a new list instead of 304 // reusing a stale list. 305 notifyRecentTasksChanged(); 306 } 307 308 /** 309 * This method only gets notified when a task is removed from recents as a result of another 310 * task being added to recent tasks. 311 */ 312 @Override onRecentTaskRemovedForAddTask(int taskId)313 public void onRecentTaskRemovedForAddTask(int taskId) { 314 mDesktopUserRepositories.ifPresent( 315 desktopUserRepositories -> desktopUserRepositories.getCurrent().removeTask( 316 INVALID_DISPLAY, taskId)); 317 } 318 onTaskAdded(RunningTaskInfo taskInfo)319 public void onTaskAdded(RunningTaskInfo taskInfo) { 320 notifyRunningTaskAppeared(taskInfo); 321 if (!enableShellTopTaskTracking()) { 322 notifyRecentTasksChanged(); 323 } 324 } 325 onTaskRemoved(RunningTaskInfo taskInfo)326 public void onTaskRemoved(RunningTaskInfo taskInfo) { 327 // Remove any split pairs associated with this task 328 removeSplitPair(taskInfo.taskId); 329 notifyRunningTaskVanished(taskInfo); 330 if (!enableShellTopTaskTracking()) { 331 // Only notify recent tasks changed if we aren't already notifying the visible tasks 332 notifyRecentTasksChanged(); 333 } 334 } 335 336 /** 337 * Notify listeners that the running infos related to recent tasks was updated. 338 * 339 * This currently includes windowing mode and visibility. 340 */ onTaskRunningInfoChanged(RunningTaskInfo taskInfo)341 public void onTaskRunningInfoChanged(RunningTaskInfo taskInfo) { 342 notifyRecentTasksChanged(); 343 notifyRunningTaskChanged(taskInfo); 344 } 345 346 @Override onActiveTasksChanged(int displayId)347 public void onActiveTasksChanged(int displayId) { 348 notifyRecentTasksChanged(); 349 } 350 351 @Override onTaskMovedToFrontThroughTransition(RunningTaskInfo runningTaskInfo)352 public void onTaskMovedToFrontThroughTransition(RunningTaskInfo runningTaskInfo) { 353 notifyTaskMovedToFront(runningTaskInfo); 354 } 355 356 @Override onTaskChangedThroughTransition(@onNull ActivityManager.RunningTaskInfo taskInfo)357 public void onTaskChangedThroughTransition(@NonNull ActivityManager.RunningTaskInfo taskInfo) { 358 notifyTaskInfoChanged(taskInfo); 359 } 360 361 @Override onVisibleTasksChanged(@onNull List<? extends RunningTaskInfo> visibleTasks)362 public void onVisibleTasksChanged(@NonNull List<? extends RunningTaskInfo> visibleTasks) { 363 mVisibleTasks.clear(); 364 mVisibleTasks.addAll(visibleTasks); 365 mVisibleTasksMap.clear(); 366 mVisibleTasksMap.putAll(mVisibleTasks.stream().collect( 367 Collectors.toMap(TaskInfo::getTaskId, task -> task))); 368 // Notify with all the info and not just the running task info 369 notifyVisibleTasksChanged(mVisibleTasks); 370 } 371 372 @VisibleForTesting notifyRecentTasksChanged()373 void notifyRecentTasksChanged() { 374 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Notify recent tasks changed"); 375 if (mListener == null) { 376 return; 377 } 378 try { 379 mListener.onRecentTasksChanged(); 380 } catch (RemoteException e) { 381 Slog.w(TAG, "Failed call notifyRecentTasksChanged", e); 382 } 383 } 384 385 /** 386 * Notify the running task listener that a task appeared on desktop environment. 387 */ notifyRunningTaskAppeared(RunningTaskInfo taskInfo)388 private void notifyRunningTaskAppeared(RunningTaskInfo taskInfo) { 389 if (mListener == null 390 || !shouldEnableRunningTasksForDesktopMode() 391 || taskInfo.realActivity == null 392 || excludeTaskFromGeneratedList(taskInfo)) { 393 return; 394 } 395 try { 396 mListener.onRunningTaskAppeared(taskInfo); 397 } catch (RemoteException e) { 398 Slog.w(TAG, "Failed call onRunningTaskAppeared", e); 399 } 400 } 401 402 /** 403 * Notify the running task listener that a task was changed on desktop environment. 404 */ notifyRunningTaskChanged(RunningTaskInfo taskInfo)405 private void notifyRunningTaskChanged(RunningTaskInfo taskInfo) { 406 if (mListener == null 407 || !shouldEnableRunningTasksForDesktopMode() 408 || taskInfo.realActivity == null 409 || excludeTaskFromGeneratedList(taskInfo)) { 410 return; 411 } 412 try { 413 mListener.onRunningTaskChanged(taskInfo); 414 } catch (RemoteException e) { 415 Slog.w(TAG, "Failed call onRunningTaskChanged", e); 416 } 417 } 418 419 /** 420 * Notify the running task listener that a task was removed on desktop environment. 421 */ notifyRunningTaskVanished(RunningTaskInfo taskInfo)422 private void notifyRunningTaskVanished(RunningTaskInfo taskInfo) { 423 if (mListener == null 424 || !shouldEnableRunningTasksForDesktopMode() 425 || taskInfo.realActivity == null 426 || excludeTaskFromGeneratedList(taskInfo)) { 427 return; 428 } 429 try { 430 mListener.onRunningTaskVanished(taskInfo); 431 } catch (RemoteException e) { 432 Slog.w(TAG, "Failed call onRunningTaskVanished", e); 433 } 434 } 435 436 /** 437 * Notify the recents task listener that a task moved to front via a transition. 438 */ notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo)439 private void notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) { 440 if (mListener == null 441 || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue() 442 || taskInfo.realActivity == null 443 || enableShellTopTaskTracking() 444 || excludeTaskFromGeneratedList(taskInfo)) { 445 return; 446 } 447 try { 448 mListener.onTaskMovedToFront(GroupedTaskInfo.forFullscreenTasks(taskInfo)); 449 } catch (RemoteException e) { 450 Slog.w(TAG, "Failed call onTaskMovedToFront", e); 451 } 452 } 453 454 /** 455 * Notify the recents task listener that a task changed via a transition. 456 */ notifyTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo)457 private void notifyTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { 458 if (mListener == null 459 || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue() 460 || taskInfo.realActivity == null 461 || enableShellTopTaskTracking() 462 || excludeTaskFromGeneratedList(taskInfo)) { 463 return; 464 } 465 try { 466 mListener.onTaskInfoChanged(taskInfo); 467 } catch (RemoteException e) { 468 Slog.w(TAG, "Failed call onTaskInfoChanged", e); 469 } 470 } 471 472 /** 473 * Notifies that the test of visible tasks have changed. 474 */ notifyVisibleTasksChanged(@onNull List<? extends RunningTaskInfo> visibleTasks)475 private void notifyVisibleTasksChanged(@NonNull List<? extends RunningTaskInfo> visibleTasks) { 476 if (mListener == null 477 || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue() 478 || !enableShellTopTaskTracking()) { 479 return; 480 } 481 try { 482 // Compute the visible recent tasks in order, and move the task to the top 483 mListener.onVisibleTasksChanged(generateList(visibleTasks, "visibleTasksChanged") 484 .toArray(new GroupedTaskInfo[0])); 485 } catch (RemoteException e) { 486 Slog.w(TAG, "Failed call onVisibleTasksChanged", e); 487 } 488 } 489 shouldEnableRunningTasksForDesktopMode()490 private boolean shouldEnableRunningTasksForDesktopMode() { 491 return mPcFeatureEnabled 492 || (DesktopModeStatus.canEnterDesktopMode(mContext) 493 && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS.isTrue()); 494 } 495 496 @VisibleForTesting registerRecentTasksListener(IRecentTasksListener listener)497 void registerRecentTasksListener(IRecentTasksListener listener) { 498 mListener = listener; 499 if (enableShellTopTaskTracking()) { 500 ProtoLog.v(WM_SHELL_TASK_OBSERVER, "registerRecentTasksListener"); 501 // Post a notification for the current set of visible tasks 502 mMainExecutor.executeDelayed(() -> notifyVisibleTasksChanged(mVisibleTasks), 0); 503 } 504 } 505 506 @VisibleForTesting unregisterRecentTasksListener()507 void unregisterRecentTasksListener() { 508 mListener = null; 509 } 510 511 @VisibleForTesting hasRecentTasksListener()512 boolean hasRecentTasksListener() { 513 return mListener != null; 514 } 515 516 @VisibleForTesting getRecentTasks(int maxNum, int flags, int userId)517 ArrayList<GroupedTaskInfo> getRecentTasks(int maxNum, int flags, int userId) { 518 // Note: the returned task list is ordered from the most-recent to least-recent order 519 return generateList(mActivityTaskManager.getRecentTasks(maxNum, flags, userId), 520 "getRecentTasks"); 521 } 522 523 /** 524 * Returns whether the given task should be excluded from the generated list. 525 */ excludeTaskFromGeneratedList(TaskInfo taskInfo)526 private boolean excludeTaskFromGeneratedList(TaskInfo taskInfo) { 527 if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) { 528 // We don't current send pinned tasks as a part of recent or running tasks 529 return true; 530 } 531 if (isWallpaperTask(taskInfo)) { 532 // Don't add the fullscreen wallpaper task as an entry in grouped tasks 533 return true; 534 } 535 return false; 536 } 537 538 /** 539 * Represents a desk whose ID is `mDeskId` inside the display with `mDisplayId` and contains 540 * the tasks in `mDeskTasks`. Some of these tasks are minimized and their IDs are contained 541 * in the `mMinimizedDeskTasks` set. 542 */ 543 private static class Desk { 544 final int mDeskId; 545 final int mDisplayId; 546 boolean mHasVisibleTasks = false; 547 final ArrayList<TaskInfo> mDeskTasks = new ArrayList<>(); 548 final Set<Integer> mMinimizedDeskTasks = new HashSet<>(); 549 Desk(int deskId, int displayId)550 Desk(int deskId, int displayId) { 551 mDeskId = deskId; 552 mDisplayId = displayId; 553 } 554 addTask(TaskInfo taskInfo, boolean isMinimized, boolean isVisible)555 void addTask(TaskInfo taskInfo, boolean isMinimized, boolean isVisible) { 556 mDeskTasks.add(taskInfo); 557 if (isMinimized) { 558 mMinimizedDeskTasks.add(taskInfo.taskId); 559 } 560 mHasVisibleTasks |= isVisible; 561 } 562 hasTasks()563 boolean hasTasks() { 564 return !mDeskTasks.isEmpty(); 565 } 566 createDeskTaskInfo()567 GroupedTaskInfo createDeskTaskInfo() { 568 return GroupedTaskInfo.forDeskTasks(mDeskId, mDisplayId, mDeskTasks, 569 mMinimizedDeskTasks); 570 } 571 } 572 573 /** 574 * Clears the `mTmpDesks` map, and re-initializes it with the current state of desks from all 575 * displays, without adding any desk tasks. This is a preparation step so that tasks can be 576 * added to these desks in `generateList()`. 577 * 578 * This is needed since with the `ENABLE_MULTIPLE_DESKTOPS_BACKEND` flag, we want to include 579 * desk even if they're empty (i.e. have no tasks). 580 * 581 * @param multipleDesktopsEnabled whether the multiple desktops backend feature is enabled. 582 */ initializeDesksMap(boolean multipleDesktopsEnabled)583 private void initializeDesksMap(boolean multipleDesktopsEnabled) { 584 mTmpDesks.clear(); 585 586 if (DesktopModeStatus.canEnterDesktopMode(mContext) 587 && mDesktopUserRepositories.isPresent()) { 588 if (multipleDesktopsEnabled) { 589 for (var deskId : mDesktopUserRepositories.get().getCurrent().getAllDeskIds()) { 590 getOrCreateDesk(deskId); 591 } 592 } else { 593 // When the multiple desks feature is disabled, we lump all freeform windows in a 594 // single `GroupedTaskInfo` regardless of their display. The `deskId` in this case 595 // doesn't matter and can be any arbitrary value. 596 getOrCreateDesk(/* deskId = */ INVALID_DESK_ID); 597 } 598 } 599 } 600 601 /** 602 * Returns the `Desk` whose ID is `deskId` from the `mTmpDesks` map if it exists, or it creates 603 * one and adds it to the map and then returns it. 604 */ getOrCreateDesk(int deskId)605 private Desk getOrCreateDesk(int deskId) { 606 var desk = mTmpDesks.get(deskId); 607 if (desk == null) { 608 desk = new Desk(deskId, 609 mDesktopUserRepositories.get().getCurrent().getDisplayForDesk(deskId)); 610 mTmpDesks.put(deskId, desk); 611 } 612 return desk; 613 } 614 615 /** 616 * Generates a list of GroupedTaskInfos for the given raw list of tasks (either recents or 617 * running tasks). 618 * 619 * The general flow is: 620 * - Collect the desktop tasks 621 * - Collect the visible tasks (in order), including the desktop tasks if visible 622 * - Construct the final list with the visible tasks, followed by the subsequent tasks 623 * - if enableShellTopTaskTracking() is enabled, the visible tasks will be grouped into 624 * a single mixed task 625 * - if the desktop tasks are not visible, they will be appended to the end of the list 626 * 627 * TODO(346588978): Generate list in per-display order 628 * 629 * @param tasks The list of tasks ordered from most recent to least recent 630 */ 631 @VisibleForTesting generateList(@onNull List<T> tasks, String reason)632 <T extends TaskInfo> ArrayList<GroupedTaskInfo> generateList(@NonNull List<T> tasks, 633 String reason) { 634 if (tasks.isEmpty()) { 635 return new ArrayList<>(); 636 } 637 638 if (enableShellTopTaskTracking()) { 639 ProtoLog.v(WM_SHELL_TASK_OBSERVER, "RecentTasksController.generateList(%s)", reason); 640 } 641 642 final boolean multipleDesktopsEnabled = 643 DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue(); 644 initializeDesksMap(multipleDesktopsEnabled); 645 646 // When the multiple desktops feature is enabled, we include all desks even if they're 647 // empty. 648 final boolean shouldIncludeEmptyDesktops = multipleDesktopsEnabled; 649 650 // Make a mapping of task id -> task info for the remaining tasks to be processed, this 651 // mapping is used to keep track of split tasks that may exist later in the task list that 652 // should be ignored because they've already been grouped 653 mTmpRemaining.clear(); 654 mTmpRemaining.putAll(tasks.stream().collect( 655 Collectors.toMap(TaskInfo::getTaskId, task -> task))); 656 657 // The final grouped tasks 658 ArrayList<GroupedTaskInfo> groupedTasks = new ArrayList<>(tasks.size()); 659 ArrayList<GroupedTaskInfo> visibleGroupedTasks = new ArrayList<>(); 660 661 // Phase 1: Extract the desktops and visible fullscreen/split tasks. 662 for (int i = 0; i < tasks.size(); i++) { 663 final TaskInfo taskInfo = tasks.get(i); 664 final int taskId = taskInfo.taskId; 665 666 if (!mTmpRemaining.containsKey(taskInfo.taskId)) { 667 // Skip if we've already processed it 668 continue; 669 } 670 671 if (excludeTaskFromGeneratedList(taskInfo)) { 672 // Skip and update the list if we are excluding this task 673 mTmpRemaining.remove(taskId); 674 continue; 675 } 676 677 // Desktop tasks 678 if (DesktopModeStatus.canEnterDesktopMode(mContext) && 679 mDesktopUserRepositories.isPresent() 680 && mDesktopUserRepositories.get().getCurrent().isActiveTask(taskId)) { 681 // If task has their app bounds set to null which happens after reboot, set the 682 // app bounds to persisted lastFullscreenBounds. Also set the position in parent 683 // to the top left of the bounds. 684 if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue() 685 && taskInfo.configuration.windowConfiguration.getAppBounds() == null) { 686 taskInfo.configuration.windowConfiguration.setAppBounds( 687 taskInfo.lastNonFullscreenBounds); 688 taskInfo.positionInParent = new Point(taskInfo.lastNonFullscreenBounds.left, 689 taskInfo.lastNonFullscreenBounds.top); 690 } 691 // Lump all freeform tasks together as if they were all in a single desk whose ID is 692 // `INVALID_DESK_ID` when the multiple desktops feature is disabled. 693 final int deskId = multipleDesktopsEnabled 694 ? mDesktopUserRepositories.get().getCurrent().getDeskIdForTask(taskId) 695 : INVALID_DESK_ID; 696 final Desk desk = getOrCreateDesk(deskId); 697 desk.addTask(taskInfo, 698 mDesktopUserRepositories.get().getCurrent().isMinimizedTask(taskId), 699 mVisibleTasksMap.containsKey(taskId)); 700 mTmpRemaining.remove(taskId); 701 continue; 702 } 703 704 if (enableShellTopTaskTracking()) { 705 // Visible tasks 706 if (mVisibleTasksMap.containsKey(taskId)) { 707 // Split tasks 708 if (extractAndAddSplitGroupedTask(taskInfo, mTmpRemaining, 709 visibleGroupedTasks)) { 710 continue; 711 } 712 713 // Fullscreen tasks 714 visibleGroupedTasks.add(GroupedTaskInfo.forFullscreenTasks(taskInfo)); 715 mTmpRemaining.remove(taskId); 716 } 717 } else { 718 // Split tasks 719 if (extractAndAddSplitGroupedTask(taskInfo, mTmpRemaining, groupedTasks)) { 720 continue; 721 } 722 723 // Fullscreen tasks 724 groupedTasks.add(GroupedTaskInfo.forFullscreenTasks(taskInfo)); 725 } 726 } 727 728 if (enableShellTopTaskTracking()) { 729 // Phase 2: If there were desktop tasks and they are visible, add them to the visible 730 // list as well (the actual order doesn't matter for Overview) 731 for (var desk : mTmpDesks.values()) { 732 if (desk.mHasVisibleTasks) { 733 visibleGroupedTasks.add(desk.createDeskTaskInfo()); 734 } 735 } 736 737 if (!visibleGroupedTasks.isEmpty()) { 738 // Phase 3: Combine the visible tasks into a single mixed grouped task, only if 739 // there are > 1 tasks to group, and add them to the final list 740 if (visibleGroupedTasks.size() > 1) { 741 groupedTasks.add(GroupedTaskInfo.forMixed(visibleGroupedTasks)); 742 } else { 743 groupedTasks.addAll(visibleGroupedTasks); 744 } 745 } 746 dumpGroupedTasks(groupedTasks, "Phase 3"); 747 748 // Phase 4: For the remaining non-visible split and fullscreen tasks, add grouped tasks 749 // in order to the final list 750 for (int i = 0; i < tasks.size(); i++) { 751 final TaskInfo taskInfo = tasks.get(i); 752 if (!mTmpRemaining.containsKey(taskInfo.taskId)) { 753 // Skip if we've already processed it 754 continue; 755 } 756 757 // Split tasks 758 if (extractAndAddSplitGroupedTask(taskInfo, mTmpRemaining, groupedTasks)) { 759 continue; 760 } 761 762 // Fullscreen tasks 763 groupedTasks.add(GroupedTaskInfo.forFullscreenTasks(taskInfo)); 764 } 765 dumpGroupedTasks(groupedTasks, "Phase 4"); 766 767 // Phase 5: If there were desktop tasks and they are not visible (ie. weren't added 768 // above), add them to the end of the final list (the actual order doesn't 769 // matter for Overview) 770 for (var desk : mTmpDesks.values()) { 771 if (!desk.mHasVisibleTasks && (desk.hasTasks() || shouldIncludeEmptyDesktops)) { 772 groupedTasks.add(desk.createDeskTaskInfo()); 773 } 774 } 775 dumpGroupedTasks(groupedTasks, "Phase 5"); 776 } else { 777 // Add the desktop tasks at the end of the list 778 for (var desk : mTmpDesks.values()) { 779 if (desk.hasTasks() || shouldIncludeEmptyDesktops) { 780 groupedTasks.add(desk.createDeskTaskInfo()); 781 } 782 } 783 } 784 785 return groupedTasks; 786 } 787 788 /** 789 * Only to be called from `generateList()`. If the given {@param taskInfo} has a paired task, 790 * then a split grouped task with the pair is added to {@param tasksOut}. 791 * 792 * @return whether a split task was extracted and added to the given list 793 */ extractAndAddSplitGroupedTask(@onNull TaskInfo taskInfo, @NonNull Map<Integer, TaskInfo> remainingTasks, @NonNull ArrayList<GroupedTaskInfo> tasksOut)794 private boolean extractAndAddSplitGroupedTask(@NonNull TaskInfo taskInfo, 795 @NonNull Map<Integer, TaskInfo> remainingTasks, 796 @NonNull ArrayList<GroupedTaskInfo> tasksOut) { 797 final int pairedTaskId = mSplitTasks.get(taskInfo.taskId, INVALID_TASK_ID); 798 if (pairedTaskId == INVALID_TASK_ID || !remainingTasks.containsKey(pairedTaskId)) { 799 return false; 800 } 801 802 // Add both this task and its pair to the list, and mark the paired task to be 803 // skipped when it is encountered in the list 804 final TaskInfo pairedTaskInfo = remainingTasks.get(pairedTaskId); 805 remainingTasks.remove(taskInfo.taskId); 806 remainingTasks.remove(pairedTaskId); 807 tasksOut.add(GroupedTaskInfo.forSplitTasks(taskInfo, pairedTaskInfo, 808 mTaskSplitBoundsMap.get(pairedTaskId))); 809 return true; 810 } 811 812 /** Dumps the set of tasks to protolog */ dumpGroupedTasks(List<GroupedTaskInfo> groupedTasks, String reason)813 private void dumpGroupedTasks(List<GroupedTaskInfo> groupedTasks, String reason) { 814 if (!WM_SHELL_TASK_OBSERVER.isEnabled()) { 815 return; 816 } 817 ProtoLog.v(WM_SHELL_TASK_OBSERVER, " Tasks (%s):", reason); 818 for (GroupedTaskInfo task : groupedTasks) { 819 ProtoLog.v(WM_SHELL_TASK_OBSERVER, " %s", task); 820 } 821 } 822 823 /** 824 * Returns the top running leaf task ignoring {@param ignoreTaskToken} if it is specified. 825 * NOTE: This path currently makes assumptions that ignoreTaskToken is for the top task. 826 */ 827 @Nullable getTopRunningTask( @ullable WindowContainerToken ignoreTaskToken)828 public RunningTaskInfo getTopRunningTask( 829 @Nullable WindowContainerToken ignoreTaskToken) { 830 final List<RunningTaskInfo> tasks = enableShellTopTaskTracking() 831 ? mVisibleTasks 832 : mActivityTaskManager.getTasks(2, false /* filterOnlyVisibleRecents */); 833 for (int i = 0; i < tasks.size(); i++) { 834 final RunningTaskInfo task = tasks.get(i); 835 if (task.token.equals(ignoreTaskToken)) { 836 continue; 837 } 838 return task; 839 } 840 return null; 841 } 842 843 /** 844 * Find the background task that match the given component. Ignores tasks match 845 * {@param ignoreTaskToken} if it is non-null. 846 */ 847 @Nullable findTaskInBackground(ComponentName componentName, int userId, @Nullable WindowContainerToken ignoreTaskToken)848 public RecentTaskInfo findTaskInBackground(ComponentName componentName, 849 int userId, @Nullable WindowContainerToken ignoreTaskToken) { 850 if (componentName == null) { 851 return null; 852 } 853 List<RecentTaskInfo> tasks = mActivityTaskManager.getRecentTasks( 854 Integer.MAX_VALUE, ActivityManager.RECENT_IGNORE_UNAVAILABLE, 855 ActivityManager.getCurrentUser()); 856 for (int i = 0; i < tasks.size(); i++) { 857 final RecentTaskInfo task = tasks.get(i); 858 if (task.isVisible) { 859 continue; 860 } 861 if (task.token.equals(ignoreTaskToken)) { 862 continue; 863 } 864 if (componentName.equals(task.baseIntent.getComponent()) && userId == task.userId) { 865 return task; 866 } 867 } 868 return null; 869 } 870 871 /** 872 * Find the background task (in the recent tasks list) that matches the given taskId. 873 */ 874 @Nullable findTaskInBackground(int taskId)875 public RecentTaskInfo findTaskInBackground(int taskId) { 876 List<RecentTaskInfo> tasks = mActivityTaskManager.getRecentTasks( 877 Integer.MAX_VALUE, ActivityManager.RECENT_IGNORE_UNAVAILABLE, 878 ActivityManager.getCurrentUser()); 879 for (int i = 0; i < tasks.size(); i++) { 880 final RecentTaskInfo task = tasks.get(i); 881 if (task.isVisible) { 882 continue; 883 } 884 if (taskId == task.taskId) { 885 return task; 886 } 887 } 888 return null; 889 } 890 891 /** 892 * Remove the background task that match the given taskId. This will remove the task regardless 893 * of whether it's active or recent. 894 */ removeBackgroundTask(int taskId)895 public boolean removeBackgroundTask(int taskId) { 896 return mActivityTaskManager.removeTask(taskId); 897 } 898 899 /** Removes all recent tasks that are visible. */ removeAllVisibleRecentTasks()900 public void removeAllVisibleRecentTasks() throws RemoteException { 901 ActivityTaskManager.getService().removeAllVisibleRecentTasks(); 902 } 903 dump(@onNull PrintWriter pw, String prefix)904 public void dump(@NonNull PrintWriter pw, String prefix) { 905 final String innerPrefix = prefix + " "; 906 pw.println(prefix + TAG); 907 pw.println(prefix + " mListener=" + mListener); 908 pw.println(prefix + "Tasks:"); 909 ArrayList<GroupedTaskInfo> recentTasks = getRecentTasks(Integer.MAX_VALUE, 910 ActivityManager.RECENT_IGNORE_UNAVAILABLE, ActivityManager.getCurrentUser()); 911 for (int i = 0; i < recentTasks.size(); i++) { 912 pw.println(innerPrefix + recentTasks.get(i)); 913 } 914 } 915 916 /** 917 * The interface for calls from outside the Shell, within the host process. 918 */ 919 @ExternalThread 920 private class RecentTasksImpl implements RecentTasks { 921 @Override getRecentTasks(int maxNum, int flags, int userId, Executor executor, Consumer<List<GroupedTaskInfo>> callback)922 public void getRecentTasks(int maxNum, int flags, int userId, Executor executor, 923 Consumer<List<GroupedTaskInfo>> callback) { 924 mMainExecutor.execute(() -> { 925 List<GroupedTaskInfo> tasks = 926 RecentTasksController.this.getRecentTasks(maxNum, flags, userId); 927 executor.execute(() -> callback.accept(tasks)); 928 }); 929 } 930 931 @Override addAnimationStateListener(Executor executor, Consumer<Boolean> listener)932 public void addAnimationStateListener(Executor executor, Consumer<Boolean> listener) { 933 mMainExecutor.execute(() -> { 934 if (mTransitionHandler == null) { 935 return; 936 } 937 mTransitionHandler.addTransitionStateListener(new RecentsTransitionStateListener() { 938 @Override 939 public void onTransitionStateChanged(@RecentsTransitionState int state) { 940 executor.execute(() -> { 941 listener.accept(RecentsTransitionStateListener.isAnimating(state)); 942 }); 943 } 944 }); 945 }); 946 } 947 948 @Override setTransitionBackgroundColor(@ullable Color color)949 public void setTransitionBackgroundColor(@Nullable Color color) { 950 mMainExecutor.execute(() -> { 951 if (mTransitionHandler == null) { 952 return; 953 } 954 mTransitionHandler.setTransitionBackgroundColor(color); 955 }); 956 } 957 } 958 959 @Override onUserChanged(int newUserId, @NonNull Context userContext)960 public void onUserChanged(int newUserId, @NonNull Context userContext) { 961 if (mDesktopUserRepositories.isEmpty()) return; 962 963 DesktopRepository previousUserRepository = 964 mDesktopUserRepositories.get().getProfile(mUserId); 965 mUserId = newUserId; 966 DesktopRepository currentUserRepository = 967 mDesktopUserRepositories.get().getProfile(newUserId); 968 969 // No-op if both profile ids map to the same user. 970 if (previousUserRepository.getUserId() == currentUserRepository.getUserId()) return; 971 previousUserRepository.removeActiveTasksListener(this); 972 currentUserRepository.addActiveTaskListener(this); 973 } 974 975 /** 976 * The interface for calls from outside the host process. 977 */ 978 @BinderThread 979 private static class IRecentTasksImpl extends IRecentTasks.Stub 980 implements ExternalInterfaceBinder { 981 private RecentTasksController mController; 982 private final SingleInstanceRemoteListener<RecentTasksController, 983 IRecentTasksListener> mListener; 984 private final IRecentTasksListener mRecentTasksListener = new IRecentTasksListener.Stub() { 985 @Override 986 public void onRecentTasksChanged() throws RemoteException { 987 mListener.call(l -> l.onRecentTasksChanged()); 988 } 989 990 @Override 991 public void onRunningTaskAppeared(RunningTaskInfo taskInfo) { 992 mListener.call(l -> l.onRunningTaskAppeared(taskInfo)); 993 } 994 995 @Override 996 public void onRunningTaskVanished(RunningTaskInfo taskInfo) { 997 mListener.call(l -> l.onRunningTaskVanished(taskInfo)); 998 } 999 1000 @Override 1001 public void onRunningTaskChanged(RunningTaskInfo taskInfo) { 1002 mListener.call(l -> l.onRunningTaskChanged(taskInfo)); 1003 } 1004 1005 @Override 1006 public void onTaskMovedToFront(GroupedTaskInfo taskToFront) { 1007 mListener.call(l -> l.onTaskMovedToFront(taskToFront)); 1008 } 1009 1010 @Override 1011 public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { 1012 mListener.call(l -> l.onTaskInfoChanged(taskInfo)); 1013 } 1014 1015 @Override 1016 public void onVisibleTasksChanged(GroupedTaskInfo[] visibleTasks) { 1017 mListener.call(l -> l.onVisibleTasksChanged(visibleTasks)); 1018 } 1019 }; 1020 IRecentTasksImpl(RecentTasksController controller)1021 public IRecentTasksImpl(RecentTasksController controller) { 1022 mController = controller; 1023 mListener = new SingleInstanceRemoteListener<>(controller, 1024 c -> c.registerRecentTasksListener(mRecentTasksListener), 1025 c -> c.unregisterRecentTasksListener()); 1026 } 1027 1028 /** 1029 * Invalidates this instance, preventing future calls from updating the controller. 1030 */ 1031 @Override invalidate()1032 public void invalidate() { 1033 mController = null; 1034 // Unregister the listener to ensure any registered binder death recipients are unlinked 1035 mListener.unregister(); 1036 } 1037 1038 @Override registerRecentTasksListener(IRecentTasksListener listener)1039 public void registerRecentTasksListener(IRecentTasksListener listener) 1040 throws RemoteException { 1041 executeRemoteCallWithTaskPermission(mController, "registerRecentTasksListener", 1042 (controller) -> mListener.register(listener)); 1043 } 1044 1045 @Override unregisterRecentTasksListener(IRecentTasksListener listener)1046 public void unregisterRecentTasksListener(IRecentTasksListener listener) 1047 throws RemoteException { 1048 executeRemoteCallWithTaskPermission(mController, "unregisterRecentTasksListener", 1049 (controller) -> mListener.unregister()); 1050 } 1051 1052 @Override getRecentTasks(int maxNum, int flags, int userId)1053 public GroupedTaskInfo[] getRecentTasks(int maxNum, int flags, int userId) 1054 throws RemoteException { 1055 if (mController == null) { 1056 // The controller is already invalidated -- just return an empty task list for now 1057 return new GroupedTaskInfo[0]; 1058 } 1059 1060 final GroupedTaskInfo[][] out = new GroupedTaskInfo[][]{null}; 1061 executeRemoteCallWithTaskPermission(mController, "getRecentTasks", 1062 (controller) -> { 1063 List<GroupedTaskInfo> tasks = controller.getRecentTasks( 1064 maxNum, flags, userId); 1065 out[0] = tasks.toArray(new GroupedTaskInfo[0]); 1066 }, 1067 true /* blocking */); 1068 return out[0]; 1069 } 1070 1071 @Override getRunningTasks(int maxNum)1072 public RunningTaskInfo[] getRunningTasks(int maxNum) { 1073 final RunningTaskInfo[][] tasks = 1074 new RunningTaskInfo[][]{null}; 1075 executeRemoteCallWithTaskPermission(mController, "getRunningTasks", 1076 (controller) -> tasks[0] = ActivityTaskManager.getInstance().getTasks(maxNum) 1077 .toArray(new RunningTaskInfo[0]), 1078 true /* blocking */); 1079 return tasks[0]; 1080 } 1081 1082 @Override startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options, IApplicationThread appThread, IRecentsAnimationRunner listener)1083 public void startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options, 1084 IApplicationThread appThread, IRecentsAnimationRunner listener) { 1085 if (mController.mTransitionHandler == null) { 1086 Slog.e(TAG, "Used shell-transitions startRecentsTransition without" 1087 + " shell-transitions"); 1088 return; 1089 } 1090 executeRemoteCallWithTaskPermission(mController, "startRecentsTransition", 1091 (controller) -> controller.mTransitionHandler.startRecentsTransition( 1092 intent, fillIn, options, appThread, listener)); 1093 } 1094 } 1095 } 1096