| /* |
| * 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 android.animation.ObjectAnimator; |
| import android.content.Context; |
| import android.graphics.Color; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.graphics.drawable.ColorDrawable; |
| import com.android.systemui.R; |
| import com.android.systemui.recents.Constants; |
| import com.android.systemui.recents.misc.NamedCounter; |
| import com.android.systemui.recents.misc.Utilities; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Random; |
| |
| import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT; |
| import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT; |
| |
| |
| /** |
| * 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(Task t, int index); |
| } |
| |
| /** |
| * A list of filtered tasks. |
| */ |
| class FilteredTaskList { |
| ArrayList<Task> mTasks = new ArrayList<Task>(); |
| ArrayList<Task> mFilteredTasks = new ArrayList<Task>(); |
| HashMap<Task.TaskKey, Integer> mTaskIndices = new HashMap<Task.TaskKey, Integer>(); |
| TaskFilter mFilter; |
| |
| /** Sets the task filter, saving the current touch state */ |
| boolean setFilter(TaskFilter filter) { |
| ArrayList<Task> prevFilteredTasks = new ArrayList<Task>(mFilteredTasks); |
| mFilter = filter; |
| updateFilteredTasks(); |
| if (!prevFilteredTasks.equals(mFilteredTasks)) { |
| return true; |
| } else { |
| // If the tasks are exactly the same pre/post filter, then just reset it |
| mFilter = null; |
| return false; |
| } |
| } |
| |
| /** Resets this FilteredTaskList. */ |
| void reset() { |
| mTasks.clear(); |
| mFilteredTasks.clear(); |
| mTaskIndices.clear(); |
| mFilter = null; |
| } |
| |
| /** 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(); |
| } |
| |
| /** 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 (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) { |
| int taskCount = mTasks.size(); |
| for (int i = 0; i < taskCount; i++) { |
| Task t = mTasks.get(i); |
| if (mFilter.acceptTask(t, i)) { |
| mFilteredTasks.add(t); |
| } |
| } |
| } else { |
| mFilteredTasks.addAll(mTasks); |
| } |
| updateFilteredTaskIndices(); |
| } |
| |
| /** Updates the mapping of tasks to indices. */ |
| private void updateFilteredTaskIndices() { |
| mTaskIndices.clear(); |
| int taskCount = mFilteredTasks.size(); |
| 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 { |
| |
| /** Task stack callbacks */ |
| public interface TaskStackCallbacks { |
| /* Notifies when a task has been added to the stack */ |
| public void onStackTaskAdded(TaskStack stack, Task t); |
| /* Notifies when a task has been removed from the stack */ |
| public void onStackTaskRemoved(TaskStack stack, Task removedTask, boolean wasFrontMostTask, |
| Task newFrontMostTask); |
| /* Notifies when all task has been removed from the stack */ |
| public void onStackAllTasksRemoved(TaskStack stack, ArrayList<Task> removedTasks); |
| /** Notifies when the stack was filtered */ |
| public void onStackFiltered(TaskStack newStack, ArrayList<Task> curTasks, Task t); |
| /** Notifies when the stack was un-filtered */ |
| public void onStackUnfiltered(TaskStack newStack, ArrayList<Task> curTasks); |
| } |
| |
| |
| public enum DockState { |
| NONE(-1, 96, null, null, null), |
| LEFT(DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT, 192, |
| new RectF(0, 0, 0.3f, 1), new RectF(0, 0, 0.3f, 1), new RectF(0.7f, 0, 1, 1)), |
| TOP(DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT, 192, |
| new RectF(0, 0, 1, 0.3f), new RectF(0, 0, 1, 0.3f), new RectF(0, 0.7f, 1, 1)), |
| RIGHT(DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT, 192, |
| new RectF(0.7f, 0, 1, 1), new RectF(0.7f, 0, 1, 1), new RectF(0, 0, 0.3f, 1)), |
| BOTTOM(DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT, 192, |
| new RectF(0, 0.7f, 1, 1), new RectF(0, 0.7f, 1, 1), new RectF(0, 0, 1, 0.3f)); |
| |
| // Represents the view state of this dock state |
| public class ViewState { |
| public final int dockAreaAlpha; |
| public final ColorDrawable dockAreaOverlay; |
| private ObjectAnimator dockAreaOverlayAnimator; |
| |
| private ViewState(int alpha) { |
| dockAreaAlpha = alpha; |
| dockAreaOverlay = new ColorDrawable(0xFFffffff); |
| dockAreaOverlay.setAlpha(0); |
| } |
| |
| /** |
| * Creates a new alpha animation. |
| */ |
| public void startAlphaAnimation(int alpha, int duration) { |
| if (dockAreaOverlay.getAlpha() != alpha) { |
| if (dockAreaOverlayAnimator != null) { |
| dockAreaOverlayAnimator.cancel(); |
| } |
| dockAreaOverlayAnimator = ObjectAnimator.ofInt(dockAreaOverlay, "alpha", alpha); |
| dockAreaOverlayAnimator.setDuration(duration); |
| dockAreaOverlayAnimator.start(); |
| } |
| } |
| } |
| |
| public final int createMode; |
| public final ViewState viewState; |
| private final RectF dockArea; |
| private final RectF touchArea; |
| private final RectF stackArea; |
| |
| /** |
| * @param createMode used to pass to ActivityManager to dock the task |
| * @param touchArea the area in which touch will initiate this dock state |
| * @param stackArea the area for the stack if a task is docked |
| */ |
| DockState(int createMode, int dockAreaAlpha, RectF touchArea, RectF dockArea, |
| RectF stackArea) { |
| this.createMode = createMode; |
| this.viewState = new ViewState(dockAreaAlpha); |
| this.dockArea = dockArea; |
| this.touchArea = touchArea; |
| this.stackArea = stackArea; |
| } |
| |
| /** |
| * Returns whether {@param x} and {@param y} are contained in the touch area scaled to the |
| * given {@param width} and {@param height}. |
| */ |
| public boolean touchAreaContainsPoint(int width, int height, float x, float y) { |
| int left = (int) (touchArea.left * width); |
| int top = (int) (touchArea.top * height); |
| int right = (int) (touchArea.right * width); |
| int bottom = (int) (touchArea.bottom * height); |
| return x >= left && y >= top && x <= right && y <= bottom; |
| } |
| |
| /** |
| * Returns the docked task bounds with the given {@param width} and {@param height}. |
| */ |
| public Rect getDockedBounds(int width, int height) { |
| return new Rect((int) (dockArea.left * width), (int) (dockArea.top * height), |
| (int) (dockArea.right * width), (int) (dockArea.bottom * height)); |
| } |
| |
| /** |
| * Returns the stack bounds with the given {@param width} and {@param height}. |
| */ |
| public Rect getStackBounds(Rect stackRect) { |
| int width = stackRect.width(); |
| int height = stackRect.height(); |
| return new Rect((int) (stackRect.left + stackArea.left * width), |
| (int) (stackRect.top + stackArea.top * height), |
| (int) (stackRect.left + (stackArea.right - stackArea.left) * width), |
| (int) (stackRect.top + (stackArea.bottom - stackArea.top) * height)); |
| } |
| } |
| |
| // The task offset to apply to a task id as a group affiliation |
| static final int IndividualTaskIdOffset = 1 << 16; |
| |
| FilteredTaskList mTaskList = new FilteredTaskList(); |
| TaskStackCallbacks mCb; |
| |
| ArrayList<TaskGrouping> mGroups = new ArrayList<TaskGrouping>(); |
| HashMap<Integer, TaskGrouping> mAffinitiesGroups = new HashMap<Integer, TaskGrouping>(); |
| |
| /** Sets the callbacks for this task stack. */ |
| public void setCallbacks(TaskStackCallbacks cb) { |
| mCb = cb; |
| } |
| |
| /** Resets this TaskStack. */ |
| public void reset() { |
| mCb = null; |
| mTaskList.reset(); |
| mGroups.clear(); |
| mAffinitiesGroups.clear(); |
| } |
| |
| /** Adds a new task */ |
| public void addTask(Task t) { |
| mTaskList.add(t); |
| if (mCb != null) { |
| mCb.onStackTaskAdded(this, t); |
| } |
| } |
| |
| /** Does the actual work associated with removing the task. */ |
| void removeTaskImpl(Task t) { |
| // Remove the task from the list |
| mTaskList.remove(t); |
| // Remove it from the group as well, and if it is empty, remove the group |
| TaskGrouping group = t.group; |
| group.removeTask(t); |
| if (group.getTaskCount() == 0) { |
| removeGroup(group); |
| } |
| // Update the lock-to-app state |
| t.lockToThisTask = false; |
| } |
| |
| /** Removes a task */ |
| public void removeTask(Task t) { |
| if (mTaskList.contains(t)) { |
| boolean wasFrontMostTask = (getFrontMostTask() == t); |
| removeTaskImpl(t); |
| Task newFrontMostTask = getFrontMostTask(); |
| if (newFrontMostTask != null && newFrontMostTask.lockToTaskEnabled) { |
| newFrontMostTask.lockToThisTask = true; |
| } |
| if (mCb != null) { |
| // Notify that a task has been removed |
| mCb.onStackTaskRemoved(this, t, wasFrontMostTask, newFrontMostTask); |
| } |
| } |
| } |
| |
| /** Removes all tasks */ |
| public void removeAllTasks() { |
| ArrayList<Task> taskList = new ArrayList<Task>(mTaskList.getTasks()); |
| int taskCount = taskList.size(); |
| for (int i = taskCount - 1; i >= 0; i--) { |
| Task t = taskList.get(i); |
| removeTaskImpl(t); |
| } |
| if (mCb != null) { |
| // Notify that all tasks have been removed |
| mCb.onStackAllTasksRemoved(this, taskList); |
| } |
| } |
| |
| /** Sets a few tasks in one go */ |
| public void setTasks(List<Task> tasks) { |
| ArrayList<Task> taskList = mTaskList.getTasks(); |
| int taskCount = taskList.size(); |
| for (int i = taskCount - 1; i >= 0; i--) { |
| Task t = taskList.get(i); |
| removeTaskImpl(t); |
| if (mCb != null) { |
| // Notify that a task has been removed |
| mCb.onStackTaskRemoved(this, t, false, null); |
| } |
| } |
| mTaskList.set(tasks); |
| for (Task t : tasks) { |
| if (mCb != null) { |
| mCb.onStackTaskAdded(this, t); |
| } |
| } |
| } |
| |
| /** Gets the front task */ |
| public Task getFrontMostTask() { |
| if (mTaskList.size() == 0) return null; |
| return mTaskList.getTasks().get(mTaskList.size() - 1); |
| } |
| |
| /** Gets the task keys */ |
| public ArrayList<Task.TaskKey> getTaskKeys() { |
| ArrayList<Task.TaskKey> taskKeys = new ArrayList<Task.TaskKey>(); |
| ArrayList<Task> tasks = mTaskList.getTasks(); |
| int taskCount = tasks.size(); |
| for (int i = 0; i < taskCount; i++) { |
| taskKeys.add(tasks.get(i).key); |
| } |
| return taskKeys; |
| } |
| |
| /** Gets the tasks */ |
| public ArrayList<Task> getTasks() { |
| return mTaskList.getTasks(); |
| } |
| |
| /** Gets the number of tasks */ |
| public int getTaskCount() { |
| return mTaskList.size(); |
| } |
| |
| /** Returns the index of this task in this current task stack */ |
| public int indexOfTask(Task t) { |
| return mTaskList.indexOf(t); |
| } |
| |
| /** Finds the task with the specified task id. */ |
| public Task findTaskWithId(int taskId) { |
| ArrayList<Task> tasks = mTaskList.getTasks(); |
| 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; |
| } |
| |
| /******** Filtering ********/ |
| |
| /** Filters the stack into tasks similar to the one specified */ |
| public void filterTasks(final Task t) { |
| ArrayList<Task> oldStack = new ArrayList<Task>(mTaskList.getTasks()); |
| |
| // Set the task list filter |
| boolean filtered = mTaskList.setFilter(new TaskFilter() { |
| @Override |
| public boolean acceptTask(Task at, int i) { |
| return t.key.baseIntent.getComponent().getPackageName().equals( |
| at.key.baseIntent.getComponent().getPackageName()); |
| } |
| }); |
| if (filtered && mCb != null) { |
| mCb.onStackFiltered(this, oldStack, t); |
| } |
| } |
| |
| /** Unfilters the current stack */ |
| public void unfilterTasks() { |
| ArrayList<Task> oldStack = new ArrayList<Task>(mTaskList.getTasks()); |
| |
| // Unset the filter, then update the virtual scroll |
| mTaskList.removeFilter(); |
| if (mCb != null) { |
| mCb.onStackUnfiltered(this, oldStack); |
| } |
| } |
| |
| /** Returns whether tasks are currently filtered */ |
| public boolean hasFilteredTasks() { |
| return mTaskList.hasFilter(); |
| } |
| |
| /******** 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 by |
| */ |
| public void createAffiliatedGroupings(Context context) { |
| if (Constants.DebugFlags.App.EnableSimulatedTaskGroups) { |
| HashMap<Task.TaskKey, Task> taskMap = new HashMap<Task.TaskKey, Task>(); |
| // Sort all tasks by increasing firstActiveTime of the task |
| ArrayList<Task> tasks = mTaskList.getTasks(); |
| Collections.sort(tasks, new Comparator<Task>() { |
| @Override |
| public int compare(Task task, Task task2) { |
| return (int) (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 = Constants.DebugFlags.App.TaskAffiliationsGroupCount; |
| for (int i = 0; i < taskCount; i++) { |
| Task t = tasks.get(i); |
| String packageName = t.key.baseIntent.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 = Constants.DebugFlags.App.TaskAffiliationsGroupCount; |
| } |
| 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 (int) (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 (int) (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++; |
| } |
| } |
| mTaskList.set(tasks); |
| } else { |
| // Create the task groups |
| HashMap<Task.TaskKey, Task> tasksMap = new HashMap<Task.TaskKey, Task>(); |
| ArrayList<Task> tasks = mTaskList.getTasks(); |
| int taskCount = tasks.size(); |
| for (int i = 0; i < taskCount; i++) { |
| Task t = tasks.get(i); |
| TaskGrouping group; |
| int affiliation = t.taskAffiliation > 0 ? t.taskAffiliation : |
| IndividualTaskIdOffset + t.key.id; |
| if (mAffinitiesGroups.containsKey(affiliation)) { |
| group = getGroupWithAffiliation(affiliation); |
| } else { |
| group = new TaskGrouping(affiliation); |
| 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)).taskAffiliationColor; |
| 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; |
| } |
| } |
| } |
| } |
| |
| @Override |
| public String toString() { |
| String str = "Tasks:\n"; |
| for (Task t : mTaskList.getTasks()) { |
| str += " " + t.toString() + "\n"; |
| } |
| return str; |
| } |
| } |