blob: 745f5a5a773e6a14c7f81d7288f4bdc90ce70130 [file] [log] [blame]
/*
* 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 {
// 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(0xFFffffff);
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 areain 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 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);
}
}
}