• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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