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_ASSISTANT; 20 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; 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 task is not visible but we are tracking it, stop tracking it 131 if (!visible) { 132 if (mMainStagePosition.taskId == taskId) { 133 resetTaskId(mMainStagePosition); 134 } else if (mSideStagePosition.taskId == taskId) { 135 resetTaskId(mSideStagePosition); 136 } // else it's an un-tracked child 137 return; 138 } 139 140 // If stage has moved to undefined, stop tracking the task 141 if (stage == SplitConfigurationOptions.STAGE_TYPE_UNDEFINED) { 142 resetTaskId(taskId == mMainStagePosition.taskId 143 ? mMainStagePosition : mSideStagePosition); 144 return; 145 } 146 147 if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN) { 148 mMainStagePosition.taskId = taskId; 149 } else { 150 mSideStagePosition.taskId = taskId; 151 } 152 } 153 154 @Override onActivityPinned(String packageName, int userId, int taskId, int stackId)155 public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { 156 mPinnedTaskId = taskId; 157 } 158 159 @Override onActivityUnpinned()160 public void onActivityUnpinned() { 161 mPinnedTaskId = INVALID_TASK_ID; 162 } 163 resetTaskId(SplitStageInfo taskPosition)164 private void resetTaskId(SplitStageInfo taskPosition) { 165 taskPosition.taskId = -1; 166 } 167 168 /** 169 * @return index 0 will be task in left/top position, index 1 in right/bottom position. 170 * Will return empty array if device is not in staged split 171 */ getRunningSplitTaskIds()172 public int[] getRunningSplitTaskIds() { 173 if (mMainStagePosition.taskId == INVALID_TASK_ID 174 || mSideStagePosition.taskId == INVALID_TASK_ID) { 175 return new int[]{}; 176 } 177 int[] out = new int[2]; 178 if (mMainStagePosition.stagePosition == STAGE_POSITION_TOP_OR_LEFT) { 179 out[0] = mMainStagePosition.taskId; 180 out[1] = mSideStagePosition.taskId; 181 } else { 182 out[1] = mMainStagePosition.taskId; 183 out[0] = mSideStagePosition.taskId; 184 } 185 return out; 186 } 187 188 189 /** 190 * Returns the CachedTaskInfo for the top most task 191 */ 192 @UiThread getCachedTopTask(boolean filterOnlyVisibleRecents)193 public CachedTaskInfo getCachedTopTask(boolean filterOnlyVisibleRecents) { 194 if (filterOnlyVisibleRecents) { 195 // Since we only know about the top most task, any filtering may not be applied on the 196 // cache. The second to top task may change while the top task is still the same. 197 RunningTaskInfo[] tasks = TraceHelper.allowIpcs("getCachedTopTask.true", () -> 198 ActivityManagerWrapper.getInstance().getRunningTasks(true)); 199 return new CachedTaskInfo(Arrays.asList(tasks)); 200 } 201 202 if (mOrderedTaskList.isEmpty()) { 203 RunningTaskInfo[] tasks = TraceHelper.allowIpcs("getCachedTopTask.false", () -> 204 ActivityManagerWrapper.getInstance().getRunningTasks( 205 false /* filterOnlyVisibleRecents */)); 206 Collections.addAll(mOrderedTaskList, tasks); 207 } 208 209 // Strip the pinned task 210 ArrayList<RunningTaskInfo> tasks = new ArrayList<>(mOrderedTaskList); 211 tasks.removeIf(t -> t.taskId == mPinnedTaskId); 212 return new CachedTaskInfo(tasks); 213 } 214 215 /** 216 * Class to provide information about a task which can be safely cached and do not change 217 * during the lifecycle of the task. 218 */ 219 public static class CachedTaskInfo { 220 221 @Nullable 222 private final RunningTaskInfo mTopTask; 223 private final List<RunningTaskInfo> mAllCachedTasks; 224 CachedTaskInfo(List<RunningTaskInfo> allCachedTasks)225 CachedTaskInfo(List<RunningTaskInfo> allCachedTasks) { 226 mAllCachedTasks = allCachedTasks; 227 mTopTask = allCachedTasks.isEmpty() ? null : allCachedTasks.get(0); 228 } 229 getTaskId()230 public int getTaskId() { 231 return mTopTask == null ? INVALID_TASK_ID : mTopTask.taskId; 232 } 233 234 /** 235 * Returns true if the root of the task chooser activity 236 */ isRootChooseActivity()237 public boolean isRootChooseActivity() { 238 return mTopTask != null && ACTION_CHOOSER.equals(mTopTask.baseIntent.getAction()); 239 } 240 241 /** 242 * Returns true if the given task holds an Assistant activity that is excluded from recents 243 */ isExcludedAssistant()244 public boolean isExcludedAssistant() { 245 return mTopTask != null && mTopTask.configuration.windowConfiguration 246 .getActivityType() == ACTIVITY_TYPE_ASSISTANT 247 && (mTopTask.baseIntent.getFlags() & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0; 248 } 249 250 /** 251 * Returns true if this represents the HOME task 252 */ isHomeTask()253 public boolean isHomeTask() { 254 return mTopTask != null && mTopTask.configuration.windowConfiguration 255 .getActivityType() == ACTIVITY_TYPE_HOME; 256 } 257 258 /** 259 * Returns {@code true} if this task windowing mode is set to {@link 260 * android.app.WindowConfiguration#WINDOWING_MODE_FREEFORM} 261 */ isFreeformTask()262 public boolean isFreeformTask() { 263 return mTopTask != null && mTopTask.configuration.windowConfiguration.getWindowingMode() 264 == WINDOWING_MODE_FREEFORM; 265 } 266 267 /** 268 * Returns {@link Task} array which can be used as a placeholder until the true object 269 * is loaded by the model 270 */ getPlaceholderTasks()271 public Task[] getPlaceholderTasks() { 272 return mTopTask == null ? new Task[0] 273 : new Task[] {Task.from(new TaskKey(mTopTask), mTopTask, false)}; 274 } 275 276 /** 277 * Returns {@link Task} array corresponding to the provided task ids which can be used as a 278 * placeholder until the true object is loaded by the model 279 */ getPlaceholderTasks(int[] taskIds)280 public Task[] getPlaceholderTasks(int[] taskIds) { 281 if (mTopTask == null) { 282 return new Task[0]; 283 } 284 Task[] result = new Task[taskIds.length]; 285 for (int i = 0; i < taskIds.length; i++) { 286 final int index = i; 287 int taskId = taskIds[i]; 288 mAllCachedTasks.forEach(rti -> { 289 if (rti.taskId == taskId) { 290 result[index] = Task.from(new TaskKey(rti), rti, false); 291 } 292 }); 293 } 294 return result; 295 } 296 297 @UserIdInt 298 @Nullable getUserId()299 public Integer getUserId() { 300 return mTopTask == null ? null : mTopTask.userId; 301 } 302 303 @Nullable getPackageName()304 public String getPackageName() { 305 if (mTopTask == null || mTopTask.baseActivity == null) { 306 return null; 307 } 308 return mTopTask.baseActivity.getPackageName(); 309 } 310 } 311 } 312