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
17package com.android.systemui.recents.model;
18
19import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT;
20import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT;
21import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
22import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
23import static android.view.WindowManager.DOCKED_BOTTOM;
24import static android.view.WindowManager.DOCKED_INVALID;
25import static android.view.WindowManager.DOCKED_LEFT;
26import static android.view.WindowManager.DOCKED_RIGHT;
27import static android.view.WindowManager.DOCKED_TOP;
28
29import android.animation.Animator;
30import android.animation.AnimatorSet;
31import android.animation.ObjectAnimator;
32import android.animation.PropertyValuesHolder;
33import android.annotation.IntDef;
34import android.content.ComponentName;
35import android.content.Context;
36import android.content.res.Configuration;
37import android.content.res.Resources;
38import android.graphics.Canvas;
39import android.graphics.Color;
40import android.graphics.Paint;
41import android.graphics.Point;
42import android.graphics.Rect;
43import android.graphics.RectF;
44import android.graphics.drawable.ColorDrawable;
45import android.util.ArrayMap;
46import android.util.ArraySet;
47import android.util.IntProperty;
48import android.util.SparseArray;
49import android.view.animation.Interpolator;
50
51import com.android.internal.policy.DockedDividerUtils;
52import com.android.systemui.Interpolators;
53import com.android.systemui.R;
54import com.android.systemui.recents.Recents;
55import com.android.systemui.recents.RecentsDebugFlags;
56import com.android.systemui.recents.misc.NamedCounter;
57import com.android.systemui.recents.misc.SystemServicesProxy;
58import com.android.systemui.recents.misc.Utilities;
59import com.android.systemui.recents.views.AnimationProps;
60import com.android.systemui.recents.views.DropTarget;
61import com.android.systemui.recents.views.TaskStackLayoutAlgorithm;
62
63import java.io.PrintWriter;
64import java.lang.annotation.Retention;
65import java.lang.annotation.RetentionPolicy;
66import java.util.ArrayList;
67import java.util.Collections;
68import java.util.Comparator;
69import java.util.List;
70import 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 */
76interface TaskFilter {
77    /** Returns whether the filter accepts the specified task */
78    public boolean acceptTask(SparseArray<Task> taskIdMap, Task t, int index);
79}
80
81/**
82 * A list of filtered tasks.
83 */
84class 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 */
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 */
104    void removeFilter() {
105        mFilter = null;
106        updateFilteredTasks();
107    }
108
109    /** Adds a new task to the task list */
110    void add(Task t) {
111        mTasks.add(t);
112        updateFilteredTasks();
113    }
114
115    /**
116     * Moves the given task.
117     */
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 */
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 */
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 */
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 */
160    int size() {
161        return mFilteredTasks.size();
162    }
163
164    /** Returns whether the filtered list contains this task */
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 */
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. */
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 */
204    boolean hasFilter() {
205        return (mFilter != null);
206    }
207
208    /** Returns the list of filtered tasks */
209    ArrayList<Task> getTasks() {
210        return mFilteredTasks;
211    }
212}
213
214/**
215 * The task stack contains a list of multiple tasks.
216 */
217public 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         */
226        void onStackTaskAdded(TaskStack stack, Task newTask);
227
228        /**
229         * Notifies when a task has been removed from the stack.
230         */
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         */
238        void onStackTasksRemoved(TaskStack stack);
239
240        /**
241         * Notifies when tasks in the stack have been updated.
242         */
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
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
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             */
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             */
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             */
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         */
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         */
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         */
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         */
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         */
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     */
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 */
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     */
792    public ArrayList<Task> getStackTasks() {
793        return mStackTaskList.getTasks();
794    }
795
796    /**
797     * Returns the set of "freeform" tasks in the stack.
798     */
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     */
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     */
824    public int getTaskCount() {
825        return mStackTaskList.size();
826    }
827
828    /**
829     * Returns the number of stack tasks.
830     */
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     */
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     */
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     */
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     */
898    public Task getNextLaunchTarget() {
899        Task nextLaunchTarget = getNextLaunchTargetRaw();
900        if (nextLaunchTarget != null) {
901            return nextLaunchTarget;
902        }
903        return getStackTasks().get(getTaskCount() - 1);
904    }
905
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 */
919    public int indexOfStackTask(Task t) {
920        return mStackTaskList.indexOf(t);
921    }
922
923    /** Finds the task with the specified task id. */
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 */
939    public void addGroup(TaskGrouping group) {
940        mGroups.add(group);
941        mAffinitiesGroups.put(group.affiliation, group);
942    }
943
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. */
950    public TaskGrouping getGroupWithAffiliation(int affiliation) {
951        return mAffinitiesGroups.get(affiliation);
952    }
953
954    /**
955     * Temporary: This method will simulate affiliation groups
956     */
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     */
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
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     */
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
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