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 static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT; 20 import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT; 21 import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID; 22 import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID; 23 import static android.view.WindowManager.DOCKED_BOTTOM; 24 import static android.view.WindowManager.DOCKED_INVALID; 25 import static android.view.WindowManager.DOCKED_LEFT; 26 import static android.view.WindowManager.DOCKED_RIGHT; 27 import static android.view.WindowManager.DOCKED_TOP; 28 29 import android.animation.Animator; 30 import android.animation.AnimatorSet; 31 import android.animation.ObjectAnimator; 32 import android.animation.PropertyValuesHolder; 33 import android.annotation.IntDef; 34 import android.content.ComponentName; 35 import android.content.Context; 36 import android.content.res.Configuration; 37 import android.content.res.Resources; 38 import android.graphics.Canvas; 39 import android.graphics.Color; 40 import android.graphics.Paint; 41 import android.graphics.Point; 42 import android.graphics.Rect; 43 import android.graphics.RectF; 44 import android.graphics.drawable.ColorDrawable; 45 import android.util.ArrayMap; 46 import android.util.ArraySet; 47 import android.util.IntProperty; 48 import android.util.SparseArray; 49 import android.view.animation.Interpolator; 50 51 import com.android.internal.policy.DockedDividerUtils; 52 import com.android.systemui.Interpolators; 53 import com.android.systemui.R; 54 import com.android.systemui.recents.Recents; 55 import com.android.systemui.recents.RecentsDebugFlags; 56 import com.android.systemui.recents.misc.NamedCounter; 57 import com.android.systemui.recents.misc.SystemServicesProxy; 58 import com.android.systemui.recents.misc.Utilities; 59 import com.android.systemui.recents.views.AnimationProps; 60 import com.android.systemui.recents.views.DropTarget; 61 import com.android.systemui.recents.views.TaskStackLayoutAlgorithm; 62 63 import java.io.PrintWriter; 64 import java.lang.annotation.Retention; 65 import java.lang.annotation.RetentionPolicy; 66 import java.util.ArrayList; 67 import java.util.Collections; 68 import java.util.Comparator; 69 import java.util.List; 70 import java.util.Random; 71 72 73 /** 74 * An interface for a task filter to query whether a particular task should show in a stack. 75 */ 76 interface TaskFilter { 77 /** Returns whether the filter accepts the specified task */ 78 public boolean acceptTask(SparseArray<Task> taskIdMap, Task t, int index); 79 } 80 81 /** 82 * A list of filtered tasks. 83 */ 84 class FilteredTaskList { 85 86 ArrayList<Task> mTasks = new ArrayList<>(); 87 ArrayList<Task> mFilteredTasks = new ArrayList<>(); 88 ArrayMap<Task.TaskKey, Integer> mTaskIndices = new ArrayMap<>(); 89 TaskFilter mFilter; 90 91 /** Sets the task filter, saving the current touch state */ 92 boolean setFilter(TaskFilter filter) { 93 ArrayList<Task> prevFilteredTasks = new ArrayList<>(mFilteredTasks); 94 mFilter = filter; 95 updateFilteredTasks(); 96 if (!prevFilteredTasks.equals(mFilteredTasks)) { 97 return true; 98 } else { 99 return false; 100 } 101 } 102 103 /** Removes the task filter and returns the previous touch state */ 104 void removeFilter() { 105 mFilter = null; 106 updateFilteredTasks(); 107 } 108 109 /** Adds a new task to the task list */ 110 void add(Task t) { 111 mTasks.add(t); 112 updateFilteredTasks(); 113 } 114 115 /** 116 * Moves the given task. 117 */ 118 public void moveTaskToStack(Task task, int insertIndex, int newStackId) { 119 int taskIndex = indexOf(task); 120 if (taskIndex != insertIndex) { 121 mTasks.remove(taskIndex); 122 if (taskIndex < insertIndex) { 123 insertIndex--; 124 } 125 mTasks.add(insertIndex, task); 126 } 127 128 // Update the stack id now, after we've moved the task, and before we update the 129 // filtered tasks 130 task.setStackId(newStackId); 131 updateFilteredTasks(); 132 } 133 134 /** Sets the list of tasks */ 135 void set(List<Task> tasks) { 136 mTasks.clear(); 137 mTasks.addAll(tasks); 138 updateFilteredTasks(); 139 } 140 141 /** Removes a task from the base list only if it is in the filtered list */ 142 boolean remove(Task t) { 143 if (mFilteredTasks.contains(t)) { 144 boolean removed = mTasks.remove(t); 145 updateFilteredTasks(); 146 return removed; 147 } 148 return false; 149 } 150 151 /** Returns the index of this task in the list of filtered tasks */ 152 int indexOf(Task t) { 153 if (t != null && mTaskIndices.containsKey(t.key)) { 154 return mTaskIndices.get(t.key); 155 } 156 return -1; 157 } 158 159 /** Returns the size of the list of filtered tasks */ 160 int size() { 161 return mFilteredTasks.size(); 162 } 163 164 /** Returns whether the filtered list contains this task */ 165 boolean contains(Task t) { 166 return mTaskIndices.containsKey(t.key); 167 } 168 169 /** Updates the list of filtered tasks whenever the base task list changes */ 170 private void updateFilteredTasks() { 171 mFilteredTasks.clear(); 172 if (mFilter != null) { 173 // Create a sparse array from task id to Task 174 SparseArray<Task> taskIdMap = new SparseArray<>(); 175 int taskCount = mTasks.size(); 176 for (int i = 0; i < taskCount; i++) { 177 Task t = mTasks.get(i); 178 taskIdMap.put(t.key.id, t); 179 } 180 181 for (int i = 0; i < taskCount; i++) { 182 Task t = mTasks.get(i); 183 if (mFilter.acceptTask(taskIdMap, t, i)) { 184 mFilteredTasks.add(t); 185 } 186 } 187 } else { 188 mFilteredTasks.addAll(mTasks); 189 } 190 updateFilteredTaskIndices(); 191 } 192 193 /** Updates the mapping of tasks to indices. */ 194 private void updateFilteredTaskIndices() { 195 int taskCount = mFilteredTasks.size(); 196 mTaskIndices.clear(); 197 for (int i = 0; i < taskCount; i++) { 198 Task t = mFilteredTasks.get(i); 199 mTaskIndices.put(t.key, i); 200 } 201 } 202 203 /** Returns whether this task list is filtered */ 204 boolean hasFilter() { 205 return (mFilter != null); 206 } 207 208 /** Returns the list of filtered tasks */ 209 ArrayList<Task> getTasks() { 210 return mFilteredTasks; 211 } 212 } 213 214 /** 215 * The task stack contains a list of multiple tasks. 216 */ 217 public class TaskStack { 218 219 private static final String TAG = "TaskStack"; 220 221 /** Task stack callbacks */ 222 public interface TaskStackCallbacks { 223 /** 224 * Notifies when a new task has been added to the stack. 225 */ 226 void onStackTaskAdded(TaskStack stack, Task newTask); 227 228 /** 229 * Notifies when a task has been removed from the stack. 230 */ 231 void onStackTaskRemoved(TaskStack stack, Task removedTask, Task newFrontMostTask, 232 AnimationProps animation, boolean fromDockGesture); 233 234 /** 235 * Notifies when all tasks have been removed from the stack. 236 */ 237 void onStackTasksRemoved(TaskStack stack); 238 239 /** 240 * Notifies when tasks in the stack have been updated. 241 */ 242 void onStackTasksUpdated(TaskStack stack); 243 } 244 245 /** 246 * The various possible dock states when dragging and dropping a task. 247 */ 248 public static class DockState implements DropTarget { 249 250 // The rotation to apply to the hint text 251 @Retention(RetentionPolicy.SOURCE) 252 @IntDef({HORIZONTAL, VERTICAL}) 253 public @interface TextOrientation {} 254 private static final int HORIZONTAL = 0; 255 private static final int VERTICAL = 1; 256 257 private static final int DOCK_AREA_ALPHA = 80; 258 public static final DockState NONE = new DockState(DOCKED_INVALID, -1, 80, 255, HORIZONTAL, 259 null, null, null); 260 public static final DockState LEFT = new DockState(DOCKED_LEFT, 261 DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT, DOCK_AREA_ALPHA, 0, VERTICAL, 262 new RectF(0, 0, 0.125f, 1), new RectF(0, 0, 0.125f, 1), 263 new RectF(0, 0, 0.5f, 1)); 264 public static final DockState TOP = new DockState(DOCKED_TOP, 265 DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT, DOCK_AREA_ALPHA, 0, HORIZONTAL, 266 new RectF(0, 0, 1, 0.125f), new RectF(0, 0, 1, 0.125f), 267 new RectF(0, 0, 1, 0.5f)); 268 public static final DockState RIGHT = new DockState(DOCKED_RIGHT, 269 DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT, DOCK_AREA_ALPHA, 0, VERTICAL, 270 new RectF(0.875f, 0, 1, 1), new RectF(0.875f, 0, 1, 1), 271 new RectF(0.5f, 0, 1, 1)); 272 public static final DockState BOTTOM = new DockState(DOCKED_BOTTOM, 273 DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT, DOCK_AREA_ALPHA, 0, HORIZONTAL, 274 new RectF(0, 0.875f, 1, 1), new RectF(0, 0.875f, 1, 1), 275 new RectF(0, 0.5f, 1, 1)); 276 277 @Override 278 public boolean acceptsDrop(int x, int y, int width, int height, boolean isCurrentTarget) { 279 return isCurrentTarget 280 ? areaContainsPoint(expandedTouchDockArea, width, height, x, y) 281 : areaContainsPoint(touchArea, width, height, x, y); 282 } 283 284 // Represents the view state of this dock state 285 public static class ViewState { 286 private static final IntProperty<ViewState> HINT_ALPHA = 287 new IntProperty<ViewState>("drawableAlpha") { 288 @Override 289 public void setValue(ViewState object, int alpha) { 290 object.mHintTextAlpha = alpha; 291 object.dockAreaOverlay.invalidateSelf(); 292 } 293 294 @Override 295 public Integer get(ViewState object) { 296 return object.mHintTextAlpha; 297 } 298 }; 299 300 public final int dockAreaAlpha; 301 public final ColorDrawable dockAreaOverlay; 302 public final int hintTextAlpha; 303 public final int hintTextOrientation; 304 305 private final int mHintTextResId; 306 private String mHintText; 307 private Paint mHintTextPaint; 308 private Point mHintTextBounds = new Point(); 309 private int mHintTextAlpha = 255; 310 private AnimatorSet mDockAreaOverlayAnimator; 311 private Rect mTmpRect = new Rect(); 312 313 private ViewState(int areaAlpha, int hintAlpha, @TextOrientation int hintOrientation, 314 int hintTextResId) { 315 dockAreaAlpha = areaAlpha; 316 dockAreaOverlay = new ColorDrawable(0xFFffffff); 317 dockAreaOverlay.setAlpha(0); 318 hintTextAlpha = hintAlpha; 319 hintTextOrientation = hintOrientation; 320 mHintTextResId = hintTextResId; 321 mHintTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 322 mHintTextPaint.setColor(Color.WHITE); 323 } 324 325 /** 326 * Updates the view state with the given context. 327 */ 328 public void update(Context context) { 329 Resources res = context.getResources(); 330 mHintText = context.getString(mHintTextResId); 331 mHintTextPaint.setTextSize(res.getDimensionPixelSize( 332 R.dimen.recents_drag_hint_text_size)); 333 mHintTextPaint.getTextBounds(mHintText, 0, mHintText.length(), mTmpRect); 334 mHintTextBounds.set((int) mHintTextPaint.measureText(mHintText), mTmpRect.height()); 335 } 336 337 /** 338 * Draws the current view state. 339 */ 340 public void draw(Canvas canvas) { 341 // Draw the overlay background 342 if (dockAreaOverlay.getAlpha() > 0) { 343 dockAreaOverlay.draw(canvas); 344 } 345 346 // Draw the hint text 347 if (mHintTextAlpha > 0) { 348 Rect bounds = dockAreaOverlay.getBounds(); 349 int x = bounds.left + (bounds.width() - mHintTextBounds.x) / 2; 350 int y = bounds.top + (bounds.height() + mHintTextBounds.y) / 2; 351 mHintTextPaint.setAlpha(mHintTextAlpha); 352 if (hintTextOrientation == VERTICAL) { 353 canvas.save(); 354 canvas.rotate(-90f, bounds.centerX(), bounds.centerY()); 355 } 356 canvas.drawText(mHintText, x, y, mHintTextPaint); 357 if (hintTextOrientation == VERTICAL) { 358 canvas.restore(); 359 } 360 } 361 } 362 363 /** 364 * Creates a new bounds and alpha animation. 365 */ 366 public void startAnimation(Rect bounds, int areaAlpha, int hintAlpha, int duration, 367 Interpolator interpolator, boolean animateAlpha, boolean animateBounds) { 368 if (mDockAreaOverlayAnimator != null) { 369 mDockAreaOverlayAnimator.cancel(); 370 } 371 372 ObjectAnimator anim; 373 ArrayList<Animator> animators = new ArrayList<>(); 374 if (dockAreaOverlay.getAlpha() != areaAlpha) { 375 if (animateAlpha) { 376 anim = ObjectAnimator.ofInt(dockAreaOverlay, 377 Utilities.DRAWABLE_ALPHA, dockAreaOverlay.getAlpha(), areaAlpha); 378 anim.setDuration(duration); 379 anim.setInterpolator(interpolator); 380 animators.add(anim); 381 } else { 382 dockAreaOverlay.setAlpha(areaAlpha); 383 } 384 } 385 if (mHintTextAlpha != hintAlpha) { 386 if (animateAlpha) { 387 anim = ObjectAnimator.ofInt(this, HINT_ALPHA, mHintTextAlpha, 388 hintAlpha); 389 anim.setDuration(150); 390 anim.setInterpolator(hintAlpha > mHintTextAlpha 391 ? Interpolators.ALPHA_IN 392 : Interpolators.ALPHA_OUT); 393 animators.add(anim); 394 } else { 395 mHintTextAlpha = hintAlpha; 396 dockAreaOverlay.invalidateSelf(); 397 } 398 } 399 if (bounds != null && !dockAreaOverlay.getBounds().equals(bounds)) { 400 if (animateBounds) { 401 PropertyValuesHolder prop = PropertyValuesHolder.ofObject( 402 Utilities.DRAWABLE_RECT, Utilities.RECT_EVALUATOR, 403 new Rect(dockAreaOverlay.getBounds()), bounds); 404 anim = ObjectAnimator.ofPropertyValuesHolder(dockAreaOverlay, prop); 405 anim.setDuration(duration); 406 anim.setInterpolator(interpolator); 407 animators.add(anim); 408 } else { 409 dockAreaOverlay.setBounds(bounds); 410 } 411 } 412 if (!animators.isEmpty()) { 413 mDockAreaOverlayAnimator = new AnimatorSet(); 414 mDockAreaOverlayAnimator.playTogether(animators); 415 mDockAreaOverlayAnimator.start(); 416 } 417 } 418 } 419 420 public final int dockSide; 421 public final int createMode; 422 public final ViewState viewState; 423 private final RectF touchArea; 424 private final RectF dockArea; 425 private final RectF expandedTouchDockArea; 426 427 /** 428 * @param createMode used to pass to ActivityManager to dock the task 429 * @param touchArea the area in which touch will initiate this dock state 430 * @param dockArea the visible dock area 431 * @param expandedTouchDockArea the areain which touch will continue to dock after entering 432 * the initial touch area. This is also the new dock area to 433 * draw. 434 */ 435 DockState(int dockSide, int createMode, int dockAreaAlpha, int hintTextAlpha, 436 @TextOrientation int hintTextOrientation, RectF touchArea, RectF dockArea, 437 RectF expandedTouchDockArea) { 438 this.dockSide = dockSide; 439 this.createMode = createMode; 440 this.viewState = new ViewState(dockAreaAlpha, hintTextAlpha, hintTextOrientation, 441 R.string.recents_drag_hint_message); 442 this.dockArea = dockArea; 443 this.touchArea = touchArea; 444 this.expandedTouchDockArea = expandedTouchDockArea; 445 } 446 447 /** 448 * Updates the dock state with the given context. 449 */ 450 public void update(Context context) { 451 viewState.update(context); 452 } 453 454 /** 455 * Returns whether {@param x} and {@param y} are contained in the area scaled to the 456 * given {@param width} and {@param height}. 457 */ 458 public boolean areaContainsPoint(RectF area, int width, int height, float x, float y) { 459 int left = (int) (area.left * width); 460 int top = (int) (area.top * height); 461 int right = (int) (area.right * width); 462 int bottom = (int) (area.bottom * height); 463 return x >= left && y >= top && x <= right && y <= bottom; 464 } 465 466 /** 467 * Returns the docked task bounds with the given {@param width} and {@param height}. 468 */ 469 public Rect getPreDockedBounds(int width, int height) { 470 return new Rect((int) (dockArea.left * width), (int) (dockArea.top * height), 471 (int) (dockArea.right * width), (int) (dockArea.bottom * height)); 472 } 473 474 /** 475 * Returns the expanded docked task bounds with the given {@param width} and 476 * {@param height}. 477 */ 478 public Rect getDockedBounds(int width, int height, int dividerSize, Rect insets, 479 Resources res) { 480 // Calculate the docked task bounds 481 boolean isHorizontalDivision = 482 res.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; 483 int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision, 484 insets, width, height, dividerSize); 485 Rect newWindowBounds = new Rect(); 486 DockedDividerUtils.calculateBoundsForPosition(position, dockSide, newWindowBounds, 487 width, height, dividerSize); 488 return newWindowBounds; 489 } 490 491 /** 492 * Returns the task stack bounds with the given {@param width} and 493 * {@param height}. 494 */ 495 public Rect getDockedTaskStackBounds(Rect displayRect, int width, int height, 496 int dividerSize, Rect insets, TaskStackLayoutAlgorithm layoutAlgorithm, 497 Resources res, Rect windowRectOut) { 498 // Calculate the inverse docked task bounds 499 boolean isHorizontalDivision = 500 res.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; 501 int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision, 502 insets, width, height, dividerSize); 503 DockedDividerUtils.calculateBoundsForPosition(position, 504 DockedDividerUtils.invertDockSide(dockSide), windowRectOut, width, height, 505 dividerSize); 506 507 // Calculate the task stack bounds from the new window bounds 508 Rect taskStackBounds = new Rect(); 509 // If the task stack bounds is specifically under the dock area, then ignore the top 510 // inset 511 int top = dockArea.bottom < 1f 512 ? 0 513 : insets.top; 514 layoutAlgorithm.getTaskStackBounds(displayRect, windowRectOut, top, insets.right, 515 taskStackBounds); 516 return taskStackBounds; 517 } 518 } 519 520 // A comparator that sorts tasks by their freeform state 521 private Comparator<Task> FREEFORM_COMPARATOR = new Comparator<Task>() { 522 @Override 523 public int compare(Task o1, Task o2) { 524 if (o1.isFreeformTask() && !o2.isFreeformTask()) { 525 return 1; 526 } else if (o2.isFreeformTask() && !o1.isFreeformTask()) { 527 return -1; 528 } 529 return Long.compare(o1.temporarySortIndexInStack, o2.temporarySortIndexInStack); 530 } 531 }; 532 533 534 // The task offset to apply to a task id as a group affiliation 535 static final int IndividualTaskIdOffset = 1 << 16; 536 537 ArrayList<Task> mRawTaskList = new ArrayList<>(); 538 FilteredTaskList mStackTaskList = new FilteredTaskList(); 539 TaskStackCallbacks mCb; 540 541 ArrayList<TaskGrouping> mGroups = new ArrayList<>(); 542 ArrayMap<Integer, TaskGrouping> mAffinitiesGroups = new ArrayMap<>(); 543 544 public TaskStack() { 545 // Ensure that we only show non-docked tasks 546 mStackTaskList.setFilter(new TaskFilter() { 547 @Override 548 public boolean acceptTask(SparseArray<Task> taskIdMap, Task t, int index) { 549 if (RecentsDebugFlags.Static.EnableAffiliatedTaskGroups) { 550 if (t.isAffiliatedTask()) { 551 // If this task is affiliated with another parent in the stack, then the 552 // historical state of this task depends on the state of the parent task 553 Task parentTask = taskIdMap.get(t.affiliationTaskId); 554 if (parentTask != null) { 555 t = parentTask; 556 } 557 } 558 } 559 return t.isStackTask; 560 } 561 }); 562 } 563 564 /** Sets the callbacks for this task stack. */ 565 public void setCallbacks(TaskStackCallbacks cb) { 566 mCb = cb; 567 } 568 569 /** 570 * Moves the given task to either the front of the freeform workspace or the stack. 571 */ 572 public void moveTaskToStack(Task task, int newStackId) { 573 // Find the index to insert into 574 ArrayList<Task> taskList = mStackTaskList.getTasks(); 575 int taskCount = taskList.size(); 576 if (!task.isFreeformTask() && (newStackId == FREEFORM_WORKSPACE_STACK_ID)) { 577 // Insert freeform tasks at the front 578 mStackTaskList.moveTaskToStack(task, taskCount, newStackId); 579 } else if (task.isFreeformTask() && (newStackId == FULLSCREEN_WORKSPACE_STACK_ID)) { 580 // Insert after the first stacked task 581 int insertIndex = 0; 582 for (int i = taskCount - 1; i >= 0; i--) { 583 if (!taskList.get(i).isFreeformTask()) { 584 insertIndex = i + 1; 585 break; 586 } 587 } 588 mStackTaskList.moveTaskToStack(task, insertIndex, newStackId); 589 } 590 } 591 592 /** Does the actual work associated with removing the task. */ 593 void removeTaskImpl(FilteredTaskList taskList, Task t) { 594 // Remove the task from the list 595 taskList.remove(t); 596 // Remove it from the group as well, and if it is empty, remove the group 597 TaskGrouping group = t.group; 598 if (group != null) { 599 group.removeTask(t); 600 if (group.getTaskCount() == 0) { 601 removeGroup(group); 602 } 603 } 604 } 605 606 /** 607 * Removes a task from the stack, with an additional {@param animation} hint to the callbacks on 608 * how they should update themselves. 609 */ 610 public void removeTask(Task t, AnimationProps animation, boolean fromDockGesture) { 611 if (mStackTaskList.contains(t)) { 612 removeTaskImpl(mStackTaskList, t); 613 Task newFrontMostTask = getStackFrontMostTask(false /* includeFreeform */); 614 if (mCb != null) { 615 // Notify that a task has been removed 616 mCb.onStackTaskRemoved(this, t, newFrontMostTask, animation, 617 fromDockGesture); 618 } 619 } 620 mRawTaskList.remove(t); 621 } 622 623 /** 624 * Removes all tasks from the stack. 625 */ 626 public void removeAllTasks() { 627 ArrayList<Task> tasks = mStackTaskList.getTasks(); 628 for (int i = tasks.size() - 1; i >= 0; i--) { 629 Task t = tasks.get(i); 630 removeTaskImpl(mStackTaskList, t); 631 mRawTaskList.remove(t); 632 } 633 if (mCb != null) { 634 // Notify that all tasks have been removed 635 mCb.onStackTasksRemoved(this); 636 } 637 } 638 639 /** 640 * Sets a few tasks in one go, without calling any callbacks. 641 * 642 * @param tasks the new set of tasks to replace the current set. 643 * @param notifyStackChanges whether or not to callback on specific changes to the list of tasks. 644 */ 645 public void setTasks(Context context, List<Task> tasks, boolean notifyStackChanges) { 646 // Compute a has set for each of the tasks 647 ArrayMap<Task.TaskKey, Task> currentTasksMap = createTaskKeyMapFromList(mRawTaskList); 648 ArrayMap<Task.TaskKey, Task> newTasksMap = createTaskKeyMapFromList(tasks); 649 ArrayList<Task> addedTasks = new ArrayList<>(); 650 ArrayList<Task> removedTasks = new ArrayList<>(); 651 ArrayList<Task> allTasks = new ArrayList<>(); 652 653 // Disable notifications if there are no callbacks 654 if (mCb == null) { 655 notifyStackChanges = false; 656 } 657 658 // Remove any tasks that no longer exist 659 int taskCount = mRawTaskList.size(); 660 for (int i = taskCount - 1; i >= 0; i--) { 661 Task task = mRawTaskList.get(i); 662 if (!newTasksMap.containsKey(task.key)) { 663 if (notifyStackChanges) { 664 removedTasks.add(task); 665 } 666 } 667 task.setGroup(null); 668 } 669 670 // Add any new tasks 671 taskCount = tasks.size(); 672 for (int i = 0; i < taskCount; i++) { 673 Task newTask = tasks.get(i); 674 Task currentTask = currentTasksMap.get(newTask.key); 675 if (currentTask == null && notifyStackChanges) { 676 addedTasks.add(newTask); 677 } else if (currentTask != null) { 678 // The current task has bound callbacks, so just copy the data from the new task 679 // state and add it back into the list 680 currentTask.copyFrom(newTask); 681 newTask = currentTask; 682 } 683 allTasks.add(newTask); 684 } 685 686 // Sort all the tasks to ensure they are ordered correctly 687 for (int i = allTasks.size() - 1; i >= 0; i--) { 688 allTasks.get(i).temporarySortIndexInStack = i; 689 } 690 Collections.sort(allTasks, FREEFORM_COMPARATOR); 691 692 mStackTaskList.set(allTasks); 693 mRawTaskList = allTasks; 694 695 // Update the affiliated groupings 696 createAffiliatedGroupings(context); 697 698 // Only callback for the removed tasks after the stack has updated 699 int removedTaskCount = removedTasks.size(); 700 Task newFrontMostTask = getStackFrontMostTask(false); 701 for (int i = 0; i < removedTaskCount; i++) { 702 mCb.onStackTaskRemoved(this, removedTasks.get(i), newFrontMostTask, 703 AnimationProps.IMMEDIATE, false /* fromDockGesture */); 704 } 705 706 // Only callback for the newly added tasks after this stack has been updated 707 int addedTaskCount = addedTasks.size(); 708 for (int i = 0; i < addedTaskCount; i++) { 709 mCb.onStackTaskAdded(this, addedTasks.get(i)); 710 } 711 712 // Notify that the task stack has been updated 713 if (notifyStackChanges) { 714 mCb.onStackTasksUpdated(this); 715 } 716 } 717 718 /** 719 * Gets the front-most task in the stack. 720 */ 721 public Task getStackFrontMostTask(boolean includeFreeformTasks) { 722 ArrayList<Task> stackTasks = mStackTaskList.getTasks(); 723 if (stackTasks.isEmpty()) { 724 return null; 725 } 726 for (int i = stackTasks.size() - 1; i >= 0; i--) { 727 Task task = stackTasks.get(i); 728 if (!task.isFreeformTask() || includeFreeformTasks) { 729 return task; 730 } 731 } 732 return null; 733 } 734 735 /** Gets the task keys */ 736 public ArrayList<Task.TaskKey> getTaskKeys() { 737 ArrayList<Task.TaskKey> taskKeys = new ArrayList<>(); 738 ArrayList<Task> tasks = computeAllTasksList(); 739 int taskCount = tasks.size(); 740 for (int i = 0; i < taskCount; i++) { 741 Task task = tasks.get(i); 742 taskKeys.add(task.key); 743 } 744 return taskKeys; 745 } 746 747 /** 748 * Returns the set of "active" (non-historical) tasks in the stack that have been used recently. 749 */ 750 public ArrayList<Task> getStackTasks() { 751 return mStackTaskList.getTasks(); 752 } 753 754 /** 755 * Returns the set of "freeform" tasks in the stack. 756 */ 757 public ArrayList<Task> getFreeformTasks() { 758 ArrayList<Task> freeformTasks = new ArrayList<>(); 759 ArrayList<Task> tasks = mStackTaskList.getTasks(); 760 int taskCount = tasks.size(); 761 for (int i = 0; i < taskCount; i++) { 762 Task task = tasks.get(i); 763 if (task.isFreeformTask()) { 764 freeformTasks.add(task); 765 } 766 } 767 return freeformTasks; 768 } 769 770 /** 771 * Computes a set of all the active and historical tasks. 772 */ 773 public ArrayList<Task> computeAllTasksList() { 774 ArrayList<Task> tasks = new ArrayList<>(); 775 tasks.addAll(mStackTaskList.getTasks()); 776 return tasks; 777 } 778 779 /** 780 * Returns the number of stack and freeform tasks. 781 */ 782 public int getTaskCount() { 783 return mStackTaskList.size(); 784 } 785 786 /** 787 * Returns the number of stack tasks. 788 */ 789 public int getStackTaskCount() { 790 ArrayList<Task> tasks = mStackTaskList.getTasks(); 791 int stackCount = 0; 792 int taskCount = tasks.size(); 793 for (int i = 0; i < taskCount; i++) { 794 Task task = tasks.get(i); 795 if (!task.isFreeformTask()) { 796 stackCount++; 797 } 798 } 799 return stackCount; 800 } 801 802 /** 803 * Returns the number of freeform tasks. 804 */ 805 public int getFreeformTaskCount() { 806 ArrayList<Task> tasks = mStackTaskList.getTasks(); 807 int freeformCount = 0; 808 int taskCount = tasks.size(); 809 for (int i = 0; i < taskCount; i++) { 810 Task task = tasks.get(i); 811 if (task.isFreeformTask()) { 812 freeformCount++; 813 } 814 } 815 return freeformCount; 816 } 817 818 /** 819 * Returns the task in stack tasks which is the launch target. 820 */ 821 public Task getLaunchTarget() { 822 ArrayList<Task> tasks = mStackTaskList.getTasks(); 823 int taskCount = tasks.size(); 824 for (int i = 0; i < taskCount; i++) { 825 Task task = tasks.get(i); 826 if (task.isLaunchTarget) { 827 return task; 828 } 829 } 830 return null; 831 } 832 833 /** Returns the index of this task in this current task stack */ 834 public int indexOfStackTask(Task t) { 835 return mStackTaskList.indexOf(t); 836 } 837 838 /** Finds the task with the specified task id. */ 839 public Task findTaskWithId(int taskId) { 840 ArrayList<Task> tasks = computeAllTasksList(); 841 int taskCount = tasks.size(); 842 for (int i = 0; i < taskCount; i++) { 843 Task task = tasks.get(i); 844 if (task.key.id == taskId) { 845 return task; 846 } 847 } 848 return null; 849 } 850 851 /******** Grouping ********/ 852 853 /** Adds a group to the set */ 854 public void addGroup(TaskGrouping group) { 855 mGroups.add(group); 856 mAffinitiesGroups.put(group.affiliation, group); 857 } 858 859 public void removeGroup(TaskGrouping group) { 860 mGroups.remove(group); 861 mAffinitiesGroups.remove(group.affiliation); 862 } 863 864 /** Returns the group with the specified affiliation. */ 865 public TaskGrouping getGroupWithAffiliation(int affiliation) { 866 return mAffinitiesGroups.get(affiliation); 867 } 868 869 /** 870 * Temporary: This method will simulate affiliation groups 871 */ 872 void createAffiliatedGroupings(Context context) { 873 mGroups.clear(); 874 mAffinitiesGroups.clear(); 875 876 if (RecentsDebugFlags.Static.EnableMockTaskGroups) { 877 ArrayMap<Task.TaskKey, Task> taskMap = new ArrayMap<>(); 878 // Sort all tasks by increasing firstActiveTime of the task 879 ArrayList<Task> tasks = mStackTaskList.getTasks(); 880 Collections.sort(tasks, new Comparator<Task>() { 881 @Override 882 public int compare(Task task, Task task2) { 883 return Long.compare(task.key.firstActiveTime, task2.key.firstActiveTime); 884 } 885 }); 886 // Create groups when sequential packages are the same 887 NamedCounter counter = new NamedCounter("task-group", ""); 888 int taskCount = tasks.size(); 889 String prevPackage = ""; 890 int prevAffiliation = -1; 891 Random r = new Random(); 892 int groupCountDown = RecentsDebugFlags.Static.MockTaskGroupsTaskCount; 893 for (int i = 0; i < taskCount; i++) { 894 Task t = tasks.get(i); 895 String packageName = t.key.getComponent().getPackageName(); 896 packageName = "pkg"; 897 TaskGrouping group; 898 if (packageName.equals(prevPackage) && groupCountDown > 0) { 899 group = getGroupWithAffiliation(prevAffiliation); 900 groupCountDown--; 901 } else { 902 int affiliation = IndividualTaskIdOffset + t.key.id; 903 group = new TaskGrouping(affiliation); 904 addGroup(group); 905 prevAffiliation = affiliation; 906 prevPackage = packageName; 907 groupCountDown = RecentsDebugFlags.Static.MockTaskGroupsTaskCount; 908 } 909 group.addTask(t); 910 taskMap.put(t.key, t); 911 } 912 // Sort groups by increasing latestActiveTime of the group 913 Collections.sort(mGroups, new Comparator<TaskGrouping>() { 914 @Override 915 public int compare(TaskGrouping taskGrouping, TaskGrouping taskGrouping2) { 916 return Long.compare(taskGrouping.latestActiveTimeInGroup, 917 taskGrouping2.latestActiveTimeInGroup); 918 } 919 }); 920 // Sort group tasks by increasing firstActiveTime of the task, and also build a new list 921 // of tasks 922 int taskIndex = 0; 923 int groupCount = mGroups.size(); 924 for (int i = 0; i < groupCount; i++) { 925 TaskGrouping group = mGroups.get(i); 926 Collections.sort(group.mTaskKeys, new Comparator<Task.TaskKey>() { 927 @Override 928 public int compare(Task.TaskKey taskKey, Task.TaskKey taskKey2) { 929 return Long.compare(taskKey.firstActiveTime, taskKey2.firstActiveTime); 930 } 931 }); 932 ArrayList<Task.TaskKey> groupTasks = group.mTaskKeys; 933 int groupTaskCount = groupTasks.size(); 934 for (int j = 0; j < groupTaskCount; j++) { 935 tasks.set(taskIndex, taskMap.get(groupTasks.get(j))); 936 taskIndex++; 937 } 938 } 939 mStackTaskList.set(tasks); 940 } else { 941 // Create the task groups 942 ArrayMap<Task.TaskKey, Task> tasksMap = new ArrayMap<>(); 943 ArrayList<Task> tasks = mStackTaskList.getTasks(); 944 int taskCount = tasks.size(); 945 for (int i = 0; i < taskCount; i++) { 946 Task t = tasks.get(i); 947 TaskGrouping group; 948 if (RecentsDebugFlags.Static.EnableAffiliatedTaskGroups) { 949 int affiliation = t.affiliationTaskId > 0 ? t.affiliationTaskId : 950 IndividualTaskIdOffset + t.key.id; 951 if (mAffinitiesGroups.containsKey(affiliation)) { 952 group = getGroupWithAffiliation(affiliation); 953 } else { 954 group = new TaskGrouping(affiliation); 955 addGroup(group); 956 } 957 } else { 958 group = new TaskGrouping(t.key.id); 959 addGroup(group); 960 } 961 group.addTask(t); 962 tasksMap.put(t.key, t); 963 } 964 // Update the task colors for each of the groups 965 float minAlpha = context.getResources().getFloat( 966 R.dimen.recents_task_affiliation_color_min_alpha_percentage); 967 int taskGroupCount = mGroups.size(); 968 for (int i = 0; i < taskGroupCount; i++) { 969 TaskGrouping group = mGroups.get(i); 970 taskCount = group.getTaskCount(); 971 // Ignore the groups that only have one task 972 if (taskCount <= 1) continue; 973 // Calculate the group color distribution 974 int affiliationColor = tasksMap.get(group.mTaskKeys.get(0)).affiliationColor; 975 float alphaStep = (1f - minAlpha) / taskCount; 976 float alpha = 1f; 977 for (int j = 0; j < taskCount; j++) { 978 Task t = tasksMap.get(group.mTaskKeys.get(j)); 979 t.colorPrimary = Utilities.getColorWithOverlay(affiliationColor, Color.WHITE, 980 alpha); 981 alpha -= alphaStep; 982 } 983 } 984 } 985 } 986 987 /** 988 * Computes the components of tasks in this stack that have been removed as a result of a change 989 * in the specified package. 990 */ 991 public ArraySet<ComponentName> computeComponentsRemoved(String packageName, int userId) { 992 // Identify all the tasks that should be removed as a result of the package being removed. 993 // Using a set to ensure that we callback once per unique component. 994 SystemServicesProxy ssp = Recents.getSystemServices(); 995 ArraySet<ComponentName> existingComponents = new ArraySet<>(); 996 ArraySet<ComponentName> removedComponents = new ArraySet<>(); 997 ArrayList<Task.TaskKey> taskKeys = getTaskKeys(); 998 int taskKeyCount = taskKeys.size(); 999 for (int i = 0; i < taskKeyCount; i++) { 1000 Task.TaskKey t = taskKeys.get(i); 1001 1002 // Skip if this doesn't apply to the current user 1003 if (t.userId != userId) continue; 1004 1005 ComponentName cn = t.getComponent(); 1006 if (cn.getPackageName().equals(packageName)) { 1007 if (existingComponents.contains(cn)) { 1008 // If we know that the component still exists in the package, then skip 1009 continue; 1010 } 1011 if (ssp.getActivityInfo(cn, userId) != null) { 1012 existingComponents.add(cn); 1013 } else { 1014 removedComponents.add(cn); 1015 } 1016 } 1017 } 1018 return removedComponents; 1019 } 1020 1021 @Override 1022 public String toString() { 1023 String str = "Stack Tasks (" + mStackTaskList.size() + "):\n"; 1024 ArrayList<Task> tasks = mStackTaskList.getTasks(); 1025 int taskCount = tasks.size(); 1026 for (int i = 0; i < taskCount; i++) { 1027 str += " " + tasks.get(i).toString() + "\n"; 1028 } 1029 return str; 1030 } 1031 1032 /** 1033 * Given a list of tasks, returns a map of each task's key to the task. 1034 */ 1035 private ArrayMap<Task.TaskKey, Task> createTaskKeyMapFromList(List<Task> tasks) { 1036 ArrayMap<Task.TaskKey, Task> map = new ArrayMap<>(tasks.size()); 1037 int taskCount = tasks.size(); 1038 for (int i = 0; i < taskCount; i++) { 1039 Task task = tasks.get(i); 1040 map.put(task.key, task); 1041 } 1042 return map; 1043 } 1044 1045 public void dump(String prefix, PrintWriter writer) { 1046 String innerPrefix = prefix + " "; 1047 1048 writer.print(prefix); writer.print(TAG); 1049 writer.print(" numStackTasks="); writer.print(mStackTaskList.size()); 1050 writer.println(); 1051 ArrayList<Task> tasks = mStackTaskList.getTasks(); 1052 int taskCount = tasks.size(); 1053 for (int i = 0; i < taskCount; i++) { 1054 tasks.get(i).dump(innerPrefix, writer); 1055 } 1056 } 1057 } 1058