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, boolean isCurrentTarget)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 ViewState(int areaAlpha, int hintAlpha, @TextOrientation int hintOrientation, int hintTextResId)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 */ update(Context context)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 */ draw(Canvas canvas)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 */ startAnimation(Rect bounds, int areaAlpha, int hintAlpha, int duration, Interpolator interpolator, boolean animateAlpha, boolean animateBounds)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 */ DockState(int dockSide, int createMode, int dockAreaAlpha, int hintTextAlpha, @TextOrientation int hintTextOrientation, RectF touchArea, RectF dockArea, RectF expandedTouchDockArea)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 */ update(Context context)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 */ areaContainsPoint(RectF area, int width, int height, float x, float y)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 */ getPreDockedBounds(int width, int height)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 */ getDockedBounds(int width, int height, int dividerSize, Rect insets, Resources res)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 */ getDockedTaskStackBounds(Rect displayRect, int width, int height, int dividerSize, Rect insets, TaskStackLayoutAlgorithm layoutAlgorithm, Resources res, Rect windowRectOut)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 */ getStackFrontMostTask(boolean includeFreeformTasks)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 */ getTaskKeys()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 */ getStackTasks()750 public ArrayList<Task> getStackTasks() { 751 return mStackTaskList.getTasks(); 752 } 753 754 /** 755 * Returns the set of "freeform" tasks in the stack. 756 */ getFreeformTasks()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 */ computeAllTasksList()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 */ getTaskCount()782 public int getTaskCount() { 783 return mStackTaskList.size(); 784 } 785 786 /** 787 * Returns the number of stack tasks. 788 */ getStackTaskCount()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 */ getFreeformTaskCount()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 */ getLaunchTarget()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 */ indexOfStackTask(Task t)834 public int indexOfStackTask(Task t) { 835 return mStackTaskList.indexOf(t); 836 } 837 838 /** Finds the task with the specified task id. */ findTaskWithId(int taskId)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 */ addGroup(TaskGrouping group)854 public void addGroup(TaskGrouping group) { 855 mGroups.add(group); 856 mAffinitiesGroups.put(group.affiliation, group); 857 } 858 removeGroup(TaskGrouping group)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. */ getGroupWithAffiliation(int affiliation)865 public TaskGrouping getGroupWithAffiliation(int affiliation) { 866 return mAffinitiesGroups.get(affiliation); 867 } 868 869 /** 870 * Temporary: This method will simulate affiliation groups 871 */ createAffiliatedGroupings(Context context)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 */ computeComponentsRemoved(String packageName, int userId)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 toString()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 */ createTaskKeyMapFromList(List<Task> tasks)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 dump(String prefix, PrintWriter writer)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