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