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