Home | History | Annotate | Download | only in model
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.systemui.recents.model;
     18 
     19 import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT;
     20 import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT;
     21 import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
     22 import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
     23 import static android.view.WindowManager.DOCKED_BOTTOM;
     24 import static android.view.WindowManager.DOCKED_INVALID;
     25 import static android.view.WindowManager.DOCKED_LEFT;
     26 import static android.view.WindowManager.DOCKED_RIGHT;
     27 import static android.view.WindowManager.DOCKED_TOP;
     28 
     29 import android.animation.Animator;
     30 import android.animation.AnimatorSet;
     31 import android.animation.ObjectAnimator;
     32 import android.animation.PropertyValuesHolder;
     33 import android.annotation.IntDef;
     34 import android.content.ComponentName;
     35 import android.content.Context;
     36 import android.content.res.Configuration;
     37 import android.content.res.Resources;
     38 import android.graphics.Canvas;
     39 import android.graphics.Color;
     40 import android.graphics.Paint;
     41 import android.graphics.Point;
     42 import android.graphics.Rect;
     43 import android.graphics.RectF;
     44 import android.graphics.drawable.ColorDrawable;
     45 import android.util.ArrayMap;
     46 import android.util.ArraySet;
     47 import android.util.IntProperty;
     48 import android.util.SparseArray;
     49 import android.view.animation.Interpolator;
     50 
     51 import com.android.internal.policy.DockedDividerUtils;
     52 import com.android.systemui.Interpolators;
     53 import com.android.systemui.R;
     54 import com.android.systemui.recents.Recents;
     55 import com.android.systemui.recents.RecentsDebugFlags;
     56 import com.android.systemui.recents.misc.NamedCounter;
     57 import com.android.systemui.recents.misc.SystemServicesProxy;
     58 import com.android.systemui.recents.misc.Utilities;
     59 import com.android.systemui.recents.views.AnimationProps;
     60 import com.android.systemui.recents.views.DropTarget;
     61 import com.android.systemui.recents.views.TaskStackLayoutAlgorithm;
     62 
     63 import java.io.PrintWriter;
     64 import java.lang.annotation.Retention;
     65 import java.lang.annotation.RetentionPolicy;
     66 import java.util.ArrayList;
     67 import java.util.Collections;
     68 import java.util.Comparator;
     69 import java.util.List;
     70 import java.util.Random;
     71 
     72 
     73 /**
     74  * An interface for a task filter to query whether a particular task should show in a stack.
     75  */
     76 interface TaskFilter {
     77     /** Returns whether the filter accepts the specified task */
     78     public boolean acceptTask(SparseArray<Task> taskIdMap, Task t, int index);
     79 }
     80 
     81 /**
     82  * A list of filtered tasks.
     83  */
     84 class FilteredTaskList {
     85 
     86     ArrayList<Task> mTasks = new ArrayList<>();
     87     ArrayList<Task> mFilteredTasks = new ArrayList<>();
     88     ArrayMap<Task.TaskKey, Integer> mTaskIndices = new ArrayMap<>();
     89     TaskFilter mFilter;
     90 
     91     /** Sets the task filter, saving the current touch state */
     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  */
    217 public class TaskStack {
    218 
    219     private static final String TAG = "TaskStack";
    220 
    221     /** Task stack callbacks */
    222     public interface TaskStackCallbacks {
    223         /**
    224          * Notifies when a new task has been added to the stack.
    225          */
    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