1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.recents.model;
18 
19 import android.graphics.Color;
20 import android.graphics.Rect;
21 import com.android.systemui.recents.Constants;
22 import com.android.systemui.recents.RecentsConfiguration;
23 import com.android.systemui.recents.misc.NamedCounter;
24 import com.android.systemui.recents.misc.Utilities;
25 
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.Comparator;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.Random;
32 
33 
34 /**
35  * An interface for a task filter to query whether a particular task should show in a stack.
36  */
37 interface TaskFilter {
38     /** Returns whether the filter accepts the specified task */
acceptTask(Task t, int index)39     public boolean acceptTask(Task t, int index);
40 }
41 
42 /**
43  * A list of filtered tasks.
44  */
45 class FilteredTaskList {
46     ArrayList<Task> mTasks = new ArrayList<Task>();
47     ArrayList<Task> mFilteredTasks = new ArrayList<Task>();
48     HashMap<Task.TaskKey, Integer> mTaskIndices = new HashMap<Task.TaskKey, Integer>();
49     TaskFilter mFilter;
50 
51     /** Sets the task filter, saving the current touch state */
setFilter(TaskFilter filter)52     boolean setFilter(TaskFilter filter) {
53         ArrayList<Task> prevFilteredTasks = new ArrayList<Task>(mFilteredTasks);
54         mFilter = filter;
55         updateFilteredTasks();
56         if (!prevFilteredTasks.equals(mFilteredTasks)) {
57             return true;
58         } else {
59             // If the tasks are exactly the same pre/post filter, then just reset it
60             mFilter = null;
61             return false;
62         }
63     }
64 
65     /** Resets this FilteredTaskList. */
reset()66     void reset() {
67         mTasks.clear();
68         mFilteredTasks.clear();
69         mTaskIndices.clear();
70         mFilter = null;
71     }
72 
73     /** Removes the task filter and returns the previous touch state */
removeFilter()74     void removeFilter() {
75         mFilter = null;
76         updateFilteredTasks();
77     }
78 
79     /** Adds a new task to the task list */
add(Task t)80     void add(Task t) {
81         mTasks.add(t);
82         updateFilteredTasks();
83     }
84 
85     /** Sets the list of tasks */
set(List<Task> tasks)86     void set(List<Task> tasks) {
87         mTasks.clear();
88         mTasks.addAll(tasks);
89         updateFilteredTasks();
90     }
91 
92     /** Removes a task from the base list only if it is in the filtered list */
remove(Task t)93     boolean remove(Task t) {
94         if (mFilteredTasks.contains(t)) {
95             boolean removed = mTasks.remove(t);
96             updateFilteredTasks();
97             return removed;
98         }
99         return false;
100     }
101 
102     /** Returns the index of this task in the list of filtered tasks */
indexOf(Task t)103     int indexOf(Task t) {
104         if (mTaskIndices.containsKey(t.key)) {
105             return mTaskIndices.get(t.key);
106         }
107         return -1;
108     }
109 
110     /** Returns the size of the list of filtered tasks */
size()111     int size() {
112         return mFilteredTasks.size();
113     }
114 
115     /** Returns whether the filtered list contains this task */
contains(Task t)116     boolean contains(Task t) {
117         return mTaskIndices.containsKey(t.key);
118     }
119 
120     /** Updates the list of filtered tasks whenever the base task list changes */
updateFilteredTasks()121     private void updateFilteredTasks() {
122         mFilteredTasks.clear();
123         if (mFilter != null) {
124             int taskCount = mTasks.size();
125             for (int i = 0; i < taskCount; i++) {
126                 Task t = mTasks.get(i);
127                 if (mFilter.acceptTask(t, i)) {
128                     mFilteredTasks.add(t);
129                 }
130             }
131         } else {
132             mFilteredTasks.addAll(mTasks);
133         }
134         updateFilteredTaskIndices();
135     }
136 
137     /** Updates the mapping of tasks to indices. */
updateFilteredTaskIndices()138     private void updateFilteredTaskIndices() {
139         mTaskIndices.clear();
140         int taskCount = mFilteredTasks.size();
141         for (int i = 0; i < taskCount; i++) {
142             Task t = mFilteredTasks.get(i);
143             mTaskIndices.put(t.key, i);
144         }
145     }
146 
147     /** Returns whether this task list is filtered */
hasFilter()148     boolean hasFilter() {
149         return (mFilter != null);
150     }
151 
152     /** Returns the list of filtered tasks */
getTasks()153     ArrayList<Task> getTasks() {
154         return mFilteredTasks;
155     }
156 }
157 
158 /**
159  * The task stack contains a list of multiple tasks.
160  */
161 public class TaskStack {
162 
163     /** Task stack callbacks */
164     public interface TaskStackCallbacks {
165         /* Notifies when a task has been added to the stack */
onStackTaskAdded(TaskStack stack, Task t)166         public void onStackTaskAdded(TaskStack stack, Task t);
167         /* Notifies when a task has been removed from the stack */
onStackTaskRemoved(TaskStack stack, Task removedTask, Task newFrontMostTask)168         public void onStackTaskRemoved(TaskStack stack, Task removedTask, Task newFrontMostTask);
169         /* Notifies when all task has been removed from the stack */
onStackAllTasksRemoved(TaskStack stack, ArrayList<Task> removedTasks)170         public void onStackAllTasksRemoved(TaskStack stack, ArrayList<Task> removedTasks);
171         /** Notifies when the stack was filtered */
onStackFiltered(TaskStack newStack, ArrayList<Task> curTasks, Task t)172         public void onStackFiltered(TaskStack newStack, ArrayList<Task> curTasks, Task t);
173         /** Notifies when the stack was un-filtered */
onStackUnfiltered(TaskStack newStack, ArrayList<Task> curTasks)174         public void onStackUnfiltered(TaskStack newStack, ArrayList<Task> curTasks);
175     }
176 
177     // The task offset to apply to a task id as a group affiliation
178     static final int IndividualTaskIdOffset = 1 << 16;
179 
180     public final int id;
181     public final Rect stackBounds = new Rect();
182     public final Rect displayBounds = new Rect();
183 
184     FilteredTaskList mTaskList = new FilteredTaskList();
185     TaskStackCallbacks mCb;
186 
187     ArrayList<TaskGrouping> mGroups = new ArrayList<TaskGrouping>();
188     HashMap<Integer, TaskGrouping> mAffinitiesGroups = new HashMap<Integer, TaskGrouping>();
189 
TaskStack()190     public TaskStack() {
191         this(0);
192     }
193 
TaskStack(int stackId)194     public TaskStack(int stackId) {
195         id = stackId;
196     }
197 
198     /** Sets the callbacks for this task stack. */
setCallbacks(TaskStackCallbacks cb)199     public void setCallbacks(TaskStackCallbacks cb) {
200         mCb = cb;
201     }
202 
203     /** Sets the bounds of this stack. */
setBounds(Rect stackBounds, Rect displayBounds)204     public void setBounds(Rect stackBounds, Rect displayBounds) {
205         this.stackBounds.set(stackBounds);
206         this.displayBounds.set(displayBounds);
207     }
208 
209     /** Resets this TaskStack. */
reset()210     public void reset() {
211         mCb = null;
212         mTaskList.reset();
213         mGroups.clear();
214         mAffinitiesGroups.clear();
215     }
216 
217     /** Adds a new task */
addTask(Task t)218     public void addTask(Task t) {
219         mTaskList.add(t);
220         if (mCb != null) {
221             mCb.onStackTaskAdded(this, t);
222         }
223     }
224 
225     /** Does the actual work associated with removing the task. */
removeTaskImpl(Task t)226     void removeTaskImpl(Task t) {
227         // Remove the task from the list
228         mTaskList.remove(t);
229         // Remove it from the group as well, and if it is empty, remove the group
230         TaskGrouping group = t.group;
231         group.removeTask(t);
232         if (group.getTaskCount() == 0) {
233             removeGroup(group);
234         }
235         // Update the lock-to-app state
236         t.lockToThisTask = false;
237     }
238 
239     /** Removes a task */
removeTask(Task t)240     public void removeTask(Task t) {
241         if (mTaskList.contains(t)) {
242             removeTaskImpl(t);
243             Task newFrontMostTask = getFrontMostTask();
244             if (newFrontMostTask != null && newFrontMostTask.lockToTaskEnabled) {
245                 newFrontMostTask.lockToThisTask = true;
246             }
247             if (mCb != null) {
248                 // Notify that a task has been removed
249                 mCb.onStackTaskRemoved(this, t, newFrontMostTask);
250             }
251         }
252     }
253 
254     /** Removes all tasks */
removeAllTasks()255     public void removeAllTasks() {
256         ArrayList<Task> taskList = new ArrayList<Task>(mTaskList.getTasks());
257         int taskCount = taskList.size();
258         for (int i = taskCount - 1; i >= 0; i--) {
259             Task t = taskList.get(i);
260             removeTaskImpl(t);
261         }
262         if (mCb != null) {
263             // Notify that all tasks have been removed
264             mCb.onStackAllTasksRemoved(this, taskList);
265         }
266     }
267 
268     /** Sets a few tasks in one go */
setTasks(List<Task> tasks)269     public void setTasks(List<Task> tasks) {
270         ArrayList<Task> taskList = mTaskList.getTasks();
271         int taskCount = taskList.size();
272         for (int i = taskCount - 1; i >= 0; i--) {
273             Task t = taskList.get(i);
274             removeTaskImpl(t);
275             if (mCb != null) {
276                 // Notify that a task has been removed
277                 mCb.onStackTaskRemoved(this, t, null);
278             }
279         }
280         mTaskList.set(tasks);
281         for (Task t : tasks) {
282             if (mCb != null) {
283                 mCb.onStackTaskAdded(this, t);
284             }
285         }
286     }
287 
288     /** Gets the front task */
getFrontMostTask()289     public Task getFrontMostTask() {
290         if (mTaskList.size() == 0) return null;
291         return mTaskList.getTasks().get(mTaskList.size() - 1);
292     }
293 
294     /** Gets the task keys */
getTaskKeys()295     public ArrayList<Task.TaskKey> getTaskKeys() {
296         ArrayList<Task.TaskKey> taskKeys = new ArrayList<Task.TaskKey>();
297         ArrayList<Task> tasks = mTaskList.getTasks();
298         int taskCount = tasks.size();
299         for (int i = 0; i < taskCount; i++) {
300             taskKeys.add(tasks.get(i).key);
301         }
302         return taskKeys;
303     }
304 
305     /** Gets the tasks */
getTasks()306     public ArrayList<Task> getTasks() {
307         return mTaskList.getTasks();
308     }
309 
310     /** Gets the number of tasks */
getTaskCount()311     public int getTaskCount() {
312         return mTaskList.size();
313     }
314 
315     /** Returns the index of this task in this current task stack */
indexOfTask(Task t)316     public int indexOfTask(Task t) {
317         return mTaskList.indexOf(t);
318     }
319 
320     /** Finds the task with the specified task id. */
findTaskWithId(int taskId)321     public Task findTaskWithId(int taskId) {
322         ArrayList<Task> tasks = mTaskList.getTasks();
323         int taskCount = tasks.size();
324         for (int i = 0; i < taskCount; i++) {
325             Task task = tasks.get(i);
326             if (task.key.id == taskId) {
327                 return task;
328             }
329         }
330         return null;
331     }
332 
333     /******** Filtering ********/
334 
335     /** Filters the stack into tasks similar to the one specified */
filterTasks(final Task t)336     public void filterTasks(final Task t) {
337         ArrayList<Task> oldStack = new ArrayList<Task>(mTaskList.getTasks());
338 
339         // Set the task list filter
340         boolean filtered = mTaskList.setFilter(new TaskFilter() {
341             @Override
342             public boolean acceptTask(Task at, int i) {
343                 return t.key.baseIntent.getComponent().getPackageName().equals(
344                         at.key.baseIntent.getComponent().getPackageName());
345             }
346         });
347         if (filtered && mCb != null) {
348             mCb.onStackFiltered(this, oldStack, t);
349         }
350     }
351 
352     /** Unfilters the current stack */
unfilterTasks()353     public void unfilterTasks() {
354         ArrayList<Task> oldStack = new ArrayList<Task>(mTaskList.getTasks());
355 
356         // Unset the filter, then update the virtual scroll
357         mTaskList.removeFilter();
358         if (mCb != null) {
359             mCb.onStackUnfiltered(this, oldStack);
360         }
361     }
362 
363     /** Returns whether tasks are currently filtered */
hasFilteredTasks()364     public boolean hasFilteredTasks() {
365         return mTaskList.hasFilter();
366     }
367 
368     /******** Grouping ********/
369 
370     /** Adds a group to the set */
addGroup(TaskGrouping group)371     public void addGroup(TaskGrouping group) {
372         mGroups.add(group);
373         mAffinitiesGroups.put(group.affiliation, group);
374     }
375 
removeGroup(TaskGrouping group)376     public void removeGroup(TaskGrouping group) {
377         mGroups.remove(group);
378         mAffinitiesGroups.remove(group.affiliation);
379     }
380 
381     /** Returns the group with the specified affiliation. */
getGroupWithAffiliation(int affiliation)382     public TaskGrouping getGroupWithAffiliation(int affiliation) {
383         return mAffinitiesGroups.get(affiliation);
384     }
385 
386     /**
387      * Temporary: This method will simulate affiliation groups by
388      */
createAffiliatedGroupings(RecentsConfiguration config)389     public void createAffiliatedGroupings(RecentsConfiguration config) {
390         if (Constants.DebugFlags.App.EnableSimulatedTaskGroups) {
391             HashMap<Task.TaskKey, Task> taskMap = new HashMap<Task.TaskKey, Task>();
392             // Sort all tasks by increasing firstActiveTime of the task
393             ArrayList<Task> tasks = mTaskList.getTasks();
394             Collections.sort(tasks, new Comparator<Task>() {
395                 @Override
396                 public int compare(Task task, Task task2) {
397                     return (int) (task.key.firstActiveTime - task2.key.firstActiveTime);
398                 }
399             });
400             // Create groups when sequential packages are the same
401             NamedCounter counter = new NamedCounter("task-group", "");
402             int taskCount = tasks.size();
403             String prevPackage = "";
404             int prevAffiliation = -1;
405             Random r = new Random();
406             int groupCountDown = Constants.DebugFlags.App.TaskAffiliationsGroupCount;
407             for (int i = 0; i < taskCount; i++) {
408                 Task t = tasks.get(i);
409                 String packageName = t.key.baseIntent.getComponent().getPackageName();
410                 packageName = "pkg";
411                 TaskGrouping group;
412                 if (packageName.equals(prevPackage) && groupCountDown > 0) {
413                     group = getGroupWithAffiliation(prevAffiliation);
414                     groupCountDown--;
415                 } else {
416                     int affiliation = IndividualTaskIdOffset + t.key.id;
417                     group = new TaskGrouping(affiliation);
418                     addGroup(group);
419                     prevAffiliation = affiliation;
420                     prevPackage = packageName;
421                     groupCountDown = Constants.DebugFlags.App.TaskAffiliationsGroupCount;
422                 }
423                 group.addTask(t);
424                 taskMap.put(t.key, t);
425             }
426             // Sort groups by increasing latestActiveTime of the group
427             Collections.sort(mGroups, new Comparator<TaskGrouping>() {
428                 @Override
429                 public int compare(TaskGrouping taskGrouping, TaskGrouping taskGrouping2) {
430                     return (int) (taskGrouping.latestActiveTimeInGroup -
431                             taskGrouping2.latestActiveTimeInGroup);
432                 }
433             });
434             // Sort group tasks by increasing firstActiveTime of the task, and also build a new list of
435             // tasks
436             int taskIndex = 0;
437             int groupCount = mGroups.size();
438             for (int i = 0; i < groupCount; i++) {
439                 TaskGrouping group = mGroups.get(i);
440                 Collections.sort(group.mTaskKeys, new Comparator<Task.TaskKey>() {
441                     @Override
442                     public int compare(Task.TaskKey taskKey, Task.TaskKey taskKey2) {
443                         return (int) (taskKey.firstActiveTime - taskKey2.firstActiveTime);
444                     }
445                 });
446                 ArrayList<Task.TaskKey> groupTasks = group.mTaskKeys;
447                 int groupTaskCount = groupTasks.size();
448                 for (int j = 0; j < groupTaskCount; j++) {
449                     tasks.set(taskIndex, taskMap.get(groupTasks.get(j)));
450                     taskIndex++;
451                 }
452             }
453             mTaskList.set(tasks);
454         } else {
455             // Create the task groups
456             HashMap<Task.TaskKey, Task> tasksMap = new HashMap<Task.TaskKey, Task>();
457             ArrayList<Task> tasks = mTaskList.getTasks();
458             int taskCount = tasks.size();
459             for (int i = 0; i < taskCount; i++) {
460                 Task t = tasks.get(i);
461                 TaskGrouping group;
462                 int affiliation = t.taskAffiliation > 0 ? t.taskAffiliation :
463                         IndividualTaskIdOffset + t.key.id;
464                 if (mAffinitiesGroups.containsKey(affiliation)) {
465                     group = getGroupWithAffiliation(affiliation);
466                 } else {
467                     group = new TaskGrouping(affiliation);
468                     addGroup(group);
469                 }
470                 group.addTask(t);
471                 tasksMap.put(t.key, t);
472             }
473             // Update the task colors for each of the groups
474             float minAlpha = config.taskBarViewAffiliationColorMinAlpha;
475             int taskGroupCount = mGroups.size();
476             for (int i = 0; i < taskGroupCount; i++) {
477                 TaskGrouping group = mGroups.get(i);
478                 taskCount = group.getTaskCount();
479                 // Ignore the groups that only have one task
480                 if (taskCount <= 1) continue;
481                 // Calculate the group color distribution
482                 int affiliationColor = tasksMap.get(group.mTaskKeys.get(0)).taskAffiliationColor;
483                 float alphaStep = (1f - minAlpha) / taskCount;
484                 float alpha = 1f;
485                 for (int j = 0; j < taskCount; j++) {
486                     Task t = tasksMap.get(group.mTaskKeys.get(j));
487                     t.colorPrimary = Utilities.getColorWithOverlay(affiliationColor, Color.WHITE,
488                             alpha);
489                     alpha -= alphaStep;
490                 }
491             }
492         }
493     }
494 
495     @Override
toString()496     public String toString() {
497         String str = "Tasks:\n";
498         for (Task t : mTaskList.getTasks()) {
499             str += "  " + t.toString() + "\n";
500         }
501         return str;
502     }
503 }