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