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