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