| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.quickstep; |
| |
| import static android.app.ActivityTaskManager.INVALID_TASK_ID; |
| import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; |
| import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; |
| import static android.content.Intent.ACTION_CHOOSER; |
| import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; |
| import static android.view.Display.DEFAULT_DISPLAY; |
| |
| import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; |
| |
| import android.annotation.UserIdInt; |
| import android.app.ActivityManager.RunningTaskInfo; |
| import android.content.Context; |
| |
| import androidx.annotation.Nullable; |
| import androidx.annotation.UiThread; |
| |
| import com.android.launcher3.util.MainThreadInitializedObject; |
| import com.android.launcher3.util.SplitConfigurationOptions; |
| import com.android.launcher3.util.SplitConfigurationOptions.SplitStageInfo; |
| import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; |
| import com.android.launcher3.util.SplitConfigurationOptions.StageType; |
| import com.android.launcher3.util.TraceHelper; |
| import com.android.systemui.shared.recents.model.Task; |
| import com.android.systemui.shared.recents.model.Task.TaskKey; |
| import com.android.systemui.shared.system.ActivityManagerWrapper; |
| import com.android.systemui.shared.system.TaskStackChangeListener; |
| import com.android.systemui.shared.system.TaskStackChangeListeners; |
| import com.android.wm.shell.splitscreen.ISplitScreenListener; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| |
| /** |
| * This class tracked the top-most task and some 'approximate' task history to allow faster |
| * system state estimation during touch interaction |
| */ |
| public class TopTaskTracker extends ISplitScreenListener.Stub implements TaskStackChangeListener { |
| |
| public static MainThreadInitializedObject<TopTaskTracker> INSTANCE = |
| new MainThreadInitializedObject<>(TopTaskTracker::new); |
| |
| private static final int HISTORY_SIZE = 5; |
| |
| // Ordered list with first item being the most recent task. |
| private final LinkedList<RunningTaskInfo> mOrderedTaskList = new LinkedList<>(); |
| |
| |
| private final SplitStageInfo mMainStagePosition = new SplitStageInfo(); |
| private final SplitStageInfo mSideStagePosition = new SplitStageInfo(); |
| private int mPinnedTaskId = INVALID_TASK_ID; |
| |
| private TopTaskTracker(Context context) { |
| mMainStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_MAIN; |
| mSideStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_SIDE; |
| |
| TaskStackChangeListeners.getInstance().registerTaskStackListener(this); |
| SystemUiProxy.INSTANCE.get(context).registerSplitScreenListener(this); |
| } |
| |
| @Override |
| public void onTaskRemoved(int taskId) { |
| mOrderedTaskList.removeIf(rto -> rto.taskId == taskId); |
| } |
| |
| @Override |
| public void onTaskMovedToFront(RunningTaskInfo taskInfo) { |
| mOrderedTaskList.removeIf(rto -> rto.taskId == taskInfo.taskId); |
| mOrderedTaskList.addFirst(taskInfo); |
| |
| // Keep the home display's top running task in the first while adding a non-home |
| // display's task to the list, to avoid showing non-home display's task upon going to |
| // Recents animation. |
| if (taskInfo.displayId != DEFAULT_DISPLAY) { |
| final RunningTaskInfo topTaskOnHomeDisplay = mOrderedTaskList.stream() |
| .filter(rto -> rto.displayId == DEFAULT_DISPLAY).findFirst().orElse(null); |
| if (topTaskOnHomeDisplay != null) { |
| mOrderedTaskList.removeIf(rto -> rto.taskId == topTaskOnHomeDisplay.taskId); |
| mOrderedTaskList.addFirst(topTaskOnHomeDisplay); |
| } |
| } |
| |
| if (mOrderedTaskList.size() >= HISTORY_SIZE) { |
| // If we grow in size, remove the last taskInfo which is not part of the split task. |
| Iterator<RunningTaskInfo> itr = mOrderedTaskList.descendingIterator(); |
| while (itr.hasNext()) { |
| RunningTaskInfo info = itr.next(); |
| if (info.taskId != taskInfo.taskId |
| && info.taskId != mMainStagePosition.taskId |
| && info.taskId != mSideStagePosition.taskId) { |
| itr.remove(); |
| return; |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void onStagePositionChanged(@StageType int stage, @StagePosition int position) { |
| if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN) { |
| mMainStagePosition.stagePosition = position; |
| } else { |
| mSideStagePosition.stagePosition = position; |
| } |
| } |
| |
| @Override |
| public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) { |
| // If task is not visible but we are tracking it, stop tracking it |
| if (!visible) { |
| if (mMainStagePosition.taskId == taskId) { |
| resetTaskId(mMainStagePosition); |
| } else if (mSideStagePosition.taskId == taskId) { |
| resetTaskId(mSideStagePosition); |
| } // else it's an un-tracked child |
| return; |
| } |
| |
| // If stage has moved to undefined, stop tracking the task |
| if (stage == SplitConfigurationOptions.STAGE_TYPE_UNDEFINED) { |
| resetTaskId(taskId == mMainStagePosition.taskId |
| ? mMainStagePosition : mSideStagePosition); |
| return; |
| } |
| |
| if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN) { |
| mMainStagePosition.taskId = taskId; |
| } else { |
| mSideStagePosition.taskId = taskId; |
| } |
| } |
| |
| @Override |
| public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { |
| mPinnedTaskId = taskId; |
| } |
| |
| @Override |
| public void onActivityUnpinned() { |
| mPinnedTaskId = INVALID_TASK_ID; |
| } |
| |
| private void resetTaskId(SplitStageInfo taskPosition) { |
| taskPosition.taskId = -1; |
| } |
| |
| /** |
| * @return index 0 will be task in left/top position, index 1 in right/bottom position. |
| * Will return empty array if device is not in staged split |
| */ |
| public int[] getRunningSplitTaskIds() { |
| if (mMainStagePosition.taskId == INVALID_TASK_ID |
| || mSideStagePosition.taskId == INVALID_TASK_ID) { |
| return new int[]{}; |
| } |
| int[] out = new int[2]; |
| if (mMainStagePosition.stagePosition == STAGE_POSITION_TOP_OR_LEFT) { |
| out[0] = mMainStagePosition.taskId; |
| out[1] = mSideStagePosition.taskId; |
| } else { |
| out[1] = mMainStagePosition.taskId; |
| out[0] = mSideStagePosition.taskId; |
| } |
| return out; |
| } |
| |
| |
| /** |
| * Returns the CachedTaskInfo for the top most task |
| */ |
| @UiThread |
| public CachedTaskInfo getCachedTopTask(boolean filterOnlyVisibleRecents) { |
| if (filterOnlyVisibleRecents) { |
| // Since we only know about the top most task, any filtering may not be applied on the |
| // cache. The second to top task may change while the top task is still the same. |
| RunningTaskInfo[] tasks = TraceHelper.allowIpcs("getCachedTopTask.true", () -> |
| ActivityManagerWrapper.getInstance().getRunningTasks(true)); |
| return new CachedTaskInfo(Arrays.asList(tasks)); |
| } |
| |
| if (mOrderedTaskList.isEmpty()) { |
| RunningTaskInfo[] tasks = TraceHelper.allowIpcs("getCachedTopTask.false", () -> |
| ActivityManagerWrapper.getInstance().getRunningTasks( |
| false /* filterOnlyVisibleRecents */)); |
| Collections.addAll(mOrderedTaskList, tasks); |
| } |
| |
| // Strip the pinned task |
| ArrayList<RunningTaskInfo> tasks = new ArrayList<>(mOrderedTaskList); |
| tasks.removeIf(t -> t.taskId == mPinnedTaskId); |
| return new CachedTaskInfo(tasks); |
| } |
| |
| /** |
| * Class to provide information about a task which can be safely cached and do not change |
| * during the lifecycle of the task. |
| */ |
| public static class CachedTaskInfo { |
| |
| @Nullable |
| private final RunningTaskInfo mTopTask; |
| private final List<RunningTaskInfo> mAllCachedTasks; |
| |
| CachedTaskInfo(List<RunningTaskInfo> allCachedTasks) { |
| mAllCachedTasks = allCachedTasks; |
| mTopTask = allCachedTasks.isEmpty() ? null : allCachedTasks.get(0); |
| } |
| |
| public int getTaskId() { |
| return mTopTask == null ? INVALID_TASK_ID : mTopTask.taskId; |
| } |
| |
| /** |
| * Returns true if the root of the task chooser activity |
| */ |
| public boolean isRootChooseActivity() { |
| return mTopTask != null && ACTION_CHOOSER.equals(mTopTask.baseIntent.getAction()); |
| } |
| |
| /** |
| * Returns true if the given task holds an Assistant activity that is excluded from recents |
| */ |
| public boolean isExcludedAssistant() { |
| return mTopTask != null && mTopTask.configuration.windowConfiguration |
| .getActivityType() == ACTIVITY_TYPE_ASSISTANT |
| && (mTopTask.baseIntent.getFlags() & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0; |
| } |
| |
| /** |
| * Returns true if this represents the HOME task |
| */ |
| public boolean isHomeTask() { |
| return mTopTask != null && mTopTask.configuration.windowConfiguration |
| .getActivityType() == ACTIVITY_TYPE_HOME; |
| } |
| |
| /** |
| * Returns {@link Task} array which can be used as a placeholder until the true object |
| * is loaded by the model |
| */ |
| public Task[] getPlaceholderTasks() { |
| return mTopTask == null ? new Task[0] |
| : new Task[] {Task.from(new TaskKey(mTopTask), mTopTask, false)}; |
| } |
| |
| /** |
| * Returns {@link Task} array corresponding to the provided task ids which can be used as a |
| * placeholder until the true object is loaded by the model |
| */ |
| public Task[] getPlaceholderTasks(int[] taskIds) { |
| if (mTopTask == null) { |
| return new Task[0]; |
| } |
| Task[] result = new Task[taskIds.length]; |
| for (int i = 0; i < taskIds.length; i++) { |
| final int index = i; |
| int taskId = taskIds[i]; |
| mAllCachedTasks.forEach(rti -> { |
| if (rti.taskId == taskId) { |
| result[index] = Task.from(new TaskKey(rti), rti, false); |
| } |
| }); |
| } |
| return result; |
| } |
| |
| @UserIdInt |
| @Nullable |
| public Integer getUserId() { |
| return mTopTask == null ? null : mTopTask.userId; |
| } |
| |
| @Nullable |
| public String getPackageName() { |
| return mTopTask == null ? null : mTopTask.baseActivity.getPackageName(); |
| } |
| } |
| } |