1 /* 2 * Copyright (C) 2022 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 package com.android.quickstep; 17 18 import static android.app.ActivityTaskManager.INVALID_TASK_ID; 19 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; 20 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; 21 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; 22 import static android.content.Intent.ACTION_CHOOSER; 23 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; 24 import static android.view.Display.DEFAULT_DISPLAY; 25 26 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; 27 28 import android.annotation.UserIdInt; 29 import android.app.ActivityManager.RunningTaskInfo; 30 import android.content.Context; 31 import android.util.Log; 32 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 import androidx.annotation.UiThread; 36 37 import com.android.launcher3.util.MainThreadInitializedObject; 38 import com.android.launcher3.util.SafeCloseable; 39 import com.android.launcher3.util.SplitConfigurationOptions; 40 import com.android.launcher3.util.SplitConfigurationOptions.SplitStageInfo; 41 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; 42 import com.android.launcher3.util.SplitConfigurationOptions.StageType; 43 import com.android.launcher3.util.TraceHelper; 44 import com.android.systemui.shared.recents.model.Task; 45 import com.android.systemui.shared.recents.model.Task.TaskKey; 46 import com.android.systemui.shared.system.ActivityManagerWrapper; 47 import com.android.systemui.shared.system.TaskStackChangeListener; 48 import com.android.systemui.shared.system.TaskStackChangeListeners; 49 import com.android.wm.shell.splitscreen.ISplitScreenListener; 50 51 import java.io.PrintWriter; 52 import java.util.ArrayList; 53 import java.util.Arrays; 54 import java.util.Collections; 55 import java.util.Iterator; 56 import java.util.LinkedList; 57 import java.util.List; 58 59 /** 60 * This class tracked the top-most task and some 'approximate' task history to allow faster 61 * system state estimation during touch interaction 62 */ 63 public class TopTaskTracker extends ISplitScreenListener.Stub 64 implements TaskStackChangeListener, SafeCloseable { 65 66 private static final String TAG = "TopTaskTracker"; 67 68 private static final boolean DEBUG = true; 69 70 public static MainThreadInitializedObject<TopTaskTracker> INSTANCE = 71 new MainThreadInitializedObject<>(TopTaskTracker::new); 72 73 private static final int HISTORY_SIZE = 5; 74 75 // Ordered list with first item being the most recent task. 76 private final LinkedList<RunningTaskInfo> mOrderedTaskList = new LinkedList<>(); 77 78 private final Context mContext; 79 private final SplitStageInfo mMainStagePosition = new SplitStageInfo(); 80 private final SplitStageInfo mSideStagePosition = new SplitStageInfo(); 81 private int mPinnedTaskId = INVALID_TASK_ID; 82 TopTaskTracker(Context context)83 private TopTaskTracker(Context context) { 84 mContext = context; 85 mMainStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_MAIN; 86 mSideStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_SIDE; 87 88 TaskStackChangeListeners.getInstance().registerTaskStackListener(this); 89 SystemUiProxy.INSTANCE.get(context).registerSplitScreenListener(this); 90 } 91 92 @Override close()93 public void close() { 94 TaskStackChangeListeners.getInstance().unregisterTaskStackListener(this); 95 SystemUiProxy.INSTANCE.get(mContext).unregisterSplitScreenListener(this); 96 } 97 98 @Override onTaskRemoved(int taskId)99 public void onTaskRemoved(int taskId) { 100 mOrderedTaskList.removeIf(rto -> rto.taskId == taskId); 101 if (DEBUG) { 102 Log.i(TAG, "onTaskRemoved: taskId=" + taskId); 103 } 104 } 105 106 @Override onTaskMovedToFront(RunningTaskInfo taskInfo)107 public void onTaskMovedToFront(RunningTaskInfo taskInfo) { 108 if (!mOrderedTaskList.isEmpty() 109 && mOrderedTaskList.getFirst().taskId != taskInfo.taskId 110 && DEBUG) { 111 Log.i(TAG, "onTaskMovedToFront: (moved taskInfo to front) taskId=" + taskInfo.taskId 112 + ", baseIntent=" + taskInfo.baseIntent); 113 } 114 mOrderedTaskList.removeIf(rto -> rto.taskId == taskInfo.taskId); 115 mOrderedTaskList.addFirst(taskInfo); 116 117 // Keep the home display's top running task in the first while adding a non-home 118 // display's task to the list, to avoid showing non-home display's task upon going to 119 // Recents animation. 120 if (taskInfo.displayId != DEFAULT_DISPLAY) { 121 final RunningTaskInfo topTaskOnHomeDisplay = mOrderedTaskList.stream() 122 .filter(rto -> rto.displayId == DEFAULT_DISPLAY).findFirst().orElse(null); 123 if (topTaskOnHomeDisplay != null) { 124 if (DEBUG) { 125 Log.i(TAG, "onTaskMovedToFront: (removing top task on home display) taskId=" 126 + topTaskOnHomeDisplay.taskId 127 + ", baseIntent=" + topTaskOnHomeDisplay.baseIntent); 128 } 129 mOrderedTaskList.removeIf(rto -> rto.taskId == topTaskOnHomeDisplay.taskId); 130 mOrderedTaskList.addFirst(topTaskOnHomeDisplay); 131 } 132 } 133 134 if (mOrderedTaskList.size() >= HISTORY_SIZE) { 135 // If we grow in size, remove the last taskInfo which is not part of the split task. 136 Iterator<RunningTaskInfo> itr = mOrderedTaskList.descendingIterator(); 137 while (itr.hasNext()) { 138 RunningTaskInfo info = itr.next(); 139 if (info.taskId != taskInfo.taskId 140 && info.taskId != mMainStagePosition.taskId 141 && info.taskId != mSideStagePosition.taskId) { 142 if (DEBUG) { 143 Log.i(TAG, "onTaskMovedToFront: (removing task list overflow) taskId=" 144 + taskInfo.taskId + ", baseIntent=" + taskInfo.baseIntent); 145 } 146 itr.remove(); 147 return; 148 } 149 } 150 } 151 } 152 153 @Override onStagePositionChanged(@tageType int stage, @StagePosition int position)154 public void onStagePositionChanged(@StageType int stage, @StagePosition int position) { 155 if (DEBUG) { 156 Log.i(TAG, "onStagePositionChanged: stage=" + stage + ", position=" + position); 157 } 158 if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN) { 159 mMainStagePosition.stagePosition = position; 160 } else { 161 mSideStagePosition.stagePosition = position; 162 } 163 } 164 165 @Override onTaskStageChanged(int taskId, @StageType int stage, boolean visible)166 public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) { 167 if (DEBUG) { 168 Log.i(TAG, "onTaskStageChanged: taskId=" + taskId 169 + ", stage=" + stage + ", visible=" + visible); 170 } 171 // If a task is not visible anymore or has been moved to undefined, stop tracking it. 172 if (!visible || stage == SplitConfigurationOptions.STAGE_TYPE_UNDEFINED) { 173 if (mMainStagePosition.taskId == taskId) { 174 mMainStagePosition.taskId = INVALID_TASK_ID; 175 } else if (mSideStagePosition.taskId == taskId) { 176 mSideStagePosition.taskId = INVALID_TASK_ID; 177 } // else it's an un-tracked child 178 return; 179 } 180 181 if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN) { 182 mMainStagePosition.taskId = taskId; 183 } else { 184 mSideStagePosition.taskId = taskId; 185 } 186 } 187 188 @Override onActivityPinned(String packageName, int userId, int taskId, int stackId)189 public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { 190 if (DEBUG) { 191 Log.i(TAG, "onActivityPinned: packageName=" + packageName 192 + ", userId=" + userId + ", stackId=" + stackId); 193 } 194 mPinnedTaskId = taskId; 195 } 196 197 @Override onActivityUnpinned()198 public void onActivityUnpinned() { 199 if (DEBUG) { 200 Log.i(TAG, "onActivityUnpinned"); 201 } 202 mPinnedTaskId = INVALID_TASK_ID; 203 } 204 205 /** 206 * @return index 0 will be task in left/top position, index 1 in right/bottom position. 207 * Will return empty array if device is not in staged split 208 */ getRunningSplitTaskIds()209 public int[] getRunningSplitTaskIds() { 210 if (mMainStagePosition.taskId == INVALID_TASK_ID 211 || mSideStagePosition.taskId == INVALID_TASK_ID) { 212 return new int[]{}; 213 } 214 int[] out = new int[2]; 215 if (mMainStagePosition.stagePosition == STAGE_POSITION_TOP_OR_LEFT) { 216 out[0] = mMainStagePosition.taskId; 217 out[1] = mSideStagePosition.taskId; 218 } else { 219 out[1] = mMainStagePosition.taskId; 220 out[0] = mSideStagePosition.taskId; 221 } 222 return out; 223 } 224 225 226 /** 227 * Returns the CachedTaskInfo for the top most task 228 */ 229 @NonNull 230 @UiThread getCachedTopTask(boolean filterOnlyVisibleRecents)231 public CachedTaskInfo getCachedTopTask(boolean filterOnlyVisibleRecents) { 232 if (filterOnlyVisibleRecents) { 233 // Since we only know about the top most task, any filtering may not be applied on the 234 // cache. The second to top task may change while the top task is still the same. 235 RunningTaskInfo[] tasks = TraceHelper.allowIpcs("getCachedTopTask.true", () -> 236 ActivityManagerWrapper.getInstance().getRunningTasks(true)); 237 return new CachedTaskInfo(Arrays.asList(tasks)); 238 } 239 240 if (mOrderedTaskList.isEmpty()) { 241 RunningTaskInfo[] tasks = TraceHelper.allowIpcs("getCachedTopTask.false", () -> 242 ActivityManagerWrapper.getInstance().getRunningTasks( 243 false /* filterOnlyVisibleRecents */)); 244 Collections.addAll(mOrderedTaskList, tasks); 245 } 246 247 // Strip the pinned task 248 ArrayList<RunningTaskInfo> tasks = new ArrayList<>(mOrderedTaskList); 249 tasks.removeIf(t -> t.taskId == mPinnedTaskId); 250 return new CachedTaskInfo(tasks); 251 } 252 dump(String prefix, PrintWriter writer)253 public void dump(String prefix, PrintWriter writer) { 254 writer.println(prefix + "TopTaskTracker:"); 255 256 writer.println(prefix + "\tmOrderedTaskList=["); 257 for (RunningTaskInfo taskInfo : mOrderedTaskList) { 258 writer.println(prefix + "\t\t(taskId=" + taskInfo.taskId 259 + "; baseIntent=" + taskInfo.baseIntent 260 + "; isRunning=" + taskInfo.isRunning + ")"); 261 } 262 writer.println(prefix + "\t]"); 263 writer.println(prefix + "\tmMainStagePosition=" + mMainStagePosition); 264 writer.println(prefix + "\tmSideStagePosition=" + mSideStagePosition); 265 writer.println(prefix + "\tmPinnedTaskId=" + mPinnedTaskId); 266 } 267 268 /** 269 * Class to provide information about a task which can be safely cached and do not change 270 * during the lifecycle of the task. 271 */ 272 public static class CachedTaskInfo { 273 274 @Nullable 275 private final RunningTaskInfo mTopTask; 276 public final List<RunningTaskInfo> mAllCachedTasks; 277 CachedTaskInfo(List<RunningTaskInfo> allCachedTasks)278 CachedTaskInfo(List<RunningTaskInfo> allCachedTasks) { 279 mAllCachedTasks = allCachedTasks; 280 mTopTask = allCachedTasks.isEmpty() ? null : allCachedTasks.get(0); 281 } 282 getTaskId()283 public int getTaskId() { 284 return mTopTask == null ? INVALID_TASK_ID : mTopTask.taskId; 285 } 286 287 /** 288 * Returns true if the root of the task chooser activity 289 */ isRootChooseActivity()290 public boolean isRootChooseActivity() { 291 return mTopTask != null && ACTION_CHOOSER.equals(mTopTask.baseIntent.getAction()); 292 } 293 294 /** 295 * If the given task holds an activity that is excluded from recents, and there 296 * is another running task that is not excluded from recents, returns that underlying task. 297 */ getVisibleNonExcludedTask()298 public @Nullable CachedTaskInfo getVisibleNonExcludedTask() { 299 if (mTopTask == null 300 || (mTopTask.baseIntent.getFlags() & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) == 0) { 301 // Not an excluded task. 302 return null; 303 } 304 List<RunningTaskInfo> visibleNonExcludedTasks = mAllCachedTasks.stream() 305 .filter(t -> t.isVisible 306 && (t.baseIntent.getFlags() & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) == 0 307 && t.getActivityType() != ACTIVITY_TYPE_HOME 308 && t.getActivityType() != ACTIVITY_TYPE_RECENTS) 309 .toList(); 310 return visibleNonExcludedTasks.isEmpty() ? null 311 : new CachedTaskInfo(visibleNonExcludedTasks); 312 } 313 314 /** 315 * Returns true if this represents the HOME task 316 */ isHomeTask()317 public boolean isHomeTask() { 318 return mTopTask != null && mTopTask.configuration.windowConfiguration 319 .getActivityType() == ACTIVITY_TYPE_HOME; 320 } 321 isRecentsTask()322 public boolean isRecentsTask() { 323 return mTopTask != null && mTopTask.configuration.windowConfiguration 324 .getActivityType() == ACTIVITY_TYPE_RECENTS; 325 } 326 327 /** 328 * Returns {@code true} if this task windowing mode is set to {@link 329 * android.app.WindowConfiguration#WINDOWING_MODE_FREEFORM} 330 */ isFreeformTask()331 public boolean isFreeformTask() { 332 return mTopTask != null && mTopTask.configuration.windowConfiguration.getWindowingMode() 333 == WINDOWING_MODE_FREEFORM; 334 } 335 336 /** 337 * Returns {@link Task} array which can be used as a placeholder until the true object 338 * is loaded by the model 339 */ getPlaceholderTasks()340 public Task[] getPlaceholderTasks() { 341 return mTopTask == null ? new Task[0] 342 : new Task[]{Task.from(new TaskKey(mTopTask), mTopTask, false)}; 343 } 344 345 /** 346 * Returns {@link Task} array corresponding to the provided task ids which can be used as a 347 * placeholder until the true object is loaded by the model 348 */ getPlaceholderTasks(int[] taskIds)349 public Task[] getPlaceholderTasks(int[] taskIds) { 350 if (mTopTask == null) { 351 return new Task[0]; 352 } 353 Task[] result = new Task[taskIds.length]; 354 for (int i = 0; i < taskIds.length; i++) { 355 final int index = i; 356 int taskId = taskIds[i]; 357 mAllCachedTasks.forEach(rti -> { 358 if (rti.taskId == taskId) { 359 result[index] = Task.from(new TaskKey(rti), rti, false); 360 } 361 }); 362 } 363 return result; 364 } 365 366 @UserIdInt 367 @Nullable getUserId()368 public Integer getUserId() { 369 return mTopTask == null ? null : mTopTask.userId; 370 } 371 372 @Nullable getPackageName()373 public String getPackageName() { 374 if (mTopTask == null || mTopTask.baseActivity == null) { 375 return null; 376 } 377 return mTopTask.baseActivity.getPackageName(); 378 } 379 } 380 } 381