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