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 boolean dismissRecentsIfAllRemoved); 234 235 /** 236 * Notifies when all tasks have been removed from the stack. 237 */ 238 void onStackTasksRemoved(TaskStack stack); 239 240 /** 241 * Notifies when tasks in the stack have been updated. 242 */ 243 void onStackTasksUpdated(TaskStack stack); 244 } 245 246 /** 247 * The various possible dock states when dragging and dropping a task. 248 */ 249 public static class DockState implements DropTarget { 250 251 public static final int DOCK_AREA_BG_COLOR = 0xFFffffff; 252 public static final int DOCK_AREA_GRID_BG_COLOR = 0xFF000000; 253 254 // The rotation to apply to the hint text 255 @Retention(RetentionPolicy.SOURCE) 256 @IntDef({HORIZONTAL, VERTICAL}) 257 public @interface TextOrientation {} 258 private static final int HORIZONTAL = 0; 259 private static final int VERTICAL = 1; 260 261 private static final int DOCK_AREA_ALPHA = 80; 262 public static final DockState NONE = new DockState(DOCKED_INVALID, -1, 80, 255, HORIZONTAL, 263 null, null, null); 264 public static final DockState LEFT = new DockState(DOCKED_LEFT, 265 DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT, DOCK_AREA_ALPHA, 0, VERTICAL, 266 new RectF(0, 0, 0.125f, 1), new RectF(0, 0, 0.125f, 1), 267 new RectF(0, 0, 0.5f, 1)); 268 public static final DockState TOP = new DockState(DOCKED_TOP, 269 DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT, DOCK_AREA_ALPHA, 0, HORIZONTAL, 270 new RectF(0, 0, 1, 0.125f), new RectF(0, 0, 1, 0.125f), 271 new RectF(0, 0, 1, 0.5f)); 272 public static final DockState RIGHT = new DockState(DOCKED_RIGHT, 273 DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT, DOCK_AREA_ALPHA, 0, VERTICAL, 274 new RectF(0.875f, 0, 1, 1), new RectF(0.875f, 0, 1, 1), 275 new RectF(0.5f, 0, 1, 1)); 276 public static final DockState BOTTOM = new DockState(DOCKED_BOTTOM, 277 DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT, DOCK_AREA_ALPHA, 0, HORIZONTAL, 278 new RectF(0, 0.875f, 1, 1), new RectF(0, 0.875f, 1, 1), 279 new RectF(0, 0.5f, 1, 1)); 280 281 @Override 282 public boolean acceptsDrop(int x, int y, int width, int height, Rect insets, 283 boolean isCurrentTarget) { 284 if (isCurrentTarget) { 285 getMappedRect(expandedTouchDockArea, width, height, mTmpRect); 286 return mTmpRect.contains(x, y); 287 } else { 288 getMappedRect(touchArea, width, height, mTmpRect); 289 updateBoundsWithSystemInsets(mTmpRect, insets); 290 return mTmpRect.contains(x, y); 291 } 292 } 293 294 // Represents the view state of this dock state 295 public static class ViewState { 296 private static final IntProperty<ViewState> HINT_ALPHA = 297 new IntProperty<ViewState>("drawableAlpha") { 298 @Override 299 public void setValue(ViewState object, int alpha) { 300 object.mHintTextAlpha = alpha; 301 object.dockAreaOverlay.invalidateSelf(); 302 } 303 304 @Override 305 public Integer get(ViewState object) { 306 return object.mHintTextAlpha; 307 } 308 }; 309 310 public final int dockAreaAlpha; 311 public final ColorDrawable dockAreaOverlay; 312 public final int hintTextAlpha; 313 public final int hintTextOrientation; 314 315 private final int mHintTextResId; 316 private String mHintText; 317 private Paint mHintTextPaint; 318 private Point mHintTextBounds = new Point(); 319 private int mHintTextAlpha = 255; 320 private AnimatorSet mDockAreaOverlayAnimator; 321 private Rect mTmpRect = new Rect(); 322 323 private ViewState(int areaAlpha, int hintAlpha, @TextOrientation int hintOrientation, 324 int hintTextResId) { 325 dockAreaAlpha = areaAlpha; 326 dockAreaOverlay = new ColorDrawable(Recents.getConfiguration().isGridEnabled 327 ? DOCK_AREA_GRID_BG_COLOR : DOCK_AREA_BG_COLOR); 328 dockAreaOverlay.setAlpha(0); 329 hintTextAlpha = hintAlpha; 330 hintTextOrientation = hintOrientation; 331 mHintTextResId = hintTextResId; 332 mHintTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 333 mHintTextPaint.setColor(Color.WHITE); 334 } 335 336 /** 337 * Updates the view state with the given context. 338 */ 339 public void update(Context context) { 340 Resources res = context.getResources(); 341 mHintText = context.getString(mHintTextResId); 342 mHintTextPaint.setTextSize(res.getDimensionPixelSize( 343 R.dimen.recents_drag_hint_text_size)); 344 mHintTextPaint.getTextBounds(mHintText, 0, mHintText.length(), mTmpRect); 345 mHintTextBounds.set((int) mHintTextPaint.measureText(mHintText), mTmpRect.height()); 346 } 347 348 /** 349 * Draws the current view state. 350 */ 351 public void draw(Canvas canvas) { 352 // Draw the overlay background 353 if (dockAreaOverlay.getAlpha() > 0) { 354 dockAreaOverlay.draw(canvas); 355 } 356 357 // Draw the hint text 358 if (mHintTextAlpha > 0) { 359 Rect bounds = dockAreaOverlay.getBounds(); 360 int x = bounds.left + (bounds.width() - mHintTextBounds.x) / 2; 361 int y = bounds.top + (bounds.height() + mHintTextBounds.y) / 2; 362 mHintTextPaint.setAlpha(mHintTextAlpha); 363 if (hintTextOrientation == VERTICAL) { 364 canvas.save(); 365 canvas.rotate(-90f, bounds.centerX(), bounds.centerY()); 366 } 367 canvas.drawText(mHintText, x, y, mHintTextPaint); 368 if (hintTextOrientation == VERTICAL) { 369 canvas.restore(); 370 } 371 } 372 } 373 374 /** 375 * Creates a new bounds and alpha animation. 376 */ 377 public void startAnimation(Rect bounds, int areaAlpha, int hintAlpha, int duration, 378 Interpolator interpolator, boolean animateAlpha, boolean animateBounds) { 379 if (mDockAreaOverlayAnimator != null) { 380 mDockAreaOverlayAnimator.cancel(); 381 } 382 383 ObjectAnimator anim; 384 ArrayList<Animator> animators = new ArrayList<>(); 385 if (dockAreaOverlay.getAlpha() != areaAlpha) { 386 if (animateAlpha) { 387 anim = ObjectAnimator.ofInt(dockAreaOverlay, 388 Utilities.DRAWABLE_ALPHA, dockAreaOverlay.getAlpha(), areaAlpha); 389 anim.setDuration(duration); 390 anim.setInterpolator(interpolator); 391 animators.add(anim); 392 } else { 393 dockAreaOverlay.setAlpha(areaAlpha); 394 } 395 } 396 if (mHintTextAlpha != hintAlpha) { 397 if (animateAlpha) { 398 anim = ObjectAnimator.ofInt(this, HINT_ALPHA, mHintTextAlpha, 399 hintAlpha); 400 anim.setDuration(150); 401 anim.setInterpolator(hintAlpha > mHintTextAlpha 402 ? Interpolators.ALPHA_IN 403 : Interpolators.ALPHA_OUT); 404 animators.add(anim); 405 } else { 406 mHintTextAlpha = hintAlpha; 407 dockAreaOverlay.invalidateSelf(); 408 } 409 } 410 if (bounds != null && !dockAreaOverlay.getBounds().equals(bounds)) { 411 if (animateBounds) { 412 PropertyValuesHolder prop = PropertyValuesHolder.ofObject( 413 Utilities.DRAWABLE_RECT, Utilities.RECT_EVALUATOR, 414 new Rect(dockAreaOverlay.getBounds()), bounds); 415 anim = ObjectAnimator.ofPropertyValuesHolder(dockAreaOverlay, prop); 416 anim.setDuration(duration); 417 anim.setInterpolator(interpolator); 418 animators.add(anim); 419 } else { 420 dockAreaOverlay.setBounds(bounds); 421 } 422 } 423 if (!animators.isEmpty()) { 424 mDockAreaOverlayAnimator = new AnimatorSet(); 425 mDockAreaOverlayAnimator.playTogether(animators); 426 mDockAreaOverlayAnimator.start(); 427 } 428 } 429 } 430 431 public final int dockSide; 432 public final int createMode; 433 public final ViewState viewState; 434 private final RectF touchArea; 435 private final RectF dockArea; 436 private final RectF expandedTouchDockArea; 437 private static final Rect mTmpRect = new Rect(); 438 439 /** 440 * @param createMode used to pass to ActivityManager to dock the task 441 * @param touchArea the area in which touch will initiate this dock state 442 * @param dockArea the visible dock area 443 * @param expandedTouchDockArea the area in which touch will continue to dock after entering 444 * the initial touch area. This is also the new dock area to 445 * draw. 446 */ 447 DockState(int dockSide, int createMode, int dockAreaAlpha, int hintTextAlpha, 448 @TextOrientation int hintTextOrientation, RectF touchArea, RectF dockArea, 449 RectF expandedTouchDockArea) { 450 this.dockSide = dockSide; 451 this.createMode = createMode; 452 this.viewState = new ViewState(dockAreaAlpha, hintTextAlpha, hintTextOrientation, 453 R.string.recents_drag_hint_message); 454 this.dockArea = dockArea; 455 this.touchArea = touchArea; 456 this.expandedTouchDockArea = expandedTouchDockArea; 457 } 458 459 /** 460 * Updates the dock state with the given context. 461 */ 462 public void update(Context context) { 463 viewState.update(context); 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, Rect insets) { 470 getMappedRect(dockArea, width, height, mTmpRect); 471 return updateBoundsWithSystemInsets(mTmpRect, insets); 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 // For now, ignore the left insets since we always dock on the left and show Recents 515 // on the right 516 layoutAlgorithm.getTaskStackBounds(displayRect, windowRectOut, top, 0, insets.right, 517 taskStackBounds); 518 return taskStackBounds; 519 } 520 521 /** 522 * Returns the expanded bounds in certain dock sides such that the bounds account for the 523 * system insets (namely the vertical nav bar). This call modifies and returns the given 524 * {@param bounds}. 525 */ 526 private Rect updateBoundsWithSystemInsets(Rect bounds, Rect insets) { 527 if (dockSide == DOCKED_LEFT) { 528 bounds.right += insets.left; 529 } else if (dockSide == DOCKED_RIGHT) { 530 bounds.left -= insets.right; 531 } 532 return bounds; 533 } 534 535 /** 536 * Returns the mapped rect to the given dimensions. 537 */ 538 private void getMappedRect(RectF bounds, int width, int height, Rect out) { 539 out.set((int) (bounds.left * width), (int) (bounds.top * height), 540 (int) (bounds.right * width), (int) (bounds.bottom * height)); 541 } 542 } 543 544 // A comparator that sorts tasks by their freeform state 545 private Comparator<Task> FREEFORM_COMPARATOR = new Comparator<Task>() { 546 @Override 547 public int compare(Task o1, Task o2) { 548 if (o1.isFreeformTask() && !o2.isFreeformTask()) { 549 return 1; 550 } else if (o2.isFreeformTask() && !o1.isFreeformTask()) { 551 return -1; 552 } 553 return Long.compare(o1.temporarySortIndexInStack, o2.temporarySortIndexInStack); 554 } 555 }; 556 557 558 // The task offset to apply to a task id as a group affiliation 559 static final int IndividualTaskIdOffset = 1 << 16; 560 561 ArrayList<Task> mRawTaskList = new ArrayList<>(); 562 FilteredTaskList mStackTaskList = new FilteredTaskList(); 563 TaskStackCallbacks mCb; 564 565 ArrayList<TaskGrouping> mGroups = new ArrayList<>(); 566 ArrayMap<Integer, TaskGrouping> mAffinitiesGroups = new ArrayMap<>(); 567 568 public TaskStack() { 569 // Ensure that we only show non-docked tasks 570 mStackTaskList.setFilter(new TaskFilter() { 571 @Override 572 public boolean acceptTask(SparseArray<Task> taskIdMap, Task t, int index) { 573 if (RecentsDebugFlags.Static.EnableAffiliatedTaskGroups) { 574 if (t.isAffiliatedTask()) { 575 // If this task is affiliated with another parent in the stack, then the 576 // historical state of this task depends on the state of the parent task 577 Task parentTask = taskIdMap.get(t.affiliationTaskId); 578 if (parentTask != null) { 579 t = parentTask; 580 } 581 } 582 } 583 return t.isStackTask; 584 } 585 }); 586 } 587 588 /** Sets the callbacks for this task stack. */ 589 public void setCallbacks(TaskStackCallbacks cb) { 590 mCb = cb; 591 } 592 593 /** 594 * Moves the given task to either the front of the freeform workspace or the stack. 595 */ 596 public void moveTaskToStack(Task task, int newStackId) { 597 // Find the index to insert into 598 ArrayList<Task> taskList = mStackTaskList.getTasks(); 599 int taskCount = taskList.size(); 600 if (!task.isFreeformTask() && (newStackId == FREEFORM_WORKSPACE_STACK_ID)) { 601 // Insert freeform tasks at the front 602 mStackTaskList.moveTaskToStack(task, taskCount, newStackId); 603 } else if (task.isFreeformTask() && (newStackId == FULLSCREEN_WORKSPACE_STACK_ID)) { 604 // Insert after the first stacked task 605 int insertIndex = 0; 606 for (int i = taskCount - 1; i >= 0; i--) { 607 if (!taskList.get(i).isFreeformTask()) { 608 insertIndex = i + 1; 609 break; 610 } 611 } 612 mStackTaskList.moveTaskToStack(task, insertIndex, newStackId); 613 } 614 } 615 616 /** Does the actual work associated with removing the task. */ 617 void removeTaskImpl(FilteredTaskList taskList, Task t) { 618 // Remove the task from the list 619 taskList.remove(t); 620 // Remove it from the group as well, and if it is empty, remove the group 621 TaskGrouping group = t.group; 622 if (group != null) { 623 group.removeTask(t); 624 if (group.getTaskCount() == 0) { 625 removeGroup(group); 626 } 627 } 628 } 629 630 /** 631 * Removes a task from the stack, with an additional {@param animation} hint to the callbacks on 632 * how they should update themselves. 633 */ 634 public void removeTask(Task t, AnimationProps animation, boolean fromDockGesture) { 635 removeTask(t, animation, fromDockGesture, true /* dismissRecentsIfAllRemoved */); 636 } 637 638 /** 639 * Removes a task from the stack, with an additional {@param animation} hint to the callbacks on 640 * how they should update themselves. 641 */ 642 public void removeTask(Task t, AnimationProps animation, boolean fromDockGesture, 643 boolean dismissRecentsIfAllRemoved) { 644 if (mStackTaskList.contains(t)) { 645 removeTaskImpl(mStackTaskList, t); 646 Task newFrontMostTask = getStackFrontMostTask(false /* includeFreeform */); 647 if (mCb != null) { 648 // Notify that a task has been removed 649 mCb.onStackTaskRemoved(this, t, newFrontMostTask, animation, 650 fromDockGesture, dismissRecentsIfAllRemoved); 651 } 652 } 653 mRawTaskList.remove(t); 654 } 655 656 /** 657 * Removes all tasks from the stack. 658 */ 659 public void removeAllTasks(boolean notifyStackChanges) { 660 ArrayList<Task> tasks = mStackTaskList.getTasks(); 661 for (int i = tasks.size() - 1; i >= 0; i--) { 662 Task t = tasks.get(i); 663 removeTaskImpl(mStackTaskList, t); 664 mRawTaskList.remove(t); 665 } 666 if (mCb != null && notifyStackChanges) { 667 // Notify that all tasks have been removed 668 mCb.onStackTasksRemoved(this); 669 } 670 } 671 672 673 /** 674 * @see #setTasks(Context, List, boolean, boolean) 675 */ 676 public void setTasks(Context context, TaskStack stack, boolean notifyStackChanges) { 677 setTasks(context, stack.mRawTaskList, notifyStackChanges); 678 } 679 680 /** 681 * Sets a few tasks in one go, without calling any callbacks. 682 * 683 * @param tasks the new set of tasks to replace the current set. 684 * @param notifyStackChanges whether or not to callback on specific changes to the list of tasks. 685 */ 686 public void setTasks(Context context, List<Task> tasks, boolean notifyStackChanges) { 687 // Compute a has set for each of the tasks 688 ArrayMap<Task.TaskKey, Task> currentTasksMap = createTaskKeyMapFromList(mRawTaskList); 689 ArrayMap<Task.TaskKey, Task> newTasksMap = createTaskKeyMapFromList(tasks); 690 ArrayList<Task> addedTasks = new ArrayList<>(); 691 ArrayList<Task> removedTasks = new ArrayList<>(); 692 ArrayList<Task> allTasks = new ArrayList<>(); 693 694 // Disable notifications if there are no callbacks 695 if (mCb == null) { 696 notifyStackChanges = false; 697 } 698 699 // Remove any tasks that no longer exist 700 int taskCount = mRawTaskList.size(); 701 for (int i = taskCount - 1; i >= 0; i--) { 702 Task task = mRawTaskList.get(i); 703 if (!newTasksMap.containsKey(task.key)) { 704 if (notifyStackChanges) { 705 removedTasks.add(task); 706 } 707 } 708 task.setGroup(null); 709 } 710 711 // Add any new tasks 712 taskCount = tasks.size(); 713 for (int i = 0; i < taskCount; i++) { 714 Task newTask = tasks.get(i); 715 Task currentTask = currentTasksMap.get(newTask.key); 716 if (currentTask == null && notifyStackChanges) { 717 addedTasks.add(newTask); 718 } else if (currentTask != null) { 719 // The current task has bound callbacks, so just copy the data from the new task 720 // state and add it back into the list 721 currentTask.copyFrom(newTask); 722 newTask = currentTask; 723 } 724 allTasks.add(newTask); 725 } 726 727 // Sort all the tasks to ensure they are ordered correctly 728 for (int i = allTasks.size() - 1; i >= 0; i--) { 729 allTasks.get(i).temporarySortIndexInStack = i; 730 } 731 Collections.sort(allTasks, FREEFORM_COMPARATOR); 732 733 mStackTaskList.set(allTasks); 734 mRawTaskList = allTasks; 735 736 // Update the affiliated groupings 737 createAffiliatedGroupings(context); 738 739 // Only callback for the removed tasks after the stack has updated 740 int removedTaskCount = removedTasks.size(); 741 Task newFrontMostTask = getStackFrontMostTask(false); 742 for (int i = 0; i < removedTaskCount; i++) { 743 mCb.onStackTaskRemoved(this, removedTasks.get(i), newFrontMostTask, 744 AnimationProps.IMMEDIATE, false /* fromDockGesture */, 745 true /* dismissRecentsIfAllRemoved */); 746 } 747 748 // Only callback for the newly added tasks after this stack has been updated 749 int addedTaskCount = addedTasks.size(); 750 for (int i = 0; i < addedTaskCount; i++) { 751 mCb.onStackTaskAdded(this, addedTasks.get(i)); 752 } 753 754 // Notify that the task stack has been updated 755 if (notifyStackChanges) { 756 mCb.onStackTasksUpdated(this); 757 } 758 } 759 760 /** 761 * Gets the front-most task in the stack. 762 */ 763 public Task getStackFrontMostTask(boolean includeFreeformTasks) { 764 ArrayList<Task> stackTasks = mStackTaskList.getTasks(); 765 if (stackTasks.isEmpty()) { 766 return null; 767 } 768 for (int i = stackTasks.size() - 1; i >= 0; i--) { 769 Task task = stackTasks.get(i); 770 if (!task.isFreeformTask() || includeFreeformTasks) { 771 return task; 772 } 773 } 774 return null; 775 } 776 777 /** Gets the task keys */ 778 public ArrayList<Task.TaskKey> getTaskKeys() { 779 ArrayList<Task.TaskKey> taskKeys = new ArrayList<>(); 780 ArrayList<Task> tasks = computeAllTasksList(); 781 int taskCount = tasks.size(); 782 for (int i = 0; i < taskCount; i++) { 783 Task task = tasks.get(i); 784 taskKeys.add(task.key); 785 } 786 return taskKeys; 787 } 788 789 /** 790 * Returns the set of "active" (non-historical) tasks in the stack that have been used recently. 791 */ 792 public ArrayList<Task> getStackTasks() { 793 return mStackTaskList.getTasks(); 794 } 795 796 /** 797 * Returns the set of "freeform" tasks in the stack. 798 */ 799 public ArrayList<Task> getFreeformTasks() { 800 ArrayList<Task> freeformTasks = new ArrayList<>(); 801 ArrayList<Task> tasks = mStackTaskList.getTasks(); 802 int taskCount = tasks.size(); 803 for (int i = 0; i < taskCount; i++) { 804 Task task = tasks.get(i); 805 if (task.isFreeformTask()) { 806 freeformTasks.add(task); 807 } 808 } 809 return freeformTasks; 810 } 811 812 /** 813 * Computes a set of all the active and historical tasks. 814 */ 815 public ArrayList<Task> computeAllTasksList() { 816 ArrayList<Task> tasks = new ArrayList<>(); 817 tasks.addAll(mStackTaskList.getTasks()); 818 return tasks; 819 } 820 821 /** 822 * Returns the number of stack and freeform tasks. 823 */ 824 public int getTaskCount() { 825 return mStackTaskList.size(); 826 } 827 828 /** 829 * Returns the number of stack tasks. 830 */ 831 public int getStackTaskCount() { 832 ArrayList<Task> tasks = mStackTaskList.getTasks(); 833 int stackCount = 0; 834 int taskCount = tasks.size(); 835 for (int i = 0; i < taskCount; i++) { 836 Task task = tasks.get(i); 837 if (!task.isFreeformTask()) { 838 stackCount++; 839 } 840 } 841 return stackCount; 842 } 843 844 /** 845 * Returns the number of freeform tasks. 846 */ 847 public int getFreeformTaskCount() { 848 ArrayList<Task> tasks = mStackTaskList.getTasks(); 849 int freeformCount = 0; 850 int taskCount = tasks.size(); 851 for (int i = 0; i < taskCount; i++) { 852 Task task = tasks.get(i); 853 if (task.isFreeformTask()) { 854 freeformCount++; 855 } 856 } 857 return freeformCount; 858 } 859 860 /** 861 * Returns the task in stack tasks which is the launch target. 862 */ 863 public Task getLaunchTarget() { 864 ArrayList<Task> tasks = mStackTaskList.getTasks(); 865 int taskCount = tasks.size(); 866 for (int i = 0; i < taskCount; i++) { 867 Task task = tasks.get(i); 868 if (task.isLaunchTarget) { 869 return task; 870 } 871 } 872 return null; 873 } 874 875 /** 876 * Returns whether the next launch target should actually be the PiP task. 877 */ 878 public boolean isNextLaunchTargetPip(long lastPipTime) { 879 Task launchTarget = getLaunchTarget(); 880 Task nextLaunchTarget = getNextLaunchTargetRaw(); 881 if (nextLaunchTarget != null && lastPipTime > 0) { 882 // If the PiP time is more recent than the next launch target, then launch the PiP task 883 return lastPipTime > nextLaunchTarget.key.lastActiveTime; 884 } else if (launchTarget != null && lastPipTime > 0 && getTaskCount() == 1) { 885 // Otherwise, if there is no next launch target, but there is a PiP, then launch 886 // the PiP task 887 return true; 888 } 889 return false; 890 } 891 892 /** 893 * Returns the task in stack tasks which should be launched next if Recents are toggled 894 * again, or null if there is no task to be launched. Callers should check 895 * {@link #isNextLaunchTargetPip(long)} before fetching the next raw launch target from the 896 * stack. 897 */ 898 public Task getNextLaunchTarget() { 899 Task nextLaunchTarget = getNextLaunchTargetRaw(); 900 if (nextLaunchTarget != null) { 901 return nextLaunchTarget; 902 } 903 return getStackTasks().get(getTaskCount() - 1); 904 } 905 906 private Task getNextLaunchTargetRaw() { 907 int taskCount = getTaskCount(); 908 if (taskCount == 0) { 909 return null; 910 } 911 int launchTaskIndex = indexOfStackTask(getLaunchTarget()); 912 if (launchTaskIndex != -1 && launchTaskIndex > 0) { 913 return getStackTasks().get(launchTaskIndex - 1); 914 } 915 return null; 916 } 917 918 /** Returns the index of this task in this current task stack */ 919 public int indexOfStackTask(Task t) { 920 return mStackTaskList.indexOf(t); 921 } 922 923 /** Finds the task with the specified task id. */ 924 public Task findTaskWithId(int taskId) { 925 ArrayList<Task> tasks = computeAllTasksList(); 926 int taskCount = tasks.size(); 927 for (int i = 0; i < taskCount; i++) { 928 Task task = tasks.get(i); 929 if (task.key.id == taskId) { 930 return task; 931 } 932 } 933 return null; 934 } 935 936 /******** Grouping ********/ 937 938 /** Adds a group to the set */ 939 public void addGroup(TaskGrouping group) { 940 mGroups.add(group); 941 mAffinitiesGroups.put(group.affiliation, group); 942 } 943 944 public void removeGroup(TaskGrouping group) { 945 mGroups.remove(group); 946 mAffinitiesGroups.remove(group.affiliation); 947 } 948 949 /** Returns the group with the specified affiliation. */ 950 public TaskGrouping getGroupWithAffiliation(int affiliation) { 951 return mAffinitiesGroups.get(affiliation); 952 } 953 954 /** 955 * Temporary: This method will simulate affiliation groups 956 */ 957 void createAffiliatedGroupings(Context context) { 958 mGroups.clear(); 959 mAffinitiesGroups.clear(); 960 961 if (RecentsDebugFlags.Static.EnableMockTaskGroups) { 962 ArrayMap<Task.TaskKey, Task> taskMap = new ArrayMap<>(); 963 // Sort all tasks by increasing firstActiveTime of the task 964 ArrayList<Task> tasks = mStackTaskList.getTasks(); 965 Collections.sort(tasks, new Comparator<Task>() { 966 @Override 967 public int compare(Task task, Task task2) { 968 return Long.compare(task.key.firstActiveTime, task2.key.firstActiveTime); 969 } 970 }); 971 // Create groups when sequential packages are the same 972 NamedCounter counter = new NamedCounter("task-group", ""); 973 int taskCount = tasks.size(); 974 String prevPackage = ""; 975 int prevAffiliation = -1; 976 Random r = new Random(); 977 int groupCountDown = RecentsDebugFlags.Static.MockTaskGroupsTaskCount; 978 for (int i = 0; i < taskCount; i++) { 979 Task t = tasks.get(i); 980 String packageName = t.key.getComponent().getPackageName(); 981 packageName = "pkg"; 982 TaskGrouping group; 983 if (packageName.equals(prevPackage) && groupCountDown > 0) { 984 group = getGroupWithAffiliation(prevAffiliation); 985 groupCountDown--; 986 } else { 987 int affiliation = IndividualTaskIdOffset + t.key.id; 988 group = new TaskGrouping(affiliation); 989 addGroup(group); 990 prevAffiliation = affiliation; 991 prevPackage = packageName; 992 groupCountDown = RecentsDebugFlags.Static.MockTaskGroupsTaskCount; 993 } 994 group.addTask(t); 995 taskMap.put(t.key, t); 996 } 997 // Sort groups by increasing latestActiveTime of the group 998 Collections.sort(mGroups, new Comparator<TaskGrouping>() { 999 @Override 1000 public int compare(TaskGrouping taskGrouping, TaskGrouping taskGrouping2) { 1001 return Long.compare(taskGrouping.latestActiveTimeInGroup, 1002 taskGrouping2.latestActiveTimeInGroup); 1003 } 1004 }); 1005 // Sort group tasks by increasing firstActiveTime of the task, and also build a new list 1006 // of tasks 1007 int taskIndex = 0; 1008 int groupCount = mGroups.size(); 1009 for (int i = 0; i < groupCount; i++) { 1010 TaskGrouping group = mGroups.get(i); 1011 Collections.sort(group.mTaskKeys, new Comparator<Task.TaskKey>() { 1012 @Override 1013 public int compare(Task.TaskKey taskKey, Task.TaskKey taskKey2) { 1014 return Long.compare(taskKey.firstActiveTime, taskKey2.firstActiveTime); 1015 } 1016 }); 1017 ArrayList<Task.TaskKey> groupTasks = group.mTaskKeys; 1018 int groupTaskCount = groupTasks.size(); 1019 for (int j = 0; j < groupTaskCount; j++) { 1020 tasks.set(taskIndex, taskMap.get(groupTasks.get(j))); 1021 taskIndex++; 1022 } 1023 } 1024 mStackTaskList.set(tasks); 1025 } else { 1026 // Create the task groups 1027 ArrayMap<Task.TaskKey, Task> tasksMap = new ArrayMap<>(); 1028 ArrayList<Task> tasks = mStackTaskList.getTasks(); 1029 int taskCount = tasks.size(); 1030 for (int i = 0; i < taskCount; i++) { 1031 Task t = tasks.get(i); 1032 TaskGrouping group; 1033 if (RecentsDebugFlags.Static.EnableAffiliatedTaskGroups) { 1034 int affiliation = t.affiliationTaskId > 0 ? t.affiliationTaskId : 1035 IndividualTaskIdOffset + t.key.id; 1036 if (mAffinitiesGroups.containsKey(affiliation)) { 1037 group = getGroupWithAffiliation(affiliation); 1038 } else { 1039 group = new TaskGrouping(affiliation); 1040 addGroup(group); 1041 } 1042 } else { 1043 group = new TaskGrouping(t.key.id); 1044 addGroup(group); 1045 } 1046 group.addTask(t); 1047 tasksMap.put(t.key, t); 1048 } 1049 // Update the task colors for each of the groups 1050 float minAlpha = context.getResources().getFloat( 1051 R.dimen.recents_task_affiliation_color_min_alpha_percentage); 1052 int taskGroupCount = mGroups.size(); 1053 for (int i = 0; i < taskGroupCount; i++) { 1054 TaskGrouping group = mGroups.get(i); 1055 taskCount = group.getTaskCount(); 1056 // Ignore the groups that only have one task 1057 if (taskCount <= 1) continue; 1058 // Calculate the group color distribution 1059 int affiliationColor = tasksMap.get(group.mTaskKeys.get(0)).affiliationColor; 1060 float alphaStep = (1f - minAlpha) / taskCount; 1061 float alpha = 1f; 1062 for (int j = 0; j < taskCount; j++) { 1063 Task t = tasksMap.get(group.mTaskKeys.get(j)); 1064 t.colorPrimary = Utilities.getColorWithOverlay(affiliationColor, Color.WHITE, 1065 alpha); 1066 alpha -= alphaStep; 1067 } 1068 } 1069 } 1070 } 1071 1072 /** 1073 * Computes the components of tasks in this stack that have been removed as a result of a change 1074 * in the specified package. 1075 */ 1076 public ArraySet<ComponentName> computeComponentsRemoved(String packageName, int userId) { 1077 // Identify all the tasks that should be removed as a result of the package being removed. 1078 // Using a set to ensure that we callback once per unique component. 1079 SystemServicesProxy ssp = Recents.getSystemServices(); 1080 ArraySet<ComponentName> existingComponents = new ArraySet<>(); 1081 ArraySet<ComponentName> removedComponents = new ArraySet<>(); 1082 ArrayList<Task.TaskKey> taskKeys = getTaskKeys(); 1083 int taskKeyCount = taskKeys.size(); 1084 for (int i = 0; i < taskKeyCount; i++) { 1085 Task.TaskKey t = taskKeys.get(i); 1086 1087 // Skip if this doesn't apply to the current user 1088 if (t.userId != userId) continue; 1089 1090 ComponentName cn = t.getComponent(); 1091 if (cn.getPackageName().equals(packageName)) { 1092 if (existingComponents.contains(cn)) { 1093 // If we know that the component still exists in the package, then skip 1094 continue; 1095 } 1096 if (ssp.getActivityInfo(cn, userId) != null) { 1097 existingComponents.add(cn); 1098 } else { 1099 removedComponents.add(cn); 1100 } 1101 } 1102 } 1103 return removedComponents; 1104 } 1105 1106 @Override 1107 public String toString() { 1108 String str = "Stack Tasks (" + mStackTaskList.size() + "):\n"; 1109 ArrayList<Task> tasks = mStackTaskList.getTasks(); 1110 int taskCount = tasks.size(); 1111 for (int i = 0; i < taskCount; i++) { 1112 str += " " + tasks.get(i).toString() + "\n"; 1113 } 1114 return str; 1115 } 1116 1117 /** 1118 * Given a list of tasks, returns a map of each task's key to the task. 1119 */ 1120 private ArrayMap<Task.TaskKey, Task> createTaskKeyMapFromList(List<Task> tasks) { 1121 ArrayMap<Task.TaskKey, Task> map = new ArrayMap<>(tasks.size()); 1122 int taskCount = tasks.size(); 1123 for (int i = 0; i < taskCount; i++) { 1124 Task task = tasks.get(i); 1125 map.put(task.key, task); 1126 } 1127 return map; 1128 } 1129 1130 public void dump(String prefix, PrintWriter writer) { 1131 String innerPrefix = prefix + " "; 1132 1133 writer.print(prefix); writer.print(TAG); 1134 writer.print(" numStackTasks="); writer.print(mStackTaskList.size()); 1135 writer.println(); 1136 ArrayList<Task> tasks = mStackTaskList.getTasks(); 1137 int taskCount = tasks.size(); 1138 for (int i = 0; i < taskCount; i++) { 1139 tasks.get(i).dump(innerPrefix, writer); 1140 } 1141 } 1142 } 1143