| /* |
| * 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<Task> taskIdMap, Task t, int index); |
| } |
| |
| /** |
| * A list of filtered tasks. |
| */ |
| class FilteredTaskList { |
| |
| ArrayList<Task> mTasks = new ArrayList<>(); |
| ArrayList<Task> mFilteredTasks = new ArrayList<>(); |
| ArrayMap<Task.TaskKey, Integer> mTaskIndices = new ArrayMap<>(); |
| TaskFilter mFilter; |
| |
| /** Sets the task filter, saving the current touch state */ |
| boolean setFilter(TaskFilter filter) { |
| ArrayList<Task> 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<Task> 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<Task> 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<Task> 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); |
| |
| /** |
| * 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<ViewState> HINT_ALPHA = |
| new IntProperty<ViewState>("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<Animator> 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<Task> FREEFORM_COMPARATOR = new Comparator<Task>() { |
| @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<Task> mRawTaskList = new ArrayList<>(); |
| FilteredTaskList mStackTaskList = new FilteredTaskList(); |
| TaskStackCallbacks mCb; |
| |
| ArrayList<TaskGrouping> mGroups = new ArrayList<>(); |
| ArrayMap<Integer, TaskGrouping> mAffinitiesGroups = new ArrayMap<>(); |
| |
| public TaskStack() { |
| // Ensure that we only show non-docked tasks |
| mStackTaskList.setFilter(new TaskFilter() { |
| @Override |
| public boolean acceptTask(SparseArray<Task> 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<Task> 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) { |
| 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); |
| } |
| } |
| mRawTaskList.remove(t); |
| } |
| |
| /** |
| * Removes all tasks from the stack. |
| */ |
| public void removeAllTasks() { |
| ArrayList<Task> 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) { |
| // Notify that all tasks have been removed |
| mCb.onStackTasksRemoved(this); |
| } |
| } |
| |
| /** |
| * 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<Task> tasks, boolean notifyStackChanges) { |
| // Compute a has set for each of the tasks |
| ArrayMap<Task.TaskKey, Task> currentTasksMap = createTaskKeyMapFromList(mRawTaskList); |
| ArrayMap<Task.TaskKey, Task> newTasksMap = createTaskKeyMapFromList(tasks); |
| ArrayList<Task> addedTasks = new ArrayList<>(); |
| ArrayList<Task> removedTasks = new ArrayList<>(); |
| ArrayList<Task> 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 */); |
| } |
| |
| // 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<Task> 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<Task.TaskKey> getTaskKeys() { |
| ArrayList<Task.TaskKey> taskKeys = new ArrayList<>(); |
| ArrayList<Task> 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<Task> getStackTasks() { |
| return mStackTaskList.getTasks(); |
| } |
| |
| /** |
| * Returns the set of "freeform" tasks in the stack. |
| */ |
| public ArrayList<Task> getFreeformTasks() { |
| ArrayList<Task> freeformTasks = new ArrayList<>(); |
| ArrayList<Task> 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<Task> computeAllTasksList() { |
| ArrayList<Task> 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<Task> 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<Task> 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<Task> 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 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. |
| */ |
| public Task getNextLaunchTarget() { |
| int taskCount = getTaskCount(); |
| if (taskCount == 0) { |
| return null; |
| } |
| int launchTaskIndex = indexOfStackTask(getLaunchTarget()); |
| if (launchTaskIndex != -1) { |
| launchTaskIndex = Math.max(0, launchTaskIndex - 1); |
| } else { |
| launchTaskIndex = getTaskCount() - 1; |
| } |
| return getStackTasks().get(launchTaskIndex); |
| } |
| |
| /** 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<Task> 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<Task.TaskKey, Task> taskMap = new ArrayMap<>(); |
| // Sort all tasks by increasing firstActiveTime of the task |
| ArrayList<Task> tasks = mStackTaskList.getTasks(); |
| Collections.sort(tasks, new Comparator<Task>() { |
| @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<TaskGrouping>() { |
| @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<Task.TaskKey>() { |
| @Override |
| public int compare(Task.TaskKey taskKey, Task.TaskKey taskKey2) { |
| return Long.compare(taskKey.firstActiveTime, taskKey2.firstActiveTime); |
| } |
| }); |
| ArrayList<Task.TaskKey> 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<Task.TaskKey, Task> tasksMap = new ArrayMap<>(); |
| ArrayList<Task> 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<ComponentName> 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<ComponentName> existingComponents = new ArraySet<>(); |
| ArraySet<ComponentName> removedComponents = new ArraySet<>(); |
| ArrayList<Task.TaskKey> 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<Task> 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<Task.TaskKey, Task> createTaskKeyMapFromList(List<Task> tasks) { |
| ArrayMap<Task.TaskKey, Task> 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<Task> tasks = mStackTaskList.getTasks(); |
| int taskCount = tasks.size(); |
| for (int i = 0; i < taskCount; i++) { |
| tasks.get(i).dump(innerPrefix, writer); |
| } |
| } |
| } |