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