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