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, boolean dismissRecentsIfAllRemoved)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 */ onStackTasksRemoved(TaskStack stack)238 void onStackTasksRemoved(TaskStack stack); 239 240 /** 241 * Notifies when tasks in the stack have been updated. 242 */ onStackTasksUpdated(TaskStack stack)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 acceptsDrop(int x, int y, int width, int height, Rect insets, boolean isCurrentTarget)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 ViewState(int areaAlpha, int hintAlpha, @TextOrientation int hintOrientation, int hintTextResId)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 */ update(Context context)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 */ draw(Canvas canvas)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 */ startAnimation(Rect bounds, int areaAlpha, int hintAlpha, int duration, Interpolator interpolator, boolean animateAlpha, boolean animateBounds)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 */ DockState(int dockSide, int createMode, int dockAreaAlpha, int hintTextAlpha, @TextOrientation int hintTextOrientation, RectF touchArea, RectF dockArea, RectF expandedTouchDockArea)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 */ update(Context context)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 */ getPreDockedBounds(int width, int height, Rect insets)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 */ 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 // 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 */ getStackFrontMostTask(boolean includeFreeformTasks)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 */ getTaskKeys()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 */ getStackTasks()792 public ArrayList<Task> getStackTasks() { 793 return mStackTaskList.getTasks(); 794 } 795 796 /** 797 * Returns the set of "freeform" tasks in the stack. 798 */ getFreeformTasks()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 */ computeAllTasksList()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 */ getTaskCount()824 public int getTaskCount() { 825 return mStackTaskList.size(); 826 } 827 828 /** 829 * Returns the number of stack tasks. 830 */ getStackTaskCount()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 */ getFreeformTaskCount()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 */ getLaunchTarget()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 */ isNextLaunchTargetPip(long lastPipTime)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 */ getNextLaunchTarget()898 public Task getNextLaunchTarget() { 899 Task nextLaunchTarget = getNextLaunchTargetRaw(); 900 if (nextLaunchTarget != null) { 901 return nextLaunchTarget; 902 } 903 return getStackTasks().get(getTaskCount() - 1); 904 } 905 getNextLaunchTargetRaw()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 */ indexOfStackTask(Task t)919 public int indexOfStackTask(Task t) { 920 return mStackTaskList.indexOf(t); 921 } 922 923 /** Finds the task with the specified task id. */ findTaskWithId(int taskId)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 */ addGroup(TaskGrouping group)939 public void addGroup(TaskGrouping group) { 940 mGroups.add(group); 941 mAffinitiesGroups.put(group.affiliation, group); 942 } 943 removeGroup(TaskGrouping group)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. */ getGroupWithAffiliation(int affiliation)950 public TaskGrouping getGroupWithAffiliation(int affiliation) { 951 return mAffinitiesGroups.get(affiliation); 952 } 953 954 /** 955 * Temporary: This method will simulate affiliation groups 956 */ createAffiliatedGroupings(Context context)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 */ computeComponentsRemoved(String packageName, int userId)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 toString()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 */ createTaskKeyMapFromList(List<Task> tasks)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 dump(String prefix, PrintWriter writer)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