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 21 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; 22 23 import android.app.ActivityManager; 24 import android.app.ActivityTaskManager; 25 import android.app.TaskInfo; 26 import android.content.Context; 27 import android.os.RemoteException; 28 import android.util.Slog; 29 import android.util.SparseArray; 30 import android.util.SparseIntArray; 31 32 import androidx.annotation.BinderThread; 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 import androidx.annotation.VisibleForTesting; 36 37 import com.android.wm.shell.common.RemoteCallable; 38 import com.android.wm.shell.common.ShellExecutor; 39 import com.android.wm.shell.common.SingleInstanceRemoteListener; 40 import com.android.wm.shell.common.TaskStackListenerCallback; 41 import com.android.wm.shell.common.TaskStackListenerImpl; 42 import com.android.wm.shell.common.annotations.ExternalThread; 43 import com.android.wm.shell.common.annotations.ShellMainThread; 44 import com.android.wm.shell.util.GroupedRecentTaskInfo; 45 import com.android.wm.shell.util.StagedSplitBounds; 46 47 import java.io.PrintWriter; 48 import java.util.ArrayList; 49 import java.util.HashMap; 50 import java.util.List; 51 import java.util.Map; 52 53 /** 54 * Manages the recent task list from the system, caching it as necessary. 55 */ 56 public class RecentTasksController implements TaskStackListenerCallback, 57 RemoteCallable<RecentTasksController> { 58 private static final String TAG = RecentTasksController.class.getSimpleName(); 59 60 private final Context mContext; 61 private final ShellExecutor mMainExecutor; 62 private final TaskStackListenerImpl mTaskStackListener; 63 private final RecentTasks mImpl = new RecentTasksImpl(); 64 65 private final ArrayList<Runnable> mCallbacks = new ArrayList<>(); 66 // Mapping of split task ids, mappings are symmetrical (ie. if t1 is the taskid of a task in a 67 // pair, then mSplitTasks[t1] = t2, and mSplitTasks[t2] = t1) 68 private final SparseIntArray mSplitTasks = new SparseIntArray(); 69 /** 70 * Maps taskId to {@link StagedSplitBounds} for both taskIDs. 71 * Meaning there will be two taskId integers mapping to the same object. 72 * If there's any ordering to the pairing than we can probably just get away with only one 73 * taskID mapping to it, leaving both for consistency with {@link #mSplitTasks} for now. 74 */ 75 private final Map<Integer, StagedSplitBounds> mTaskSplitBoundsMap = new HashMap<>(); 76 77 /** 78 * Creates {@link RecentTasksController}, returns {@code null} if the feature is not 79 * supported. 80 */ 81 @Nullable create( Context context, TaskStackListenerImpl taskStackListener, @ShellMainThread ShellExecutor mainExecutor )82 public static RecentTasksController create( 83 Context context, 84 TaskStackListenerImpl taskStackListener, 85 @ShellMainThread ShellExecutor mainExecutor 86 ) { 87 if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) { 88 return null; 89 } 90 return new RecentTasksController(context, taskStackListener, mainExecutor); 91 } 92 RecentTasksController(Context context, TaskStackListenerImpl taskStackListener, ShellExecutor mainExecutor)93 RecentTasksController(Context context, TaskStackListenerImpl taskStackListener, 94 ShellExecutor mainExecutor) { 95 mContext = context; 96 mTaskStackListener = taskStackListener; 97 mMainExecutor = mainExecutor; 98 } 99 asRecentTasks()100 public RecentTasks asRecentTasks() { 101 return mImpl; 102 } 103 init()104 public void init() { 105 mTaskStackListener.addListener(this); 106 } 107 108 /** 109 * Adds a split pair. This call does not validate the taskIds, only that they are not the same. 110 */ addSplitPair(int taskId1, int taskId2, StagedSplitBounds splitBounds)111 public void addSplitPair(int taskId1, int taskId2, StagedSplitBounds splitBounds) { 112 if (taskId1 == taskId2) { 113 return; 114 } 115 if (mSplitTasks.get(taskId1, INVALID_TASK_ID) == taskId2 116 && mTaskSplitBoundsMap.get(taskId1).equals(splitBounds)) { 117 // If the two tasks are already paired and the bounds are the same, then skip updating 118 return; 119 } 120 // Remove any previous pairs 121 removeSplitPair(taskId1); 122 removeSplitPair(taskId2); 123 mTaskSplitBoundsMap.remove(taskId1); 124 mTaskSplitBoundsMap.remove(taskId2); 125 126 mSplitTasks.put(taskId1, taskId2); 127 mSplitTasks.put(taskId2, taskId1); 128 mTaskSplitBoundsMap.put(taskId1, splitBounds); 129 mTaskSplitBoundsMap.put(taskId2, splitBounds); 130 notifyRecentTasksChanged(); 131 } 132 133 /** 134 * Removes a split pair. 135 */ removeSplitPair(int taskId)136 public void removeSplitPair(int taskId) { 137 int pairedTaskId = mSplitTasks.get(taskId, INVALID_TASK_ID); 138 if (pairedTaskId != INVALID_TASK_ID) { 139 mSplitTasks.delete(taskId); 140 mSplitTasks.delete(pairedTaskId); 141 mTaskSplitBoundsMap.remove(taskId); 142 mTaskSplitBoundsMap.remove(pairedTaskId); 143 notifyRecentTasksChanged(); 144 } 145 } 146 147 @Override getContext()148 public Context getContext() { 149 return mContext; 150 } 151 152 @Override getRemoteCallExecutor()153 public ShellExecutor getRemoteCallExecutor() { 154 return mMainExecutor; 155 } 156 157 @Override onTaskStackChanged()158 public void onTaskStackChanged() { 159 notifyRecentTasksChanged(); 160 } 161 162 @Override onRecentTaskListUpdated()163 public void onRecentTaskListUpdated() { 164 // In some cases immediately after booting, the tasks in the system recent task list may be 165 // loaded, but not in the active task hierarchy in the system. These tasks are displayed in 166 // overview, but removing them don't result in a onTaskStackChanged() nor a onTaskRemoved() 167 // callback (those are for changes to the active tasks), but the task list is still updated, 168 // so we should also invalidate the change id to ensure we load a new list instead of 169 // reusing a stale list. 170 notifyRecentTasksChanged(); 171 } 172 onTaskRemoved(TaskInfo taskInfo)173 public void onTaskRemoved(TaskInfo taskInfo) { 174 // Remove any split pairs associated with this task 175 removeSplitPair(taskInfo.taskId); 176 notifyRecentTasksChanged(); 177 } 178 onTaskWindowingModeChanged(TaskInfo taskInfo)179 public void onTaskWindowingModeChanged(TaskInfo taskInfo) { 180 notifyRecentTasksChanged(); 181 } 182 183 @VisibleForTesting notifyRecentTasksChanged()184 void notifyRecentTasksChanged() { 185 for (int i = 0; i < mCallbacks.size(); i++) { 186 mCallbacks.get(i).run(); 187 } 188 } 189 registerRecentTasksListener(Runnable listener)190 private void registerRecentTasksListener(Runnable listener) { 191 if (!mCallbacks.contains(listener)) { 192 mCallbacks.add(listener); 193 } 194 } 195 unregisterRecentTasksListener(Runnable listener)196 private void unregisterRecentTasksListener(Runnable listener) { 197 mCallbacks.remove(listener); 198 } 199 200 @VisibleForTesting getRawRecentTasks(int maxNum, int flags, int userId)201 List<ActivityManager.RecentTaskInfo> getRawRecentTasks(int maxNum, int flags, int userId) { 202 return ActivityTaskManager.getInstance().getRecentTasks(maxNum, flags, userId); 203 } 204 205 @VisibleForTesting getRecentTasks(int maxNum, int flags, int userId)206 ArrayList<GroupedRecentTaskInfo> getRecentTasks(int maxNum, int flags, int userId) { 207 // Note: the returned task list is from the most-recent to least-recent order 208 final List<ActivityManager.RecentTaskInfo> rawList = getRawRecentTasks(maxNum, flags, 209 userId); 210 211 // Make a mapping of task id -> task info 212 final SparseArray<ActivityManager.RecentTaskInfo> rawMapping = new SparseArray<>(); 213 for (int i = 0; i < rawList.size(); i++) { 214 final ActivityManager.RecentTaskInfo taskInfo = rawList.get(i); 215 rawMapping.put(taskInfo.taskId, taskInfo); 216 } 217 218 // Pull out the pairs as we iterate back in the list 219 ArrayList<GroupedRecentTaskInfo> recentTasks = new ArrayList<>(); 220 for (int i = 0; i < rawList.size(); i++) { 221 final ActivityManager.RecentTaskInfo taskInfo = rawList.get(i); 222 if (!rawMapping.contains(taskInfo.taskId)) { 223 // If it's not in the mapping, then it was already paired with another task 224 continue; 225 } 226 227 final int pairedTaskId = mSplitTasks.get(taskInfo.taskId); 228 if (pairedTaskId != INVALID_TASK_ID && rawMapping.contains(pairedTaskId)) { 229 final ActivityManager.RecentTaskInfo pairedTaskInfo = rawMapping.get(pairedTaskId); 230 rawMapping.remove(pairedTaskId); 231 recentTasks.add(new GroupedRecentTaskInfo(taskInfo, pairedTaskInfo, 232 mTaskSplitBoundsMap.get(pairedTaskId))); 233 } else { 234 recentTasks.add(new GroupedRecentTaskInfo(taskInfo)); 235 } 236 } 237 return recentTasks; 238 } 239 dump(@onNull PrintWriter pw, String prefix)240 public void dump(@NonNull PrintWriter pw, String prefix) { 241 final String innerPrefix = prefix + " "; 242 pw.println(prefix + TAG); 243 ArrayList<GroupedRecentTaskInfo> recentTasks = getRecentTasks(Integer.MAX_VALUE, 244 ActivityManager.RECENT_IGNORE_UNAVAILABLE, ActivityManager.getCurrentUser()); 245 for (int i = 0; i < recentTasks.size(); i++) { 246 pw.println(innerPrefix + recentTasks.get(i)); 247 } 248 } 249 250 /** 251 * The interface for calls from outside the Shell, within the host process. 252 */ 253 @ExternalThread 254 private class RecentTasksImpl implements RecentTasks { 255 private IRecentTasksImpl mIRecentTasks; 256 257 @Override createExternalInterface()258 public IRecentTasks createExternalInterface() { 259 if (mIRecentTasks != null) { 260 mIRecentTasks.invalidate(); 261 } 262 mIRecentTasks = new IRecentTasksImpl(RecentTasksController.this); 263 return mIRecentTasks; 264 } 265 } 266 267 268 /** 269 * The interface for calls from outside the host process. 270 */ 271 @BinderThread 272 private static class IRecentTasksImpl extends IRecentTasks.Stub { 273 private RecentTasksController mController; 274 private final SingleInstanceRemoteListener<RecentTasksController, 275 IRecentTasksListener> mListener; 276 private final Runnable mRecentTasksListener = 277 new Runnable() { 278 @Override 279 public void run() { 280 mListener.call(l -> l.onRecentTasksChanged()); 281 } 282 }; 283 IRecentTasksImpl(RecentTasksController controller)284 public IRecentTasksImpl(RecentTasksController controller) { 285 mController = controller; 286 mListener = new SingleInstanceRemoteListener<>(controller, 287 c -> c.registerRecentTasksListener(mRecentTasksListener), 288 c -> c.unregisterRecentTasksListener(mRecentTasksListener)); 289 } 290 291 /** 292 * Invalidates this instance, preventing future calls from updating the controller. 293 */ invalidate()294 void invalidate() { 295 mController = null; 296 } 297 298 @Override registerRecentTasksListener(IRecentTasksListener listener)299 public void registerRecentTasksListener(IRecentTasksListener listener) 300 throws RemoteException { 301 executeRemoteCallWithTaskPermission(mController, "registerRecentTasksListener", 302 (controller) -> mListener.register(listener)); 303 } 304 305 @Override unregisterRecentTasksListener(IRecentTasksListener listener)306 public void unregisterRecentTasksListener(IRecentTasksListener listener) 307 throws RemoteException { 308 executeRemoteCallWithTaskPermission(mController, "unregisterRecentTasksListener", 309 (controller) -> mListener.unregister()); 310 } 311 312 @Override getRecentTasks(int maxNum, int flags, int userId)313 public GroupedRecentTaskInfo[] getRecentTasks(int maxNum, int flags, int userId) 314 throws RemoteException { 315 if (mController == null) { 316 // The controller is already invalidated -- just return an empty task list for now 317 return new GroupedRecentTaskInfo[0]; 318 } 319 320 final GroupedRecentTaskInfo[][] out = new GroupedRecentTaskInfo[][]{null}; 321 executeRemoteCallWithTaskPermission(mController, "getRecentTasks", 322 (controller) -> out[0] = controller.getRecentTasks(maxNum, flags, userId) 323 .toArray(new GroupedRecentTaskInfo[0]), 324 true /* blocking */); 325 return out[0]; 326 } 327 } 328 }