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