/* * Copyright (C) 2014 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.systemui.recents.model; import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT; import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT; import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID; import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID; import static android.view.WindowManager.DOCKED_BOTTOM; import static android.view.WindowManager.DOCKED_INVALID; import static android.view.WindowManager.DOCKED_LEFT; import static android.view.WindowManager.DOCKED_RIGHT; import static android.view.WindowManager.DOCKED_TOP; import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.annotation.IntDef; import android.content.ComponentName; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.ColorDrawable; import android.util.ArrayMap; import android.util.ArraySet; import android.util.IntProperty; import android.util.SparseArray; import android.view.animation.Interpolator; import com.android.internal.policy.DockedDividerUtils; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.recents.Recents; import com.android.systemui.recents.RecentsDebugFlags; import com.android.systemui.recents.misc.NamedCounter; import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.recents.misc.Utilities; import com.android.systemui.recents.views.AnimationProps; import com.android.systemui.recents.views.DropTarget; import com.android.systemui.recents.views.TaskStackLayoutAlgorithm; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Random; /** * An interface for a task filter to query whether a particular task should show in a stack. */ interface TaskFilter { /** Returns whether the filter accepts the specified task */ public boolean acceptTask(SparseArray taskIdMap, Task t, int index); } /** * A list of filtered tasks. */ class FilteredTaskList { ArrayList mTasks = new ArrayList<>(); ArrayList mFilteredTasks = new ArrayList<>(); ArrayMap mTaskIndices = new ArrayMap<>(); TaskFilter mFilter; /** Sets the task filter, saving the current touch state */ boolean setFilter(TaskFilter filter) { ArrayList prevFilteredTasks = new ArrayList<>(mFilteredTasks); mFilter = filter; updateFilteredTasks(); if (!prevFilteredTasks.equals(mFilteredTasks)) { return true; } else { return false; } } /** Removes the task filter and returns the previous touch state */ void removeFilter() { mFilter = null; updateFilteredTasks(); } /** Adds a new task to the task list */ void add(Task t) { mTasks.add(t); updateFilteredTasks(); } /** * Moves the given task. */ public void moveTaskToStack(Task task, int insertIndex, int newStackId) { int taskIndex = indexOf(task); if (taskIndex != insertIndex) { mTasks.remove(taskIndex); if (taskIndex < insertIndex) { insertIndex--; } mTasks.add(insertIndex, task); } // Update the stack id now, after we've moved the task, and before we update the // filtered tasks task.setStackId(newStackId); updateFilteredTasks(); } /** Sets the list of tasks */ void set(List tasks) { mTasks.clear(); mTasks.addAll(tasks); updateFilteredTasks(); } /** Removes a task from the base list only if it is in the filtered list */ boolean remove(Task t) { if (mFilteredTasks.contains(t)) { boolean removed = mTasks.remove(t); updateFilteredTasks(); return removed; } return false; } /** Returns the index of this task in the list of filtered tasks */ int indexOf(Task t) { if (t != null && mTaskIndices.containsKey(t.key)) { return mTaskIndices.get(t.key); } return -1; } /** Returns the size of the list of filtered tasks */ int size() { return mFilteredTasks.size(); } /** Returns whether the filtered list contains this task */ boolean contains(Task t) { return mTaskIndices.containsKey(t.key); } /** Updates the list of filtered tasks whenever the base task list changes */ private void updateFilteredTasks() { mFilteredTasks.clear(); if (mFilter != null) { // Create a sparse array from task id to Task SparseArray taskIdMap = new SparseArray<>(); int taskCount = mTasks.size(); for (int i = 0; i < taskCount; i++) { Task t = mTasks.get(i); taskIdMap.put(t.key.id, t); } for (int i = 0; i < taskCount; i++) { Task t = mTasks.get(i); if (mFilter.acceptTask(taskIdMap, t, i)) { mFilteredTasks.add(t); } } } else { mFilteredTasks.addAll(mTasks); } updateFilteredTaskIndices(); } /** Updates the mapping of tasks to indices. */ private void updateFilteredTaskIndices() { int taskCount = mFilteredTasks.size(); mTaskIndices.clear(); for (int i = 0; i < taskCount; i++) { Task t = mFilteredTasks.get(i); mTaskIndices.put(t.key, i); } } /** Returns whether this task list is filtered */ boolean hasFilter() { return (mFilter != null); } /** Returns the list of filtered tasks */ ArrayList getTasks() { return mFilteredTasks; } } /** * The task stack contains a list of multiple tasks. */ public class TaskStack { private static final String TAG = "TaskStack"; /** Task stack callbacks */ public interface TaskStackCallbacks { /** * Notifies when a new task has been added to the stack. */ void onStackTaskAdded(TaskStack stack, Task newTask); /** * Notifies when a task has been removed from the stack. */ void onStackTaskRemoved(TaskStack stack, Task removedTask, Task newFrontMostTask, AnimationProps animation, boolean fromDockGesture, boolean dismissRecentsIfAllRemoved); /** * Notifies when all tasks have been removed from the stack. */ void onStackTasksRemoved(TaskStack stack); /** * Notifies when tasks in the stack have been updated. */ void onStackTasksUpdated(TaskStack stack); } /** * The various possible dock states when dragging and dropping a task. */ public static class DockState implements DropTarget { public static final int DOCK_AREA_BG_COLOR = 0xFFffffff; public static final int DOCK_AREA_GRID_BG_COLOR = 0xFF000000; // The rotation to apply to the hint text @Retention(RetentionPolicy.SOURCE) @IntDef({HORIZONTAL, VERTICAL}) public @interface TextOrientation {} private static final int HORIZONTAL = 0; private static final int VERTICAL = 1; private static final int DOCK_AREA_ALPHA = 80; public static final DockState NONE = new DockState(DOCKED_INVALID, -1, 80, 255, HORIZONTAL, null, null, null); public static final DockState LEFT = new DockState(DOCKED_LEFT, DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT, DOCK_AREA_ALPHA, 0, VERTICAL, new RectF(0, 0, 0.125f, 1), new RectF(0, 0, 0.125f, 1), new RectF(0, 0, 0.5f, 1)); public static final DockState TOP = new DockState(DOCKED_TOP, DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT, DOCK_AREA_ALPHA, 0, HORIZONTAL, new RectF(0, 0, 1, 0.125f), new RectF(0, 0, 1, 0.125f), new RectF(0, 0, 1, 0.5f)); public static final DockState RIGHT = new DockState(DOCKED_RIGHT, DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT, DOCK_AREA_ALPHA, 0, VERTICAL, new RectF(0.875f, 0, 1, 1), new RectF(0.875f, 0, 1, 1), new RectF(0.5f, 0, 1, 1)); public static final DockState BOTTOM = new DockState(DOCKED_BOTTOM, DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT, DOCK_AREA_ALPHA, 0, HORIZONTAL, new RectF(0, 0.875f, 1, 1), new RectF(0, 0.875f, 1, 1), new RectF(0, 0.5f, 1, 1)); @Override public boolean acceptsDrop(int x, int y, int width, int height, Rect insets, boolean isCurrentTarget) { if (isCurrentTarget) { getMappedRect(expandedTouchDockArea, width, height, mTmpRect); return mTmpRect.contains(x, y); } else { getMappedRect(touchArea, width, height, mTmpRect); updateBoundsWithSystemInsets(mTmpRect, insets); return mTmpRect.contains(x, y); } } // Represents the view state of this dock state public static class ViewState { private static final IntProperty HINT_ALPHA = new IntProperty("drawableAlpha") { @Override public void setValue(ViewState object, int alpha) { object.mHintTextAlpha = alpha; object.dockAreaOverlay.invalidateSelf(); } @Override public Integer get(ViewState object) { return object.mHintTextAlpha; } }; public final int dockAreaAlpha; public final ColorDrawable dockAreaOverlay; public final int hintTextAlpha; public final int hintTextOrientation; private final int mHintTextResId; private String mHintText; private Paint mHintTextPaint; private Point mHintTextBounds = new Point(); private int mHintTextAlpha = 255; private AnimatorSet mDockAreaOverlayAnimator; private Rect mTmpRect = new Rect(); private ViewState(int areaAlpha, int hintAlpha, @TextOrientation int hintOrientation, int hintTextResId) { dockAreaAlpha = areaAlpha; dockAreaOverlay = new ColorDrawable(Recents.getConfiguration().isGridEnabled ? DOCK_AREA_GRID_BG_COLOR : DOCK_AREA_BG_COLOR); dockAreaOverlay.setAlpha(0); hintTextAlpha = hintAlpha; hintTextOrientation = hintOrientation; mHintTextResId = hintTextResId; mHintTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mHintTextPaint.setColor(Color.WHITE); } /** * Updates the view state with the given context. */ public void update(Context context) { Resources res = context.getResources(); mHintText = context.getString(mHintTextResId); mHintTextPaint.setTextSize(res.getDimensionPixelSize( R.dimen.recents_drag_hint_text_size)); mHintTextPaint.getTextBounds(mHintText, 0, mHintText.length(), mTmpRect); mHintTextBounds.set((int) mHintTextPaint.measureText(mHintText), mTmpRect.height()); } /** * Draws the current view state. */ public void draw(Canvas canvas) { // Draw the overlay background if (dockAreaOverlay.getAlpha() > 0) { dockAreaOverlay.draw(canvas); } // Draw the hint text if (mHintTextAlpha > 0) { Rect bounds = dockAreaOverlay.getBounds(); int x = bounds.left + (bounds.width() - mHintTextBounds.x) / 2; int y = bounds.top + (bounds.height() + mHintTextBounds.y) / 2; mHintTextPaint.setAlpha(mHintTextAlpha); if (hintTextOrientation == VERTICAL) { canvas.save(); canvas.rotate(-90f, bounds.centerX(), bounds.centerY()); } canvas.drawText(mHintText, x, y, mHintTextPaint); if (hintTextOrientation == VERTICAL) { canvas.restore(); } } } /** * Creates a new bounds and alpha animation. */ public void startAnimation(Rect bounds, int areaAlpha, int hintAlpha, int duration, Interpolator interpolator, boolean animateAlpha, boolean animateBounds) { if (mDockAreaOverlayAnimator != null) { mDockAreaOverlayAnimator.cancel(); } ObjectAnimator anim; ArrayList animators = new ArrayList<>(); if (dockAreaOverlay.getAlpha() != areaAlpha) { if (animateAlpha) { anim = ObjectAnimator.ofInt(dockAreaOverlay, Utilities.DRAWABLE_ALPHA, dockAreaOverlay.getAlpha(), areaAlpha); anim.setDuration(duration); anim.setInterpolator(interpolator); animators.add(anim); } else { dockAreaOverlay.setAlpha(areaAlpha); } } if (mHintTextAlpha != hintAlpha) { if (animateAlpha) { anim = ObjectAnimator.ofInt(this, HINT_ALPHA, mHintTextAlpha, hintAlpha); anim.setDuration(150); anim.setInterpolator(hintAlpha > mHintTextAlpha ? Interpolators.ALPHA_IN : Interpolators.ALPHA_OUT); animators.add(anim); } else { mHintTextAlpha = hintAlpha; dockAreaOverlay.invalidateSelf(); } } if (bounds != null && !dockAreaOverlay.getBounds().equals(bounds)) { if (animateBounds) { PropertyValuesHolder prop = PropertyValuesHolder.ofObject( Utilities.DRAWABLE_RECT, Utilities.RECT_EVALUATOR, new Rect(dockAreaOverlay.getBounds()), bounds); anim = ObjectAnimator.ofPropertyValuesHolder(dockAreaOverlay, prop); anim.setDuration(duration); anim.setInterpolator(interpolator); animators.add(anim); } else { dockAreaOverlay.setBounds(bounds); } } if (!animators.isEmpty()) { mDockAreaOverlayAnimator = new AnimatorSet(); mDockAreaOverlayAnimator.playTogether(animators); mDockAreaOverlayAnimator.start(); } } } public final int dockSide; public final int createMode; public final ViewState viewState; private final RectF touchArea; private final RectF dockArea; private final RectF expandedTouchDockArea; private static final Rect mTmpRect = new Rect(); /** * @param createMode used to pass to ActivityManager to dock the task * @param touchArea the area in which touch will initiate this dock state * @param dockArea the visible dock area * @param expandedTouchDockArea the area in which touch will continue to dock after entering * the initial touch area. This is also the new dock area to * draw. */ DockState(int dockSide, int createMode, int dockAreaAlpha, int hintTextAlpha, @TextOrientation int hintTextOrientation, RectF touchArea, RectF dockArea, RectF expandedTouchDockArea) { this.dockSide = dockSide; this.createMode = createMode; this.viewState = new ViewState(dockAreaAlpha, hintTextAlpha, hintTextOrientation, R.string.recents_drag_hint_message); this.dockArea = dockArea; this.touchArea = touchArea; this.expandedTouchDockArea = expandedTouchDockArea; } /** * Updates the dock state with the given context. */ public void update(Context context) { viewState.update(context); } /** * Returns the docked task bounds with the given {@param width} and {@param height}. */ public Rect getPreDockedBounds(int width, int height, Rect insets) { getMappedRect(dockArea, width, height, mTmpRect); return updateBoundsWithSystemInsets(mTmpRect, insets); } /** * Returns the expanded docked task bounds with the given {@param width} and * {@param height}. */ public Rect getDockedBounds(int width, int height, int dividerSize, Rect insets, Resources res) { // Calculate the docked task bounds boolean isHorizontalDivision = res.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision, insets, width, height, dividerSize); Rect newWindowBounds = new Rect(); DockedDividerUtils.calculateBoundsForPosition(position, dockSide, newWindowBounds, width, height, dividerSize); return newWindowBounds; } /** * Returns the task stack bounds with the given {@param width} and * {@param height}. */ public Rect getDockedTaskStackBounds(Rect displayRect, int width, int height, int dividerSize, Rect insets, TaskStackLayoutAlgorithm layoutAlgorithm, Resources res, Rect windowRectOut) { // Calculate the inverse docked task bounds boolean isHorizontalDivision = res.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision, insets, width, height, dividerSize); DockedDividerUtils.calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(dockSide), windowRectOut, width, height, dividerSize); // Calculate the task stack bounds from the new window bounds Rect taskStackBounds = new Rect(); // If the task stack bounds is specifically under the dock area, then ignore the top // inset int top = dockArea.bottom < 1f ? 0 : insets.top; // For now, ignore the left insets since we always dock on the left and show Recents // on the right layoutAlgorithm.getTaskStackBounds(displayRect, windowRectOut, top, 0, insets.right, taskStackBounds); return taskStackBounds; } /** * Returns the expanded bounds in certain dock sides such that the bounds account for the * system insets (namely the vertical nav bar). This call modifies and returns the given * {@param bounds}. */ private Rect updateBoundsWithSystemInsets(Rect bounds, Rect insets) { if (dockSide == DOCKED_LEFT) { bounds.right += insets.left; } else if (dockSide == DOCKED_RIGHT) { bounds.left -= insets.right; } return bounds; } /** * Returns the mapped rect to the given dimensions. */ private void getMappedRect(RectF bounds, int width, int height, Rect out) { out.set((int) (bounds.left * width), (int) (bounds.top * height), (int) (bounds.right * width), (int) (bounds.bottom * height)); } } // A comparator that sorts tasks by their freeform state private Comparator FREEFORM_COMPARATOR = new Comparator() { @Override public int compare(Task o1, Task o2) { if (o1.isFreeformTask() && !o2.isFreeformTask()) { return 1; } else if (o2.isFreeformTask() && !o1.isFreeformTask()) { return -1; } return Long.compare(o1.temporarySortIndexInStack, o2.temporarySortIndexInStack); } }; // The task offset to apply to a task id as a group affiliation static final int IndividualTaskIdOffset = 1 << 16; ArrayList mRawTaskList = new ArrayList<>(); FilteredTaskList mStackTaskList = new FilteredTaskList(); TaskStackCallbacks mCb; ArrayList mGroups = new ArrayList<>(); ArrayMap mAffinitiesGroups = new ArrayMap<>(); public TaskStack() { // Ensure that we only show non-docked tasks mStackTaskList.setFilter(new TaskFilter() { @Override public boolean acceptTask(SparseArray taskIdMap, Task t, int index) { if (RecentsDebugFlags.Static.EnableAffiliatedTaskGroups) { if (t.isAffiliatedTask()) { // If this task is affiliated with another parent in the stack, then the // historical state of this task depends on the state of the parent task Task parentTask = taskIdMap.get(t.affiliationTaskId); if (parentTask != null) { t = parentTask; } } } return t.isStackTask; } }); } /** Sets the callbacks for this task stack. */ public void setCallbacks(TaskStackCallbacks cb) { mCb = cb; } /** * Moves the given task to either the front of the freeform workspace or the stack. */ public void moveTaskToStack(Task task, int newStackId) { // Find the index to insert into ArrayList taskList = mStackTaskList.getTasks(); int taskCount = taskList.size(); if (!task.isFreeformTask() && (newStackId == FREEFORM_WORKSPACE_STACK_ID)) { // Insert freeform tasks at the front mStackTaskList.moveTaskToStack(task, taskCount, newStackId); } else if (task.isFreeformTask() && (newStackId == FULLSCREEN_WORKSPACE_STACK_ID)) { // Insert after the first stacked task int insertIndex = 0; for (int i = taskCount - 1; i >= 0; i--) { if (!taskList.get(i).isFreeformTask()) { insertIndex = i + 1; break; } } mStackTaskList.moveTaskToStack(task, insertIndex, newStackId); } } /** Does the actual work associated with removing the task. */ void removeTaskImpl(FilteredTaskList taskList, Task t) { // Remove the task from the list taskList.remove(t); // Remove it from the group as well, and if it is empty, remove the group TaskGrouping group = t.group; if (group != null) { group.removeTask(t); if (group.getTaskCount() == 0) { removeGroup(group); } } } /** * Removes a task from the stack, with an additional {@param animation} hint to the callbacks on * how they should update themselves. */ public void removeTask(Task t, AnimationProps animation, boolean fromDockGesture) { removeTask(t, animation, fromDockGesture, true /* dismissRecentsIfAllRemoved */); } /** * Removes a task from the stack, with an additional {@param animation} hint to the callbacks on * how they should update themselves. */ public void removeTask(Task t, AnimationProps animation, boolean fromDockGesture, boolean dismissRecentsIfAllRemoved) { if (mStackTaskList.contains(t)) { removeTaskImpl(mStackTaskList, t); Task newFrontMostTask = getStackFrontMostTask(false /* includeFreeform */); if (mCb != null) { // Notify that a task has been removed mCb.onStackTaskRemoved(this, t, newFrontMostTask, animation, fromDockGesture, dismissRecentsIfAllRemoved); } } mRawTaskList.remove(t); } /** * Removes all tasks from the stack. */ public void removeAllTasks(boolean notifyStackChanges) { ArrayList tasks = mStackTaskList.getTasks(); for (int i = tasks.size() - 1; i >= 0; i--) { Task t = tasks.get(i); removeTaskImpl(mStackTaskList, t); mRawTaskList.remove(t); } if (mCb != null && notifyStackChanges) { // Notify that all tasks have been removed mCb.onStackTasksRemoved(this); } } /** * @see #setTasks(Context, List, boolean, boolean) */ public void setTasks(Context context, TaskStack stack, boolean notifyStackChanges) { setTasks(context, stack.mRawTaskList, notifyStackChanges); } /** * Sets a few tasks in one go, without calling any callbacks. * * @param tasks the new set of tasks to replace the current set. * @param notifyStackChanges whether or not to callback on specific changes to the list of tasks. */ public void setTasks(Context context, List tasks, boolean notifyStackChanges) { // Compute a has set for each of the tasks ArrayMap currentTasksMap = createTaskKeyMapFromList(mRawTaskList); ArrayMap newTasksMap = createTaskKeyMapFromList(tasks); ArrayList addedTasks = new ArrayList<>(); ArrayList removedTasks = new ArrayList<>(); ArrayList allTasks = new ArrayList<>(); // Disable notifications if there are no callbacks if (mCb == null) { notifyStackChanges = false; } // Remove any tasks that no longer exist int taskCount = mRawTaskList.size(); for (int i = taskCount - 1; i >= 0; i--) { Task task = mRawTaskList.get(i); if (!newTasksMap.containsKey(task.key)) { if (notifyStackChanges) { removedTasks.add(task); } } task.setGroup(null); } // Add any new tasks taskCount = tasks.size(); for (int i = 0; i < taskCount; i++) { Task newTask = tasks.get(i); Task currentTask = currentTasksMap.get(newTask.key); if (currentTask == null && notifyStackChanges) { addedTasks.add(newTask); } else if (currentTask != null) { // The current task has bound callbacks, so just copy the data from the new task // state and add it back into the list currentTask.copyFrom(newTask); newTask = currentTask; } allTasks.add(newTask); } // Sort all the tasks to ensure they are ordered correctly for (int i = allTasks.size() - 1; i >= 0; i--) { allTasks.get(i).temporarySortIndexInStack = i; } Collections.sort(allTasks, FREEFORM_COMPARATOR); mStackTaskList.set(allTasks); mRawTaskList = allTasks; // Update the affiliated groupings createAffiliatedGroupings(context); // Only callback for the removed tasks after the stack has updated int removedTaskCount = removedTasks.size(); Task newFrontMostTask = getStackFrontMostTask(false); for (int i = 0; i < removedTaskCount; i++) { mCb.onStackTaskRemoved(this, removedTasks.get(i), newFrontMostTask, AnimationProps.IMMEDIATE, false /* fromDockGesture */, true /* dismissRecentsIfAllRemoved */); } // Only callback for the newly added tasks after this stack has been updated int addedTaskCount = addedTasks.size(); for (int i = 0; i < addedTaskCount; i++) { mCb.onStackTaskAdded(this, addedTasks.get(i)); } // Notify that the task stack has been updated if (notifyStackChanges) { mCb.onStackTasksUpdated(this); } } /** * Gets the front-most task in the stack. */ public Task getStackFrontMostTask(boolean includeFreeformTasks) { ArrayList stackTasks = mStackTaskList.getTasks(); if (stackTasks.isEmpty()) { return null; } for (int i = stackTasks.size() - 1; i >= 0; i--) { Task task = stackTasks.get(i); if (!task.isFreeformTask() || includeFreeformTasks) { return task; } } return null; } /** Gets the task keys */ public ArrayList getTaskKeys() { ArrayList taskKeys = new ArrayList<>(); ArrayList tasks = computeAllTasksList(); int taskCount = tasks.size(); for (int i = 0; i < taskCount; i++) { Task task = tasks.get(i); taskKeys.add(task.key); } return taskKeys; } /** * Returns the set of "active" (non-historical) tasks in the stack that have been used recently. */ public ArrayList getStackTasks() { return mStackTaskList.getTasks(); } /** * Returns the set of "freeform" tasks in the stack. */ public ArrayList getFreeformTasks() { ArrayList freeformTasks = new ArrayList<>(); ArrayList tasks = mStackTaskList.getTasks(); int taskCount = tasks.size(); for (int i = 0; i < taskCount; i++) { Task task = tasks.get(i); if (task.isFreeformTask()) { freeformTasks.add(task); } } return freeformTasks; } /** * Computes a set of all the active and historical tasks. */ public ArrayList computeAllTasksList() { ArrayList tasks = new ArrayList<>(); tasks.addAll(mStackTaskList.getTasks()); return tasks; } /** * Returns the number of stack and freeform tasks. */ public int getTaskCount() { return mStackTaskList.size(); } /** * Returns the number of stack tasks. */ public int getStackTaskCount() { ArrayList tasks = mStackTaskList.getTasks(); int stackCount = 0; int taskCount = tasks.size(); for (int i = 0; i < taskCount; i++) { Task task = tasks.get(i); if (!task.isFreeformTask()) { stackCount++; } } return stackCount; } /** * Returns the number of freeform tasks. */ public int getFreeformTaskCount() { ArrayList tasks = mStackTaskList.getTasks(); int freeformCount = 0; int taskCount = tasks.size(); for (int i = 0; i < taskCount; i++) { Task task = tasks.get(i); if (task.isFreeformTask()) { freeformCount++; } } return freeformCount; } /** * Returns the task in stack tasks which is the launch target. */ public Task getLaunchTarget() { ArrayList tasks = mStackTaskList.getTasks(); int taskCount = tasks.size(); for (int i = 0; i < taskCount; i++) { Task task = tasks.get(i); if (task.isLaunchTarget) { return task; } } return null; } /** * Returns whether the next launch target should actually be the PiP task. */ public boolean isNextLaunchTargetPip(long lastPipTime) { Task launchTarget = getLaunchTarget(); Task nextLaunchTarget = getNextLaunchTargetRaw(); if (nextLaunchTarget != null && lastPipTime > 0) { // If the PiP time is more recent than the next launch target, then launch the PiP task return lastPipTime > nextLaunchTarget.key.lastActiveTime; } else if (launchTarget != null && lastPipTime > 0 && getTaskCount() == 1) { // Otherwise, if there is no next launch target, but there is a PiP, then launch // the PiP task return true; } return false; } /** * Returns the task in stack tasks which should be launched next if Recents are toggled * again, or null if there is no task to be launched. Callers should check * {@link #isNextLaunchTargetPip(long)} before fetching the next raw launch target from the * stack. */ public Task getNextLaunchTarget() { Task nextLaunchTarget = getNextLaunchTargetRaw(); if (nextLaunchTarget != null) { return nextLaunchTarget; } return getStackTasks().get(getTaskCount() - 1); } private Task getNextLaunchTargetRaw() { int taskCount = getTaskCount(); if (taskCount == 0) { return null; } int launchTaskIndex = indexOfStackTask(getLaunchTarget()); if (launchTaskIndex != -1 && launchTaskIndex > 0) { return getStackTasks().get(launchTaskIndex - 1); } return null; } /** Returns the index of this task in this current task stack */ public int indexOfStackTask(Task t) { return mStackTaskList.indexOf(t); } /** Finds the task with the specified task id. */ public Task findTaskWithId(int taskId) { ArrayList tasks = computeAllTasksList(); int taskCount = tasks.size(); for (int i = 0; i < taskCount; i++) { Task task = tasks.get(i); if (task.key.id == taskId) { return task; } } return null; } /******** Grouping ********/ /** Adds a group to the set */ public void addGroup(TaskGrouping group) { mGroups.add(group); mAffinitiesGroups.put(group.affiliation, group); } public void removeGroup(TaskGrouping group) { mGroups.remove(group); mAffinitiesGroups.remove(group.affiliation); } /** Returns the group with the specified affiliation. */ public TaskGrouping getGroupWithAffiliation(int affiliation) { return mAffinitiesGroups.get(affiliation); } /** * Temporary: This method will simulate affiliation groups */ void createAffiliatedGroupings(Context context) { mGroups.clear(); mAffinitiesGroups.clear(); if (RecentsDebugFlags.Static.EnableMockTaskGroups) { ArrayMap taskMap = new ArrayMap<>(); // Sort all tasks by increasing firstActiveTime of the task ArrayList tasks = mStackTaskList.getTasks(); Collections.sort(tasks, new Comparator() { @Override public int compare(Task task, Task task2) { return Long.compare(task.key.firstActiveTime, task2.key.firstActiveTime); } }); // Create groups when sequential packages are the same NamedCounter counter = new NamedCounter("task-group", ""); int taskCount = tasks.size(); String prevPackage = ""; int prevAffiliation = -1; Random r = new Random(); int groupCountDown = RecentsDebugFlags.Static.MockTaskGroupsTaskCount; for (int i = 0; i < taskCount; i++) { Task t = tasks.get(i); String packageName = t.key.getComponent().getPackageName(); packageName = "pkg"; TaskGrouping group; if (packageName.equals(prevPackage) && groupCountDown > 0) { group = getGroupWithAffiliation(prevAffiliation); groupCountDown--; } else { int affiliation = IndividualTaskIdOffset + t.key.id; group = new TaskGrouping(affiliation); addGroup(group); prevAffiliation = affiliation; prevPackage = packageName; groupCountDown = RecentsDebugFlags.Static.MockTaskGroupsTaskCount; } group.addTask(t); taskMap.put(t.key, t); } // Sort groups by increasing latestActiveTime of the group Collections.sort(mGroups, new Comparator() { @Override public int compare(TaskGrouping taskGrouping, TaskGrouping taskGrouping2) { return Long.compare(taskGrouping.latestActiveTimeInGroup, taskGrouping2.latestActiveTimeInGroup); } }); // Sort group tasks by increasing firstActiveTime of the task, and also build a new list // of tasks int taskIndex = 0; int groupCount = mGroups.size(); for (int i = 0; i < groupCount; i++) { TaskGrouping group = mGroups.get(i); Collections.sort(group.mTaskKeys, new Comparator() { @Override public int compare(Task.TaskKey taskKey, Task.TaskKey taskKey2) { return Long.compare(taskKey.firstActiveTime, taskKey2.firstActiveTime); } }); ArrayList groupTasks = group.mTaskKeys; int groupTaskCount = groupTasks.size(); for (int j = 0; j < groupTaskCount; j++) { tasks.set(taskIndex, taskMap.get(groupTasks.get(j))); taskIndex++; } } mStackTaskList.set(tasks); } else { // Create the task groups ArrayMap tasksMap = new ArrayMap<>(); ArrayList tasks = mStackTaskList.getTasks(); int taskCount = tasks.size(); for (int i = 0; i < taskCount; i++) { Task t = tasks.get(i); TaskGrouping group; if (RecentsDebugFlags.Static.EnableAffiliatedTaskGroups) { int affiliation = t.affiliationTaskId > 0 ? t.affiliationTaskId : IndividualTaskIdOffset + t.key.id; if (mAffinitiesGroups.containsKey(affiliation)) { group = getGroupWithAffiliation(affiliation); } else { group = new TaskGrouping(affiliation); addGroup(group); } } else { group = new TaskGrouping(t.key.id); addGroup(group); } group.addTask(t); tasksMap.put(t.key, t); } // Update the task colors for each of the groups float minAlpha = context.getResources().getFloat( R.dimen.recents_task_affiliation_color_min_alpha_percentage); int taskGroupCount = mGroups.size(); for (int i = 0; i < taskGroupCount; i++) { TaskGrouping group = mGroups.get(i); taskCount = group.getTaskCount(); // Ignore the groups that only have one task if (taskCount <= 1) continue; // Calculate the group color distribution int affiliationColor = tasksMap.get(group.mTaskKeys.get(0)).affiliationColor; float alphaStep = (1f - minAlpha) / taskCount; float alpha = 1f; for (int j = 0; j < taskCount; j++) { Task t = tasksMap.get(group.mTaskKeys.get(j)); t.colorPrimary = Utilities.getColorWithOverlay(affiliationColor, Color.WHITE, alpha); alpha -= alphaStep; } } } } /** * Computes the components of tasks in this stack that have been removed as a result of a change * in the specified package. */ public ArraySet computeComponentsRemoved(String packageName, int userId) { // Identify all the tasks that should be removed as a result of the package being removed. // Using a set to ensure that we callback once per unique component. SystemServicesProxy ssp = Recents.getSystemServices(); ArraySet existingComponents = new ArraySet<>(); ArraySet removedComponents = new ArraySet<>(); ArrayList taskKeys = getTaskKeys(); int taskKeyCount = taskKeys.size(); for (int i = 0; i < taskKeyCount; i++) { Task.TaskKey t = taskKeys.get(i); // Skip if this doesn't apply to the current user if (t.userId != userId) continue; ComponentName cn = t.getComponent(); if (cn.getPackageName().equals(packageName)) { if (existingComponents.contains(cn)) { // If we know that the component still exists in the package, then skip continue; } if (ssp.getActivityInfo(cn, userId) != null) { existingComponents.add(cn); } else { removedComponents.add(cn); } } } return removedComponents; } @Override public String toString() { String str = "Stack Tasks (" + mStackTaskList.size() + "):\n"; ArrayList tasks = mStackTaskList.getTasks(); int taskCount = tasks.size(); for (int i = 0; i < taskCount; i++) { str += " " + tasks.get(i).toString() + "\n"; } return str; } /** * Given a list of tasks, returns a map of each task's key to the task. */ private ArrayMap createTaskKeyMapFromList(List tasks) { ArrayMap map = new ArrayMap<>(tasks.size()); int taskCount = tasks.size(); for (int i = 0; i < taskCount; i++) { Task task = tasks.get(i); map.put(task.key, task); } return map; } public void dump(String prefix, PrintWriter writer) { String innerPrefix = prefix + " "; writer.print(prefix); writer.print(TAG); writer.print(" numStackTasks="); writer.print(mStackTaskList.size()); writer.println(); ArrayList tasks = mStackTaskList.getTasks(); int taskCount = tasks.size(); for (int i = 0; i < taskCount; i++) { tasks.get(i).dump(innerPrefix, writer); } } }