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