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.content.pm.PackageManager.FEATURE_PC; 21 22 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; 23 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS; 24 25 import android.app.ActivityManager; 26 import android.app.ActivityTaskManager; 27 import android.app.IApplicationThread; 28 import android.app.PendingIntent; 29 import android.app.TaskInfo; 30 import android.content.ComponentName; 31 import android.content.Context; 32 import android.content.Intent; 33 import android.os.Bundle; 34 import android.os.RemoteException; 35 import android.util.Slog; 36 import android.util.SparseArray; 37 import android.util.SparseIntArray; 38 import android.view.IRecentsAnimationRunner; 39 40 import androidx.annotation.BinderThread; 41 import androidx.annotation.NonNull; 42 import androidx.annotation.Nullable; 43 import androidx.annotation.VisibleForTesting; 44 45 import com.android.internal.protolog.common.ProtoLog; 46 import com.android.wm.shell.common.ExternalInterfaceBinder; 47 import com.android.wm.shell.common.RemoteCallable; 48 import com.android.wm.shell.common.ShellExecutor; 49 import com.android.wm.shell.common.SingleInstanceRemoteListener; 50 import com.android.wm.shell.common.TaskStackListenerCallback; 51 import com.android.wm.shell.common.TaskStackListenerImpl; 52 import com.android.wm.shell.common.annotations.ExternalThread; 53 import com.android.wm.shell.common.annotations.ShellMainThread; 54 import com.android.wm.shell.desktopmode.DesktopModeStatus; 55 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; 56 import com.android.wm.shell.protolog.ShellProtoLogGroup; 57 import com.android.wm.shell.sysui.ShellCommandHandler; 58 import com.android.wm.shell.sysui.ShellController; 59 import com.android.wm.shell.sysui.ShellInit; 60 import com.android.wm.shell.util.GroupedRecentTaskInfo; 61 import com.android.wm.shell.util.SplitBounds; 62 63 import java.io.PrintWriter; 64 import java.util.ArrayList; 65 import java.util.HashMap; 66 import java.util.List; 67 import java.util.Map; 68 import java.util.Optional; 69 import java.util.concurrent.Executor; 70 import java.util.function.Consumer; 71 72 /** 73 * Manages the recent task list from the system, caching it as necessary. 74 */ 75 public class RecentTasksController implements TaskStackListenerCallback, 76 RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener { 77 private static final String TAG = RecentTasksController.class.getSimpleName(); 78 79 private final Context mContext; 80 private final ShellController mShellController; 81 private final ShellCommandHandler mShellCommandHandler; 82 private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository; 83 private final ShellExecutor mMainExecutor; 84 private final TaskStackListenerImpl mTaskStackListener; 85 private final RecentTasksImpl mImpl = new RecentTasksImpl(); 86 private final ActivityTaskManager mActivityTaskManager; 87 private RecentsTransitionHandler mTransitionHandler = null; 88 private IRecentTasksListener mListener; 89 private final boolean mIsDesktopMode; 90 91 // Mapping of split task ids, mappings are symmetrical (ie. if t1 is the taskid of a task in a 92 // pair, then mSplitTasks[t1] = t2, and mSplitTasks[t2] = t1) 93 private final SparseIntArray mSplitTasks = new SparseIntArray(); 94 /** 95 * Maps taskId to {@link SplitBounds} for both taskIDs. 96 * Meaning there will be two taskId integers mapping to the same object. 97 * If there's any ordering to the pairing than we can probably just get away with only one 98 * taskID mapping to it, leaving both for consistency with {@link #mSplitTasks} for now. 99 */ 100 private final Map<Integer, SplitBounds> mTaskSplitBoundsMap = new HashMap<>(); 101 102 /** 103 * Creates {@link RecentTasksController}, returns {@code null} if the feature is not 104 * supported. 105 */ 106 @Nullable create( Context context, ShellInit shellInit, ShellController shellController, ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, ActivityTaskManager activityTaskManager, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, @ShellMainThread ShellExecutor mainExecutor )107 public static RecentTasksController create( 108 Context context, 109 ShellInit shellInit, 110 ShellController shellController, 111 ShellCommandHandler shellCommandHandler, 112 TaskStackListenerImpl taskStackListener, 113 ActivityTaskManager activityTaskManager, 114 Optional<DesktopModeTaskRepository> desktopModeTaskRepository, 115 @ShellMainThread ShellExecutor mainExecutor 116 ) { 117 if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) { 118 return null; 119 } 120 return new RecentTasksController(context, shellInit, shellController, shellCommandHandler, 121 taskStackListener, activityTaskManager, desktopModeTaskRepository, mainExecutor); 122 } 123 RecentTasksController(Context context, ShellInit shellInit, ShellController shellController, ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, ActivityTaskManager activityTaskManager, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, ShellExecutor mainExecutor)124 RecentTasksController(Context context, 125 ShellInit shellInit, 126 ShellController shellController, 127 ShellCommandHandler shellCommandHandler, 128 TaskStackListenerImpl taskStackListener, 129 ActivityTaskManager activityTaskManager, 130 Optional<DesktopModeTaskRepository> desktopModeTaskRepository, 131 ShellExecutor mainExecutor) { 132 mContext = context; 133 mShellController = shellController; 134 mShellCommandHandler = shellCommandHandler; 135 mActivityTaskManager = activityTaskManager; 136 mIsDesktopMode = mContext.getPackageManager().hasSystemFeature(FEATURE_PC); 137 mTaskStackListener = taskStackListener; 138 mDesktopModeTaskRepository = desktopModeTaskRepository; 139 mMainExecutor = mainExecutor; 140 shellInit.addInitCallback(this::onInit, this); 141 } 142 asRecentTasks()143 public RecentTasks asRecentTasks() { 144 return mImpl; 145 } 146 createExternalInterface()147 private ExternalInterfaceBinder createExternalInterface() { 148 return new IRecentTasksImpl(this); 149 } 150 onInit()151 private void onInit() { 152 mShellController.addExternalInterface(KEY_EXTRA_SHELL_RECENT_TASKS, 153 this::createExternalInterface, this); 154 mShellCommandHandler.addDumpCallback(this::dump, this); 155 mTaskStackListener.addListener(this); 156 mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTaskListener(this)); 157 } 158 setTransitionHandler(RecentsTransitionHandler handler)159 void setTransitionHandler(RecentsTransitionHandler handler) { 160 mTransitionHandler = handler; 161 } 162 163 /** 164 * Adds a split pair. This call does not validate the taskIds, only that they are not the same. 165 */ addSplitPair(int taskId1, int taskId2, SplitBounds splitBounds)166 public void addSplitPair(int taskId1, int taskId2, SplitBounds splitBounds) { 167 if (taskId1 == taskId2) { 168 return; 169 } 170 if (mSplitTasks.get(taskId1, INVALID_TASK_ID) == taskId2 171 && mTaskSplitBoundsMap.get(taskId1).equals(splitBounds)) { 172 // If the two tasks are already paired and the bounds are the same, then skip updating 173 return; 174 } 175 // Remove any previous pairs 176 removeSplitPair(taskId1); 177 removeSplitPair(taskId2); 178 mTaskSplitBoundsMap.remove(taskId1); 179 mTaskSplitBoundsMap.remove(taskId2); 180 181 mSplitTasks.put(taskId1, taskId2); 182 mSplitTasks.put(taskId2, taskId1); 183 mTaskSplitBoundsMap.put(taskId1, splitBounds); 184 mTaskSplitBoundsMap.put(taskId2, splitBounds); 185 notifyRecentTasksChanged(); 186 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Add split pair: %d, %d, %s", 187 taskId1, taskId2, splitBounds); 188 } 189 190 /** 191 * Removes a split pair. 192 */ removeSplitPair(int taskId)193 public void removeSplitPair(int taskId) { 194 int pairedTaskId = mSplitTasks.get(taskId, INVALID_TASK_ID); 195 if (pairedTaskId != INVALID_TASK_ID) { 196 mSplitTasks.delete(taskId); 197 mSplitTasks.delete(pairedTaskId); 198 mTaskSplitBoundsMap.remove(taskId); 199 mTaskSplitBoundsMap.remove(pairedTaskId); 200 notifyRecentTasksChanged(); 201 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Remove split pair: %d, %d", 202 taskId, pairedTaskId); 203 } 204 } 205 206 @Override getContext()207 public Context getContext() { 208 return mContext; 209 } 210 211 @Override getRemoteCallExecutor()212 public ShellExecutor getRemoteCallExecutor() { 213 return mMainExecutor; 214 } 215 216 @Override onTaskStackChanged()217 public void onTaskStackChanged() { 218 notifyRecentTasksChanged(); 219 } 220 221 @Override onRecentTaskListUpdated()222 public void onRecentTaskListUpdated() { 223 // In some cases immediately after booting, the tasks in the system recent task list may be 224 // loaded, but not in the active task hierarchy in the system. These tasks are displayed in 225 // overview, but removing them don't result in a onTaskStackChanged() nor a onTaskRemoved() 226 // callback (those are for changes to the active tasks), but the task list is still updated, 227 // so we should also invalidate the change id to ensure we load a new list instead of 228 // reusing a stale list. 229 notifyRecentTasksChanged(); 230 } 231 onTaskAdded(ActivityManager.RunningTaskInfo taskInfo)232 public void onTaskAdded(ActivityManager.RunningTaskInfo taskInfo) { 233 notifyRunningTaskAppeared(taskInfo); 234 } 235 onTaskRemoved(ActivityManager.RunningTaskInfo taskInfo)236 public void onTaskRemoved(ActivityManager.RunningTaskInfo taskInfo) { 237 // Remove any split pairs associated with this task 238 removeSplitPair(taskInfo.taskId); 239 notifyRecentTasksChanged(); 240 notifyRunningTaskVanished(taskInfo); 241 } 242 onTaskWindowingModeChanged(TaskInfo taskInfo)243 public void onTaskWindowingModeChanged(TaskInfo taskInfo) { 244 notifyRecentTasksChanged(); 245 } 246 247 @Override onActiveTasksChanged(int displayId)248 public void onActiveTasksChanged(int displayId) { 249 notifyRecentTasksChanged(); 250 } 251 252 @VisibleForTesting notifyRecentTasksChanged()253 void notifyRecentTasksChanged() { 254 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Notify recent tasks changed"); 255 if (mListener == null) { 256 return; 257 } 258 try { 259 mListener.onRecentTasksChanged(); 260 } catch (RemoteException e) { 261 Slog.w(TAG, "Failed call notifyRecentTasksChanged", e); 262 } 263 } 264 265 /** 266 * Notify the running task listener that a task appeared on desktop environment. 267 */ notifyRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo)268 private void notifyRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) { 269 if (mListener == null || !mIsDesktopMode || taskInfo.realActivity == null) { 270 return; 271 } 272 try { 273 mListener.onRunningTaskAppeared(taskInfo); 274 } catch (RemoteException e) { 275 Slog.w(TAG, "Failed call onRunningTaskAppeared", e); 276 } 277 } 278 279 /** 280 * Notify the running task listener that a task was removed on desktop environment. 281 */ notifyRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo)282 private void notifyRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { 283 if (mListener == null || !mIsDesktopMode || taskInfo.realActivity == null) { 284 return; 285 } 286 try { 287 mListener.onRunningTaskVanished(taskInfo); 288 } catch (RemoteException e) { 289 Slog.w(TAG, "Failed call onRunningTaskVanished", e); 290 } 291 } 292 293 @VisibleForTesting registerRecentTasksListener(IRecentTasksListener listener)294 void registerRecentTasksListener(IRecentTasksListener listener) { 295 mListener = listener; 296 } 297 298 @VisibleForTesting unregisterRecentTasksListener()299 void unregisterRecentTasksListener() { 300 mListener = null; 301 } 302 303 @VisibleForTesting hasRecentTasksListener()304 boolean hasRecentTasksListener() { 305 return mListener != null; 306 } 307 308 @VisibleForTesting getRecentTasks(int maxNum, int flags, int userId)309 ArrayList<GroupedRecentTaskInfo> getRecentTasks(int maxNum, int flags, int userId) { 310 // Note: the returned task list is from the most-recent to least-recent order 311 final List<ActivityManager.RecentTaskInfo> rawList = mActivityTaskManager.getRecentTasks( 312 maxNum, flags, userId); 313 314 // Make a mapping of task id -> task info 315 final SparseArray<ActivityManager.RecentTaskInfo> rawMapping = new SparseArray<>(); 316 for (int i = 0; i < rawList.size(); i++) { 317 final ActivityManager.RecentTaskInfo taskInfo = rawList.get(i); 318 rawMapping.put(taskInfo.taskId, taskInfo); 319 } 320 321 ArrayList<ActivityManager.RecentTaskInfo> freeformTasks = new ArrayList<>(); 322 323 // Pull out the pairs as we iterate back in the list 324 ArrayList<GroupedRecentTaskInfo> recentTasks = new ArrayList<>(); 325 for (int i = 0; i < rawList.size(); i++) { 326 final ActivityManager.RecentTaskInfo taskInfo = rawList.get(i); 327 if (!rawMapping.contains(taskInfo.taskId)) { 328 // If it's not in the mapping, then it was already paired with another task 329 continue; 330 } 331 332 if (DesktopModeStatus.isProto2Enabled() && mDesktopModeTaskRepository.isPresent() 333 && mDesktopModeTaskRepository.get().isActiveTask(taskInfo.taskId)) { 334 // Freeform tasks will be added as a separate entry 335 freeformTasks.add(taskInfo); 336 continue; 337 } 338 339 final int pairedTaskId = mSplitTasks.get(taskInfo.taskId); 340 if (pairedTaskId != INVALID_TASK_ID && rawMapping.contains( 341 pairedTaskId)) { 342 final ActivityManager.RecentTaskInfo pairedTaskInfo = rawMapping.get(pairedTaskId); 343 rawMapping.remove(pairedTaskId); 344 recentTasks.add(GroupedRecentTaskInfo.forSplitTasks(taskInfo, pairedTaskInfo, 345 mTaskSplitBoundsMap.get(pairedTaskId))); 346 } else { 347 recentTasks.add(GroupedRecentTaskInfo.forSingleTask(taskInfo)); 348 } 349 } 350 351 // Add a special entry for freeform tasks 352 if (!freeformTasks.isEmpty()) { 353 recentTasks.add(0, GroupedRecentTaskInfo.forFreeformTasks( 354 freeformTasks.toArray(new ActivityManager.RecentTaskInfo[0]))); 355 } 356 357 return recentTasks; 358 } 359 360 /** 361 * Returns the top running leaf task. 362 */ 363 @Nullable getTopRunningTask()364 public ActivityManager.RunningTaskInfo getTopRunningTask() { 365 List<ActivityManager.RunningTaskInfo> tasks = mActivityTaskManager.getTasks(1, 366 false /* filterOnlyVisibleRecents */); 367 return tasks.isEmpty() ? null : tasks.get(0); 368 } 369 370 /** 371 * Find the background task that match the given component. 372 */ 373 @Nullable findTaskInBackground(ComponentName componentName, int userId)374 public ActivityManager.RecentTaskInfo findTaskInBackground(ComponentName componentName, 375 int userId) { 376 if (componentName == null) { 377 return null; 378 } 379 List<ActivityManager.RecentTaskInfo> tasks = mActivityTaskManager.getRecentTasks( 380 Integer.MAX_VALUE, ActivityManager.RECENT_IGNORE_UNAVAILABLE, 381 ActivityManager.getCurrentUser()); 382 for (int i = 0; i < tasks.size(); i++) { 383 final ActivityManager.RecentTaskInfo task = tasks.get(i); 384 if (task.isVisible) { 385 continue; 386 } 387 if (componentName.equals(task.baseIntent.getComponent()) && userId == task.userId) { 388 return task; 389 } 390 } 391 return null; 392 } 393 dump(@onNull PrintWriter pw, String prefix)394 public void dump(@NonNull PrintWriter pw, String prefix) { 395 final String innerPrefix = prefix + " "; 396 pw.println(prefix + TAG); 397 pw.println(prefix + " mListener=" + mListener); 398 pw.println(prefix + "Tasks:"); 399 ArrayList<GroupedRecentTaskInfo> recentTasks = getRecentTasks(Integer.MAX_VALUE, 400 ActivityManager.RECENT_IGNORE_UNAVAILABLE, ActivityManager.getCurrentUser()); 401 for (int i = 0; i < recentTasks.size(); i++) { 402 pw.println(innerPrefix + recentTasks.get(i)); 403 } 404 } 405 406 /** 407 * The interface for calls from outside the Shell, within the host process. 408 */ 409 @ExternalThread 410 private class RecentTasksImpl implements RecentTasks { 411 @Override getRecentTasks(int maxNum, int flags, int userId, Executor executor, Consumer<List<GroupedRecentTaskInfo>> callback)412 public void getRecentTasks(int maxNum, int flags, int userId, Executor executor, 413 Consumer<List<GroupedRecentTaskInfo>> callback) { 414 mMainExecutor.execute(() -> { 415 List<GroupedRecentTaskInfo> tasks = 416 RecentTasksController.this.getRecentTasks(maxNum, flags, userId); 417 executor.execute(() -> callback.accept(tasks)); 418 }); 419 } 420 } 421 422 423 /** 424 * The interface for calls from outside the host process. 425 */ 426 @BinderThread 427 private static class IRecentTasksImpl extends IRecentTasks.Stub 428 implements ExternalInterfaceBinder { 429 private RecentTasksController mController; 430 private final SingleInstanceRemoteListener<RecentTasksController, 431 IRecentTasksListener> mListener; 432 private final IRecentTasksListener mRecentTasksListener = new IRecentTasksListener.Stub() { 433 @Override 434 public void onRecentTasksChanged() throws RemoteException { 435 mListener.call(l -> l.onRecentTasksChanged()); 436 } 437 438 @Override 439 public void onRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) { 440 mListener.call(l -> l.onRunningTaskAppeared(taskInfo)); 441 } 442 443 @Override 444 public void onRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { 445 mListener.call(l -> l.onRunningTaskVanished(taskInfo)); 446 } 447 }; 448 IRecentTasksImpl(RecentTasksController controller)449 public IRecentTasksImpl(RecentTasksController controller) { 450 mController = controller; 451 mListener = new SingleInstanceRemoteListener<>(controller, 452 c -> c.registerRecentTasksListener(mRecentTasksListener), 453 c -> c.unregisterRecentTasksListener()); 454 } 455 456 /** 457 * Invalidates this instance, preventing future calls from updating the controller. 458 */ 459 @Override invalidate()460 public void invalidate() { 461 mController = null; 462 // Unregister the listener to ensure any registered binder death recipients are unlinked 463 mListener.unregister(); 464 } 465 466 @Override registerRecentTasksListener(IRecentTasksListener listener)467 public void registerRecentTasksListener(IRecentTasksListener listener) 468 throws RemoteException { 469 executeRemoteCallWithTaskPermission(mController, "registerRecentTasksListener", 470 (controller) -> mListener.register(listener)); 471 } 472 473 @Override unregisterRecentTasksListener(IRecentTasksListener listener)474 public void unregisterRecentTasksListener(IRecentTasksListener listener) 475 throws RemoteException { 476 executeRemoteCallWithTaskPermission(mController, "unregisterRecentTasksListener", 477 (controller) -> mListener.unregister()); 478 } 479 480 @Override getRecentTasks(int maxNum, int flags, int userId)481 public GroupedRecentTaskInfo[] getRecentTasks(int maxNum, int flags, int userId) 482 throws RemoteException { 483 if (mController == null) { 484 // The controller is already invalidated -- just return an empty task list for now 485 return new GroupedRecentTaskInfo[0]; 486 } 487 488 final GroupedRecentTaskInfo[][] out = new GroupedRecentTaskInfo[][]{null}; 489 executeRemoteCallWithTaskPermission(mController, "getRecentTasks", 490 (controller) -> out[0] = controller.getRecentTasks(maxNum, flags, userId) 491 .toArray(new GroupedRecentTaskInfo[0]), 492 true /* blocking */); 493 return out[0]; 494 } 495 496 @Override getRunningTasks(int maxNum)497 public ActivityManager.RunningTaskInfo[] getRunningTasks(int maxNum) { 498 final ActivityManager.RunningTaskInfo[][] tasks = 499 new ActivityManager.RunningTaskInfo[][] {null}; 500 executeRemoteCallWithTaskPermission(mController, "getRunningTasks", 501 (controller) -> tasks[0] = ActivityTaskManager.getInstance().getTasks(maxNum) 502 .toArray(new ActivityManager.RunningTaskInfo[0]), 503 true /* blocking */); 504 return tasks[0]; 505 } 506 507 @Override startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options, IApplicationThread appThread, IRecentsAnimationRunner listener)508 public void startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options, 509 IApplicationThread appThread, IRecentsAnimationRunner listener) { 510 if (mController.mTransitionHandler == null) { 511 Slog.e(TAG, "Used shell-transitions startRecentsTransition without" 512 + " shell-transitions"); 513 return; 514 } 515 executeRemoteCallWithTaskPermission(mController, "startRecentsTransition", 516 (controller) -> controller.mTransitionHandler.startRecentsTransition( 517 intent, fillIn, options, appThread, listener)); 518 } 519 } 520 } 521