Home | History | Annotate | Download | only in views
      1 /*
      2  * Copyright (C) 2017 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.quickstep.views;
     18 
     19 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
     20 import static com.android.launcher3.anim.Interpolators.ACCEL;
     21 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
     22 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
     23 import static com.android.launcher3.anim.Interpolators.LINEAR;
     24 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
     25 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
     26 
     27 import android.animation.Animator;
     28 import android.animation.AnimatorSet;
     29 import android.animation.ObjectAnimator;
     30 import android.animation.TimeInterpolator;
     31 import android.animation.ValueAnimator;
     32 import android.annotation.TargetApi;
     33 import android.app.ActivityManager;
     34 import android.content.ComponentName;
     35 import android.content.Context;
     36 import android.content.Intent;
     37 import android.graphics.Canvas;
     38 import android.graphics.Point;
     39 import android.graphics.Rect;
     40 import android.graphics.drawable.Drawable;
     41 import android.os.Build;
     42 import android.os.Bundle;
     43 import android.os.Handler;
     44 import android.os.UserHandle;
     45 import android.support.annotation.Nullable;
     46 import android.text.Layout;
     47 import android.text.StaticLayout;
     48 import android.text.TextPaint;
     49 import android.util.ArraySet;
     50 import android.util.AttributeSet;
     51 import android.util.SparseBooleanArray;
     52 import android.view.KeyEvent;
     53 import android.view.LayoutInflater;
     54 import android.view.MotionEvent;
     55 import android.view.View;
     56 import android.view.ViewDebug;
     57 import android.view.accessibility.AccessibilityEvent;
     58 import android.view.accessibility.AccessibilityNodeInfo;
     59 import android.widget.ListView;
     60 
     61 import com.android.launcher3.BaseActivity;
     62 import com.android.launcher3.DeviceProfile;
     63 import com.android.launcher3.Insettable;
     64 import com.android.launcher3.PagedView;
     65 import com.android.launcher3.R;
     66 import com.android.launcher3.Utilities;
     67 import com.android.launcher3.anim.AnimatorPlaybackController;
     68 import com.android.launcher3.anim.PropertyListBuilder;
     69 import com.android.launcher3.config.FeatureFlags;
     70 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
     71 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
     72 import com.android.launcher3.util.PendingAnimation;
     73 import com.android.launcher3.util.Themes;
     74 import com.android.quickstep.OverviewCallbacks;
     75 import com.android.quickstep.QuickScrubController;
     76 import com.android.quickstep.RecentsModel;
     77 import com.android.quickstep.TaskUtils;
     78 import com.android.quickstep.util.ClipAnimationHelper;
     79 import com.android.quickstep.util.TaskViewDrawable;
     80 import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
     81 import com.android.systemui.shared.recents.model.RecentsTaskLoader;
     82 import com.android.systemui.shared.recents.model.Task;
     83 import com.android.systemui.shared.recents.model.TaskStack;
     84 import com.android.systemui.shared.recents.model.ThumbnailData;
     85 import com.android.systemui.shared.system.ActivityManagerWrapper;
     86 import com.android.systemui.shared.system.BackgroundExecutor;
     87 import com.android.systemui.shared.system.PackageManagerWrapper;
     88 import com.android.systemui.shared.system.TaskStackChangeListener;
     89 
     90 import java.util.ArrayList;
     91 import java.util.function.Consumer;
     92 
     93 /**
     94  * A list of recent tasks.
     95  */
     96 @TargetApi(Build.VERSION_CODES.P)
     97 public abstract class RecentsView<T extends BaseActivity> extends PagedView implements Insettable {
     98 
     99     private static final String TAG = RecentsView.class.getSimpleName();
    100 
    101     private final Rect mTempRect = new Rect();
    102 
    103     private static final int DISMISS_TASK_DURATION = 300;
    104     // The threshold at which we update the SystemUI flags when animating from the task into the app
    105     private static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.6f;
    106 
    107     private static final float[] sTempFloatArray = new float[3];
    108 
    109     protected final T mActivity;
    110     private final QuickScrubController mQuickScrubController;
    111     private final float mFastFlingVelocity;
    112     private final RecentsModel mModel;
    113     private final int mTaskTopMargin;
    114 
    115     private final ScrollState mScrollState = new ScrollState();
    116     // Keeps track of the previously known visible tasks for purposes of loading/unloading task data
    117     private final SparseBooleanArray mHasVisibleTaskData = new SparseBooleanArray();
    118 
    119     private boolean mIsClearAllButtonFullyRevealed;
    120 
    121     /**
    122      * TODO: Call reloadIdNeeded in onTaskStackChanged.
    123      */
    124     private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
    125         @Override
    126         public void onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) {
    127             if (!mHandleTaskStackChanges) {
    128                 return;
    129             }
    130             updateThumbnail(taskId, snapshot);
    131         }
    132 
    133         @Override
    134         public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
    135             if (!mHandleTaskStackChanges) {
    136                 return;
    137             }
    138             // Check this is for the right user
    139             if (!checkCurrentOrManagedUserId(userId, getContext())) {
    140                 return;
    141             }
    142 
    143             // Remove the task immediately from the task list
    144             TaskView taskView = getTaskView(taskId);
    145             if (taskView != null) {
    146                 removeView(taskView);
    147             }
    148         }
    149 
    150         @Override
    151         public void onActivityUnpinned() {
    152             if (!mHandleTaskStackChanges) {
    153                 return;
    154             }
    155             // TODO: Re-enable layout transitions for addition of the unpinned task
    156             reloadIfNeeded();
    157         }
    158 
    159         @Override
    160         public void onTaskRemoved(int taskId) {
    161             if (!mHandleTaskStackChanges) {
    162                 return;
    163             }
    164             BackgroundExecutor.get().submit(() -> {
    165                 TaskView taskView = getTaskView(taskId);
    166                 if (taskView == null) {
    167                     return;
    168                 }
    169                 Handler handler = taskView.getHandler();
    170                 if (handler == null) {
    171                     return;
    172                 }
    173 
    174                 // TODO: Add callbacks from AM reflecting adding/removing from the recents list, and
    175                 //       remove all these checks
    176                 Task.TaskKey taskKey = taskView.getTask().key;
    177                 if (PackageManagerWrapper.getInstance().getActivityInfo(taskKey.getComponent(),
    178                         taskKey.userId) == null) {
    179                     // The package was uninstalled
    180                     handler.post(() ->
    181                             dismissTask(taskView, true /* animate */, false /* removeTask */));
    182                 } else {
    183                     RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(getContext());
    184                     RecentsTaskLoadPlan.PreloadOptions opts =
    185                             new RecentsTaskLoadPlan.PreloadOptions();
    186                     opts.loadTitles = false;
    187                     loadPlan.preloadPlan(opts, mModel.getRecentsTaskLoader(), -1,
    188                             UserHandle.myUserId());
    189                     if (loadPlan.getTaskStack().findTaskWithId(taskId) == null) {
    190                         // The task was removed from the recents list
    191                         handler.post(() ->
    192                                 dismissTask(taskView, true /* animate */, false /* removeTask */));
    193                     }
    194                 }
    195             });
    196         }
    197 
    198         @Override
    199         public void onPinnedStackAnimationStarted() {
    200             // Needed for activities that auto-enter PiP, which will not trigger a remote
    201             // animation to be created
    202             mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER);
    203         }
    204     };
    205 
    206     private int mLoadPlanId = -1;
    207 
    208     // Only valid until the launcher state changes to NORMAL
    209     private int mRunningTaskId = -1;
    210     private boolean mRunningTaskTileHidden;
    211     private Task mTmpRunningTask;
    212 
    213     private boolean mRunningTaskIconScaledDown = false;
    214 
    215     private boolean mOverviewStateEnabled;
    216     private boolean mHandleTaskStackChanges;
    217     private Runnable mNextPageSwitchRunnable;
    218     private boolean mSwipeDownShouldLaunchApp;
    219 
    220     private PendingAnimation mPendingAnimation;
    221 
    222     @ViewDebug.ExportedProperty(category = "launcher")
    223     private float mContentAlpha = 1;
    224 
    225     // Keeps track of task views whose visual state should not be reset
    226     private ArraySet<TaskView> mIgnoreResetTaskViews = new ArraySet<>();
    227 
    228     private View mClearAllButton;
    229 
    230     // Variables for empty state
    231     private final Drawable mEmptyIcon;
    232     private final CharSequence mEmptyMessage;
    233     private final TextPaint mEmptyMessagePaint;
    234     private final Point mLastMeasureSize = new Point();
    235     private final int mEmptyMessagePadding;
    236     private boolean mShowEmptyMessage;
    237     private Layout mEmptyTextLayout;
    238 
    239     private BaseActivity.MultiWindowModeChangedListener mMultiWindowModeChangedListener =
    240             (inMultiWindowMode) -> {
    241         if (!inMultiWindowMode && mOverviewStateEnabled) {
    242             // TODO: Re-enable layout transitions for addition of the unpinned task
    243             reloadIfNeeded();
    244         }
    245     };
    246 
    247     public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
    248         super(context, attrs, defStyleAttr);
    249         setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing));
    250         enableFreeScroll(true);
    251         setClipToOutline(true);
    252 
    253         mFastFlingVelocity = getResources()
    254                 .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
    255         mActivity = (T) BaseActivity.fromContext(context);
    256         mQuickScrubController = new QuickScrubController(mActivity, this);
    257         mModel = RecentsModel.getInstance(context);
    258 
    259         mIsRtl = !Utilities.isRtl(getResources());
    260         setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
    261         mTaskTopMargin = getResources()
    262                 .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
    263 
    264         mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents);
    265         mEmptyIcon.setCallback(this);
    266         mEmptyMessage = context.getText(R.string.recents_empty_message);
    267         mEmptyMessagePaint = new TextPaint();
    268         mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary));
    269         mEmptyMessagePaint.setTextSize(getResources()
    270                 .getDimension(R.dimen.recents_empty_message_text_size));
    271         mEmptyMessagePadding = getResources()
    272                 .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
    273         setWillNotDraw(false);
    274         updateEmptyMessage();
    275         setFocusable(false);
    276     }
    277 
    278     public boolean isRtl() {
    279         return mIsRtl;
    280     }
    281 
    282     public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData) {
    283         TaskView taskView = getTaskView(taskId);
    284         if (taskView != null) {
    285             taskView.onTaskDataLoaded(taskView.getTask(), thumbnailData);
    286         }
    287         return taskView;
    288     }
    289 
    290     @Override
    291     protected void onWindowVisibilityChanged(int visibility) {
    292         super.onWindowVisibilityChanged(visibility);
    293         updateTaskStackListenerState();
    294     }
    295 
    296     @Override
    297     protected void onAttachedToWindow() {
    298         super.onAttachedToWindow();
    299         updateTaskStackListenerState();
    300         mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
    301         ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
    302     }
    303 
    304     @Override
    305     protected void onDetachedFromWindow() {
    306         super.onDetachedFromWindow();
    307         updateTaskStackListenerState();
    308         mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
    309         ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
    310     }
    311 
    312     @Override
    313     public void onViewRemoved(View child) {
    314         super.onViewRemoved(child);
    315 
    316         // Clear the task data for the removed child if it was visible
    317         Task task = ((TaskView) child).getTask();
    318         if (mHasVisibleTaskData.get(task.key.id)) {
    319             mHasVisibleTaskData.delete(task.key.id);
    320             RecentsTaskLoader loader = mModel.getRecentsTaskLoader();
    321             loader.unloadTaskData(task);
    322             loader.getHighResThumbnailLoader().onTaskInvisible(task);
    323         }
    324         onChildViewsChanged();
    325     }
    326 
    327     public boolean isTaskViewVisible(TaskView tv) {
    328         // For now, just check if it's the active task or an adjacent task
    329         return Math.abs(indexOfChild(tv) - getNextPage()) <= 1;
    330     }
    331 
    332     public TaskView getTaskView(int taskId) {
    333         for (int i = 0; i < getChildCount(); i++) {
    334             TaskView tv = (TaskView) getChildAt(i);
    335             if (tv.getTask().key.id == taskId) {
    336                 return tv;
    337             }
    338         }
    339         return null;
    340     }
    341 
    342     public void setOverviewStateEnabled(boolean enabled) {
    343         mOverviewStateEnabled = enabled;
    344         updateTaskStackListenerState();
    345     }
    346 
    347     public void setNextPageSwitchRunnable(Runnable r) {
    348         mNextPageSwitchRunnable = r;
    349     }
    350 
    351     @Override
    352     protected void onPageEndTransition() {
    353         super.onPageEndTransition();
    354         if (mNextPageSwitchRunnable != null) {
    355             mNextPageSwitchRunnable.run();
    356             mNextPageSwitchRunnable = null;
    357         }
    358         if (getNextPage() > 0) {
    359             setSwipeDownShouldLaunchApp(true);
    360         }
    361     }
    362 
    363     private int getScrollEnd() {
    364         return mIsRtl ? 0 : mMaxScrollX;
    365     }
    366 
    367     private float calculateClearAllButtonAlpha() {
    368         final int childCount = getChildCount();
    369         if (mShowEmptyMessage || childCount == 0 || mPageScrolls == null
    370                 || childCount != mPageScrolls.length) {
    371             return 0;
    372         }
    373 
    374         final int scrollEnd = getScrollEnd();
    375         final int oldestChildScroll = getScrollForPage(childCount - 1);
    376 
    377         final int clearAllButtonMotionRange = scrollEnd - oldestChildScroll;
    378         if (clearAllButtonMotionRange == 0) return 0;
    379 
    380         final float alphaUnbound = ((float) (getScrollX() - oldestChildScroll)) /
    381                 clearAllButtonMotionRange;
    382         if (alphaUnbound > 1) return 0;
    383 
    384         return Math.max(alphaUnbound, 0);
    385     }
    386 
    387     private void updateClearAllButtonAlpha() {
    388         if (mClearAllButton != null) {
    389             final float alpha = calculateClearAllButtonAlpha();
    390             final boolean revealed = alpha == 1;
    391             if (mIsClearAllButtonFullyRevealed != revealed) {
    392                 mIsClearAllButtonFullyRevealed = revealed;
    393                 mClearAllButton.setImportantForAccessibility(revealed ?
    394                         IMPORTANT_FOR_ACCESSIBILITY_YES :
    395                         IMPORTANT_FOR_ACCESSIBILITY_NO);
    396             }
    397             mClearAllButton.setAlpha(alpha * mContentAlpha);
    398         }
    399     }
    400 
    401     @Override
    402     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
    403         super.onScrollChanged(l, t, oldl, oldt);
    404         updateClearAllButtonAlpha();
    405     }
    406 
    407     @Override
    408     protected void restoreScrollOnLayout() {
    409         if (mIsClearAllButtonFullyRevealed) {
    410             scrollAndForceFinish(getScrollEnd());
    411         } else {
    412             super.restoreScrollOnLayout();
    413         }
    414     }
    415 
    416     @Override
    417     public boolean onTouchEvent(MotionEvent ev) {
    418         if (ev.getAction() == MotionEvent.ACTION_DOWN && mTouchState == TOUCH_STATE_REST
    419                 && mScroller.isFinished() && mIsClearAllButtonFullyRevealed) {
    420             mClearAllButton.getHitRect(mTempRect);
    421             mTempRect.offset(-getLeft(), -getTop());
    422             if (mTempRect.contains((int) ev.getX(), (int) ev.getY())) {
    423                 // If nothing is in motion, let the Clear All button process the event.
    424                 return false;
    425             }
    426         }
    427 
    428         if (ev.getAction() == MotionEvent.ACTION_UP && mShowEmptyMessage) {
    429             onAllTasksRemoved();
    430         }
    431         return super.onTouchEvent(ev);
    432     }
    433 
    434     private void applyLoadPlan(RecentsTaskLoadPlan loadPlan) {
    435         if (mPendingAnimation != null) {
    436             mPendingAnimation.addEndListener((onEndListener) -> applyLoadPlan(loadPlan));
    437             return;
    438         }
    439         TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null;
    440         if (stack == null) {
    441             removeAllViews();
    442             onTaskStackUpdated();
    443             return;
    444         }
    445 
    446         int oldChildCount = getChildCount();
    447 
    448         // Ensure there are as many views as there are tasks in the stack (adding and trimming as
    449         // necessary)
    450         final LayoutInflater inflater = LayoutInflater.from(getContext());
    451         final ArrayList<Task> tasks = new ArrayList<>(stack.getTasks());
    452 
    453         final int requiredChildCount = tasks.size();
    454         for (int i = getChildCount(); i < requiredChildCount; i++) {
    455             final TaskView taskView = (TaskView) inflater.inflate(R.layout.task, this, false);
    456             addView(taskView);
    457         }
    458         while (getChildCount() > requiredChildCount) {
    459             final TaskView taskView = (TaskView) getChildAt(getChildCount() - 1);
    460             removeView(taskView);
    461         }
    462 
    463         // Unload existing visible task data
    464         unloadVisibleTaskData();
    465 
    466         // Rebind and reset all task views
    467         for (int i = requiredChildCount - 1; i >= 0; i--) {
    468             final int pageIndex = requiredChildCount - i - 1;
    469             final Task task = tasks.get(i);
    470             final TaskView taskView = (TaskView) getChildAt(pageIndex);
    471             taskView.bind(task);
    472         }
    473         resetTaskVisuals();
    474 
    475         if (oldChildCount != getChildCount()) {
    476             mQuickScrubController.snapToNextTaskIfAvailable();
    477         }
    478         onTaskStackUpdated();
    479     }
    480 
    481     protected void onTaskStackUpdated() { }
    482 
    483     public void resetTaskVisuals() {
    484         for (int i = getChildCount() - 1; i >= 0; i--) {
    485             TaskView taskView = (TaskView) getChildAt(i);
    486             if (!mIgnoreResetTaskViews.contains(taskView)) {
    487                 taskView.resetVisualProperties();
    488             }
    489         }
    490         if (mRunningTaskTileHidden) {
    491             setRunningTaskHidden(mRunningTaskTileHidden);
    492         }
    493         applyIconScale(false /* animate */);
    494 
    495         updateCurveProperties();
    496         // Update the set of visible task's data
    497         loadVisibleTaskData();
    498     }
    499 
    500     private void updateTaskStackListenerState() {
    501         boolean handleTaskStackChanges = mOverviewStateEnabled && isAttachedToWindow()
    502                 && getWindowVisibility() == VISIBLE;
    503         if (handleTaskStackChanges != mHandleTaskStackChanges) {
    504             mHandleTaskStackChanges = handleTaskStackChanges;
    505             if (handleTaskStackChanges) {
    506                 reloadIfNeeded();
    507             }
    508         }
    509     }
    510 
    511     @Override
    512     public void setInsets(Rect insets) {
    513         mInsets.set(insets);
    514         DeviceProfile dp = mActivity.getDeviceProfile();
    515         getTaskSize(dp, mTempRect);
    516 
    517         // Keep this logic in sync with ActivityControlHelper.getTranslationYForQuickScrub.
    518         mTempRect.top -= mTaskTopMargin;
    519         setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top,
    520                 dp.availableWidthPx + mInsets.left - mTempRect.right,
    521                 dp.availableHeightPx + mInsets.top - mTempRect.bottom);
    522     }
    523 
    524     protected abstract void getTaskSize(DeviceProfile dp, Rect outRect);
    525 
    526     public void getTaskSize(Rect outRect) {
    527         getTaskSize(mActivity.getDeviceProfile(), outRect);
    528     }
    529 
    530     @Override
    531     protected boolean computeScrollHelper() {
    532         boolean scrolling = super.computeScrollHelper();
    533         boolean isFlingingFast = false;
    534         updateCurveProperties();
    535         if (scrolling || (mTouchState == TOUCH_STATE_SCROLLING)) {
    536             if (scrolling) {
    537                 // Check if we are flinging quickly to disable high res thumbnail loading
    538                 isFlingingFast = mScroller.getCurrVelocity() > mFastFlingVelocity;
    539             }
    540 
    541             // After scrolling, update the visible task's data
    542             loadVisibleTaskData();
    543         }
    544 
    545         // Update the high res thumbnail loader
    546         RecentsTaskLoader loader = mModel.getRecentsTaskLoader();
    547         loader.getHighResThumbnailLoader().setFlingingFast(isFlingingFast);
    548         return scrolling;
    549     }
    550 
    551     /**
    552      * Scales and adjusts translation of adjacent pages as if on a curved carousel.
    553      */
    554     public void updateCurveProperties() {
    555         if (getPageCount() == 0 || getPageAt(0).getMeasuredWidth() == 0) {
    556             return;
    557         }
    558         final int halfPageWidth = getNormalChildWidth() / 2;
    559         final int screenCenter = mInsets.left + getPaddingLeft() + getScrollX() + halfPageWidth;
    560         final int halfScreenWidth = getMeasuredWidth() / 2;
    561         final int pageSpacing = mPageSpacing;
    562 
    563         final int pageCount = getPageCount();
    564         for (int i = 0; i < pageCount; i++) {
    565             View page = getPageAt(i);
    566             float pageCenter = page.getLeft() + page.getTranslationX() + halfPageWidth;
    567             float distanceFromScreenCenter = screenCenter - pageCenter;
    568             float distanceToReachEdge = halfScreenWidth + halfPageWidth + pageSpacing;
    569             mScrollState.linearInterpolation = Math.min(1,
    570                     Math.abs(distanceFromScreenCenter) / distanceToReachEdge);
    571             ((PageCallbacks) page).onPageScroll(mScrollState);
    572         }
    573     }
    574 
    575     /**
    576      * Iterates through all thet asks, and loads the associated task data for newly visible tasks,
    577      * and unloads the associated task data for tasks that are no longer visible.
    578      */
    579     public void loadVisibleTaskData() {
    580         if (!mOverviewStateEnabled) {
    581             // Skip loading visible task data if we've already left the overview state
    582             return;
    583         }
    584 
    585         RecentsTaskLoader loader = mModel.getRecentsTaskLoader();
    586         int centerPageIndex = getPageNearestToCenterOfScreen();
    587         int lower = Math.max(0, centerPageIndex - 2);
    588         int upper = Math.min(centerPageIndex + 2, getChildCount() - 1);
    589         int numChildren = getChildCount();
    590 
    591         // Update the task data for the in/visible children
    592         for (int i = 0; i < numChildren; i++) {
    593             TaskView taskView = (TaskView) getChildAt(i);
    594             Task task = taskView.getTask();
    595             boolean visible = lower <= i && i <= upper;
    596             if (visible) {
    597                 if (task == mTmpRunningTask) {
    598                     // Skip loading if this is the task that we are animating into
    599                     continue;
    600                 }
    601                 if (!mHasVisibleTaskData.get(task.key.id)) {
    602                     loader.loadTaskData(task);
    603                     loader.getHighResThumbnailLoader().onTaskVisible(task);
    604                 }
    605                 mHasVisibleTaskData.put(task.key.id, visible);
    606             } else {
    607                 if (mHasVisibleTaskData.get(task.key.id)) {
    608                     loader.unloadTaskData(task);
    609                     loader.getHighResThumbnailLoader().onTaskInvisible(task);
    610                 }
    611                 mHasVisibleTaskData.delete(task.key.id);
    612             }
    613         }
    614     }
    615 
    616     /**
    617      * Unloads any associated data from the currently visible tasks
    618      */
    619     private void unloadVisibleTaskData() {
    620         RecentsTaskLoader loader = mModel.getRecentsTaskLoader();
    621         for (int i = 0; i < mHasVisibleTaskData.size(); i++) {
    622             if (mHasVisibleTaskData.valueAt(i)) {
    623                 TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i));
    624                 Task task = taskView.getTask();
    625                 loader.unloadTaskData(task);
    626                 loader.getHighResThumbnailLoader().onTaskInvisible(task);
    627             }
    628         }
    629         mHasVisibleTaskData.clear();
    630     }
    631 
    632     protected abstract void onAllTasksRemoved();
    633 
    634     public void reset() {
    635         mRunningTaskId = -1;
    636         mRunningTaskTileHidden = false;
    637 
    638         unloadVisibleTaskData();
    639         setCurrentPage(0);
    640 
    641         OverviewCallbacks.get(getContext()).onResetOverview();
    642     }
    643 
    644     /**
    645      * Reloads the view if anything in recents changed.
    646      */
    647     public void reloadIfNeeded() {
    648         if (!mModel.isLoadPlanValid(mLoadPlanId)) {
    649             mLoadPlanId = mModel.loadTasks(mRunningTaskId, this::applyLoadPlan);
    650         }
    651     }
    652 
    653     /**
    654      * Ensures that the first task in the view represents {@param task} and reloads the view
    655      * if needed. This allows the swipe-up gesture to assume that the first tile always
    656      * corresponds to the correct task.
    657      * All subsequent calls to reload will keep the task as the first item until {@link #reset()}
    658      * is called.
    659      * Also scrolls the view to this task
    660      */
    661     public void showTask(int runningTaskId) {
    662         if (getChildCount() == 0) {
    663             // Add an empty view for now until the task plan is loaded and applied
    664             final TaskView taskView = (TaskView) LayoutInflater.from(getContext())
    665                     .inflate(R.layout.task, this, false);
    666             addView(taskView);
    667 
    668             // The temporary running task is only used for the duration between the start of the
    669             // gesture and the task list is loaded and applied
    670             mTmpRunningTask = new Task(new Task.TaskKey(runningTaskId, 0, new Intent(), 0, 0), null,
    671                     null, "", "", 0, 0, false, true, false, false,
    672                     new ActivityManager.TaskDescription(), 0, new ComponentName("", ""), false);
    673             taskView.bind(mTmpRunningTask);
    674         }
    675         setCurrentTask(runningTaskId);
    676     }
    677 
    678     /**
    679      * Hides the tile associated with {@link #mRunningTaskId}
    680      */
    681     public void setRunningTaskHidden(boolean isHidden) {
    682         mRunningTaskTileHidden = isHidden;
    683         TaskView runningTask = getTaskView(mRunningTaskId);
    684         if (runningTask != null) {
    685             runningTask.setAlpha(isHidden ? 0 : mContentAlpha);
    686         }
    687     }
    688 
    689     /**
    690      * Similar to {@link #showTask(int)} but does not put any restrictions on the first tile.
    691      */
    692     public void setCurrentTask(int runningTaskId) {
    693         boolean runningTaskTileHidden = mRunningTaskTileHidden;
    694         boolean runningTaskIconScaledDown = mRunningTaskIconScaledDown;
    695 
    696         setRunningTaskIconScaledDown(false, false);
    697         setRunningTaskHidden(false);
    698         mRunningTaskId = runningTaskId;
    699         setRunningTaskIconScaledDown(runningTaskIconScaledDown, false);
    700         setRunningTaskHidden(runningTaskTileHidden);
    701 
    702         setCurrentPage(0);
    703 
    704         // Load the tasks (if the loading is already
    705         mLoadPlanId = mModel.loadTasks(runningTaskId, this::applyLoadPlan);
    706     }
    707 
    708     public void showNextTask() {
    709         TaskView runningTaskView = getTaskView(mRunningTaskId);
    710         if (runningTaskView == null) {
    711             // Launch the first task
    712             if (getChildCount() > 0) {
    713                 ((TaskView) getChildAt(0)).launchTask(true /* animate */);
    714             }
    715         } else {
    716             // Get the next launch task
    717             int runningTaskIndex = indexOfChild(runningTaskView);
    718             int nextTaskIndex = Math.max(0, Math.min(getChildCount() - 1, runningTaskIndex + 1));
    719             if (nextTaskIndex < getChildCount()) {
    720                 ((TaskView) getChildAt(nextTaskIndex)).launchTask(true /* animate */);
    721             }
    722         }
    723     }
    724 
    725     public QuickScrubController getQuickScrubController() {
    726         return mQuickScrubController;
    727     }
    728 
    729     public void setRunningTaskIconScaledDown(boolean isScaledDown, boolean animate) {
    730         if (mRunningTaskIconScaledDown == isScaledDown) {
    731             return;
    732         }
    733         mRunningTaskIconScaledDown = isScaledDown;
    734         applyIconScale(animate);
    735     }
    736 
    737     private void applyIconScale(boolean animate) {
    738         float scale = mRunningTaskIconScaledDown ? 0 : 1;
    739         TaskView firstTask = getTaskView(mRunningTaskId);
    740         if (firstTask != null) {
    741             if (animate) {
    742                 firstTask.animateIconToScaleAndDim(scale);
    743             } else {
    744                 firstTask.setIconScaleAndDim(scale);
    745             }
    746         }
    747     }
    748 
    749     public void setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp) {
    750         mSwipeDownShouldLaunchApp = swipeDownShouldLaunchApp;
    751     }
    752 
    753     public boolean shouldSwipeDownLaunchApp() {
    754         return mSwipeDownShouldLaunchApp;
    755     }
    756 
    757     public interface PageCallbacks {
    758 
    759         /**
    760          * Updates the page UI based on scroll params.
    761          */
    762         default void onPageScroll(ScrollState scrollState) {};
    763     }
    764 
    765     public static class ScrollState {
    766 
    767         /**
    768          * The progress from 0 to 1, where 0 is the center
    769          * of the screen and 1 is the edge of the screen.
    770          */
    771         public float linearInterpolation;
    772     }
    773 
    774     public void addIgnoreResetTask(TaskView taskView) {
    775         mIgnoreResetTaskViews.add(taskView);
    776     }
    777 
    778     public void removeIgnoreResetTask(TaskView taskView) {
    779         mIgnoreResetTaskViews.remove(taskView);
    780     }
    781 
    782     private void addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration) {
    783         addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim);
    784         addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()),
    785                 duration, LINEAR, anim);
    786     }
    787 
    788     private void removeTask(Task task, int index, PendingAnimation.OnEndListener onEndListener,
    789                             boolean shouldLog) {
    790         if (task != null) {
    791             ActivityManagerWrapper.getInstance().removeTask(task.key.id);
    792             if (shouldLog) {
    793                 mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
    794                         onEndListener.logAction, Direction.UP, index,
    795                         TaskUtils.getComponentKeyForTask(task.key));
    796             }
    797         }
    798     }
    799 
    800     public PendingAnimation createTaskDismissAnimation(TaskView taskView, boolean animateTaskView,
    801             boolean shouldRemoveTask, long duration) {
    802         if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) {
    803             throw new IllegalStateException("Another pending animation is still running");
    804         }
    805         AnimatorSet anim = new AnimatorSet();
    806         PendingAnimation pendingAnimation = new PendingAnimation(anim);
    807 
    808         int count = getChildCount();
    809         if (count == 0) {
    810             return pendingAnimation;
    811         }
    812 
    813         int[] oldScroll = new int[count];
    814         getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
    815 
    816         int[] newScroll = new int[count];
    817         getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView);
    818 
    819         int scrollDiffPerPage = 0;
    820         int leftmostPage = mIsRtl ? count -1 : 0;
    821         int rightmostPage = mIsRtl ? 0 : count - 1;
    822         if (count > 1) {
    823             int secondRightmostPage = mIsRtl ? 1 : count - 2;
    824             scrollDiffPerPage = oldScroll[rightmostPage] - oldScroll[secondRightmostPage];
    825         }
    826         int draggedIndex = indexOfChild(taskView);
    827 
    828         boolean needsCurveUpdates = false;
    829         for (int i = 0; i < count; i++) {
    830             View child = getChildAt(i);
    831             if (child == taskView) {
    832                 if (animateTaskView) {
    833                     addDismissedTaskAnimations(taskView, anim, duration);
    834                 }
    835             } else {
    836                 // If we just take newScroll - oldScroll, everything to the right of dragged task
    837                 // translates to the left. We need to offset this in some cases:
    838                 // - In RTL, add page offset to all pages, since we want pages to move to the right
    839                 // Additionally, add a page offset if:
    840                 // - Current page is rightmost page (leftmost for RTL)
    841                 // - Dragging an adjacent page on the left side (right side for RTL)
    842                 int offset = mIsRtl ? scrollDiffPerPage : 0;
    843                 if (mCurrentPage == draggedIndex) {
    844                     int lastPage = mIsRtl ? leftmostPage : rightmostPage;
    845                     if (mCurrentPage == lastPage) {
    846                         offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
    847                     }
    848                 } else {
    849                     // Dragging an adjacent page.
    850                     int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR)
    851                     if (draggedIndex == negativeAdjacent) {
    852                         offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
    853                     }
    854                 }
    855                 int scrollDiff = newScroll[i] - oldScroll[i] + offset;
    856                 if (scrollDiff != 0) {
    857                     addAnim(ObjectAnimator.ofFloat(child, TRANSLATION_X, scrollDiff),
    858                             duration, ACCEL, anim);
    859                     needsCurveUpdates = true;
    860                 }
    861             }
    862         }
    863 
    864         if (needsCurveUpdates) {
    865             ValueAnimator va = ValueAnimator.ofFloat(0, 1);
    866             va.addUpdateListener((a) -> updateCurveProperties());
    867             anim.play(va);
    868         }
    869 
    870         // Add a tiny bit of translation Z, so that it draws on top of other views
    871         if (animateTaskView) {
    872             taskView.setTranslationZ(0.1f);
    873         }
    874 
    875         mPendingAnimation = pendingAnimation;
    876         mPendingAnimation.addEndListener((onEndListener) -> {
    877            if (onEndListener.isSuccess) {
    878                if (shouldRemoveTask) {
    879                    removeTask(taskView.getTask(), draggedIndex, onEndListener, true);
    880                }
    881                int pageToSnapTo = mCurrentPage;
    882                if (draggedIndex < pageToSnapTo) {
    883                    pageToSnapTo -= 1;
    884                }
    885                removeView(taskView);
    886                if (getChildCount() == 0) {
    887                    onAllTasksRemoved();
    888                } else if (!mIsClearAllButtonFullyRevealed) {
    889                    snapToPageImmediately(pageToSnapTo);
    890                }
    891            }
    892            resetTaskVisuals();
    893            mPendingAnimation = null;
    894         });
    895         return pendingAnimation;
    896     }
    897 
    898     public PendingAnimation createAllTasksDismissAnimation(long duration) {
    899         if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) {
    900             throw new IllegalStateException("Another pending animation is still running");
    901         }
    902         AnimatorSet anim = new AnimatorSet();
    903         PendingAnimation pendingAnimation = new PendingAnimation(anim);
    904 
    905         int count = getChildCount();
    906         for (int i = 0; i < count; i++) {
    907             addDismissedTaskAnimations(getChildAt(i), anim, duration);
    908         }
    909 
    910         mPendingAnimation = pendingAnimation;
    911         mPendingAnimation.addEndListener((onEndListener) -> {
    912             if (onEndListener.isSuccess) {
    913                 while (getChildCount() != 0) {
    914                     TaskView taskView = getPageAt(getChildCount() - 1);
    915                     removeTask(taskView.getTask(), -1, onEndListener, false);
    916                     removeView(taskView);
    917                 }
    918                 onAllTasksRemoved();
    919             }
    920             mPendingAnimation = null;
    921         });
    922         return pendingAnimation;
    923     }
    924 
    925     private static void addAnim(ObjectAnimator anim, long duration,
    926             TimeInterpolator interpolator, AnimatorSet set) {
    927         anim.setDuration(duration).setInterpolator(interpolator);
    928         set.play(anim);
    929     }
    930 
    931     private boolean snapToPageRelative(int delta, boolean cycle) {
    932         if (getPageCount() == 0) {
    933             return false;
    934         }
    935         final int newPageUnbound = getNextPage() + delta;
    936         if (!cycle && (newPageUnbound < 0 || newPageUnbound >= getChildCount())) {
    937             return false;
    938         }
    939         snapToPage((newPageUnbound + getPageCount()) % getPageCount());
    940         return true;
    941     }
    942 
    943     private void runDismissAnimation(PendingAnimation pendingAnim) {
    944         AnimatorPlaybackController controller = AnimatorPlaybackController.wrap(
    945                 pendingAnim.anim, DISMISS_TASK_DURATION);
    946         controller.dispatchOnStart();
    947         controller.setEndAction(() -> pendingAnim.finish(true, Touch.SWIPE));
    948         controller.getAnimationPlayer().setInterpolator(FAST_OUT_SLOW_IN);
    949         controller.start();
    950     }
    951 
    952     public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) {
    953         runDismissAnimation(createTaskDismissAnimation(taskView, animateTaskView, removeTask,
    954                 DISMISS_TASK_DURATION));
    955     }
    956 
    957     public void dismissAllTasks() {
    958         runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION));
    959     }
    960 
    961     @Override
    962     public boolean dispatchKeyEvent(KeyEvent event) {
    963         if (event.getAction() == KeyEvent.ACTION_DOWN) {
    964             switch (event.getKeyCode()) {
    965                 case KeyEvent.KEYCODE_TAB:
    966                     return snapToPageRelative(event.isShiftPressed() ? -1 : 1,
    967                             event.isAltPressed() /* cycle */);
    968                 case KeyEvent.KEYCODE_DPAD_RIGHT:
    969                     return snapToPageRelative(mIsRtl ? -1 : 1, false /* cycle */);
    970                 case KeyEvent.KEYCODE_DPAD_LEFT:
    971                     return snapToPageRelative(mIsRtl ? 1 : -1, false /* cycle */);
    972                 case KeyEvent.KEYCODE_DEL:
    973                 case KeyEvent.KEYCODE_FORWARD_DEL:
    974                     dismissTask((TaskView) getChildAt(getNextPage()), true /*animateTaskView*/,
    975                             true /*removeTask*/);
    976                     return true;
    977                 case KeyEvent.KEYCODE_NUMPAD_DOT:
    978                     if (event.isAltPressed()) {
    979                         // Numpad DEL pressed while holding Alt.
    980                         dismissTask((TaskView) getChildAt(getNextPage()), true /*animateTaskView*/,
    981                                 true /*removeTask*/);
    982                         return true;
    983                     }
    984             }
    985         }
    986         return super.dispatchKeyEvent(event);
    987     }
    988 
    989     @Override
    990     protected void onFocusChanged(boolean gainFocus, int direction,
    991             @Nullable Rect previouslyFocusedRect) {
    992         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
    993         if (gainFocus && getChildCount() > 0) {
    994             switch (direction) {
    995                 case FOCUS_FORWARD:
    996                     setCurrentPage(0);
    997                     break;
    998                 case FOCUS_BACKWARD:
    999                 case FOCUS_RIGHT:
   1000                 case FOCUS_LEFT:
   1001                     setCurrentPage(getChildCount() - 1);
   1002                     break;
   1003             }
   1004         }
   1005     }
   1006 
   1007     public float getContentAlpha() {
   1008         return mContentAlpha;
   1009     }
   1010 
   1011     public void setContentAlpha(float alpha) {
   1012         alpha = Utilities.boundToRange(alpha, 0, 1);
   1013         mContentAlpha = alpha;
   1014         for (int i = getChildCount() - 1; i >= 0; i--) {
   1015             TaskView child = getPageAt(i);
   1016             if (!mRunningTaskTileHidden || child.getTask().key.id != mRunningTaskId) {
   1017                 getChildAt(i).setAlpha(alpha);
   1018             }
   1019         }
   1020 
   1021         int alphaInt = Math.round(alpha * 255);
   1022         mEmptyMessagePaint.setAlpha(alphaInt);
   1023         mEmptyIcon.setAlpha(alphaInt);
   1024         updateClearAllButtonAlpha();
   1025     }
   1026 
   1027     private float[] getAdjacentScaleAndTranslation(TaskView currTask,
   1028             float currTaskToScale, float currTaskToTranslationY) {
   1029         float displacement = currTask.getWidth() * (currTaskToScale - currTask.getCurveScale());
   1030         sTempFloatArray[0] = currTaskToScale;
   1031         sTempFloatArray[1] = mIsRtl ? -displacement : displacement;
   1032         sTempFloatArray[2] = currTaskToTranslationY;
   1033         return sTempFloatArray;
   1034     }
   1035 
   1036     @Override
   1037     public void onViewAdded(View child) {
   1038         super.onViewAdded(child);
   1039         child.setAlpha(mContentAlpha);
   1040         onChildViewsChanged();
   1041     }
   1042 
   1043     @Override
   1044     public TaskView getPageAt(int index) {
   1045         return (TaskView) getChildAt(index);
   1046     }
   1047 
   1048     public void updateEmptyMessage() {
   1049         boolean isEmpty = getChildCount() == 0;
   1050         boolean hasSizeChanged = mLastMeasureSize.x != getWidth()
   1051                 || mLastMeasureSize.y != getHeight();
   1052         if (isEmpty == mShowEmptyMessage && !hasSizeChanged) {
   1053             return;
   1054         }
   1055         setContentDescription(isEmpty ? mEmptyMessage : "");
   1056         mShowEmptyMessage = isEmpty;
   1057         updateEmptyStateUi(hasSizeChanged);
   1058         invalidate();
   1059     }
   1060 
   1061     @Override
   1062     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
   1063         super.onLayout(changed, left, top, right, bottom);
   1064         updateEmptyStateUi(changed);
   1065 
   1066         // Set the pivot points to match the task preview center
   1067         setPivotY(((mInsets.top + getPaddingTop() + mTaskTopMargin)
   1068                 + (getHeight() - mInsets.bottom - getPaddingBottom())) / 2);
   1069         setPivotX(((mInsets.left + getPaddingLeft())
   1070                 + (getWidth() - mInsets.right - getPaddingRight())) / 2);
   1071     }
   1072 
   1073     private void updateEmptyStateUi(boolean sizeChanged) {
   1074         boolean hasValidSize = getWidth() > 0 && getHeight() > 0;
   1075         if (sizeChanged && hasValidSize) {
   1076             mEmptyTextLayout = null;
   1077             mLastMeasureSize.set(getWidth(), getHeight());
   1078         }
   1079         updateClearAllButtonAlpha();
   1080 
   1081         if (mShowEmptyMessage && hasValidSize && mEmptyTextLayout == null) {
   1082             int availableWidth = mLastMeasureSize.x - mEmptyMessagePadding - mEmptyMessagePadding;
   1083             mEmptyTextLayout = StaticLayout.Builder.obtain(mEmptyMessage, 0, mEmptyMessage.length(),
   1084                     mEmptyMessagePaint, availableWidth)
   1085                     .setAlignment(Layout.Alignment.ALIGN_CENTER)
   1086                     .build();
   1087             int totalHeight = mEmptyTextLayout.getHeight()
   1088                     + mEmptyMessagePadding + mEmptyIcon.getIntrinsicHeight();
   1089 
   1090             int top = (mLastMeasureSize.y - totalHeight) / 2;
   1091             int left = (mLastMeasureSize.x - mEmptyIcon.getIntrinsicWidth()) / 2;
   1092             mEmptyIcon.setBounds(left, top, left + mEmptyIcon.getIntrinsicWidth(),
   1093                     top + mEmptyIcon.getIntrinsicHeight());
   1094         }
   1095     }
   1096 
   1097     @Override
   1098     protected boolean verifyDrawable(Drawable who) {
   1099         return super.verifyDrawable(who) || (mShowEmptyMessage && who == mEmptyIcon);
   1100     }
   1101 
   1102     protected void maybeDrawEmptyMessage(Canvas canvas) {
   1103         if (mShowEmptyMessage && mEmptyTextLayout != null) {
   1104             // Offset to center in the visible (non-padded) part of RecentsView
   1105             mTempRect.set(mInsets.left + getPaddingLeft(), mInsets.top + getPaddingTop(),
   1106                     mInsets.right + getPaddingRight(), mInsets.bottom + getPaddingBottom());
   1107             canvas.save();
   1108             canvas.translate(getScrollX() + (mTempRect.left - mTempRect.right) / 2,
   1109                     (mTempRect.top - mTempRect.bottom) / 2);
   1110             mEmptyIcon.draw(canvas);
   1111             canvas.translate(mEmptyMessagePadding,
   1112                     mEmptyIcon.getBounds().bottom + mEmptyMessagePadding);
   1113             mEmptyTextLayout.draw(canvas);
   1114             canvas.restore();
   1115         }
   1116     }
   1117 
   1118     /**
   1119      * Animate adjacent tasks off screen while scaling up.
   1120      *
   1121      * If launching one of the adjacent tasks, parallax the center task and other adjacent task
   1122      * to the right.
   1123      */
   1124     public AnimatorSet createAdjacentPageAnimForTaskLaunch(
   1125             TaskView tv, ClipAnimationHelper clipAnimationHelper) {
   1126         AnimatorSet anim = new AnimatorSet();
   1127 
   1128         int taskIndex = indexOfChild(tv);
   1129         int centerTaskIndex = getCurrentPage();
   1130         boolean launchingCenterTask = taskIndex == centerTaskIndex;
   1131 
   1132         float toScale = clipAnimationHelper.getSourceRect().width()
   1133                 / clipAnimationHelper.getTargetRect().width();
   1134         float toTranslationY = clipAnimationHelper.getSourceRect().centerY()
   1135                 - clipAnimationHelper.getTargetRect().centerY();
   1136         if (launchingCenterTask) {
   1137             TaskView centerTask = getPageAt(centerTaskIndex);
   1138             if (taskIndex - 1 >= 0) {
   1139                 TaskView adjacentTask = getPageAt(taskIndex - 1);
   1140                 float[] scaleAndTranslation = getAdjacentScaleAndTranslation(centerTask,
   1141                         toScale, toTranslationY);
   1142                 scaleAndTranslation[1] = -scaleAndTranslation[1];
   1143                 anim.play(createAnimForChild(adjacentTask, scaleAndTranslation));
   1144             }
   1145             if (taskIndex + 1 < getPageCount()) {
   1146                 TaskView adjacentTask = getPageAt(taskIndex + 1);
   1147                 float[] scaleAndTranslation = getAdjacentScaleAndTranslation(centerTask,
   1148                         toScale, toTranslationY);
   1149                 anim.play(createAnimForChild(adjacentTask, scaleAndTranslation));
   1150             }
   1151         } else {
   1152             // We are launching an adjacent task, so parallax the center and other adjacent task.
   1153             float displacementX = tv.getWidth() * (toScale - tv.getCurveScale());
   1154             anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex), TRANSLATION_X,
   1155                     mIsRtl ? -displacementX : displacementX));
   1156 
   1157             int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - taskIndex);
   1158             if (otherAdjacentTaskIndex >= 0 && otherAdjacentTaskIndex < getPageCount()) {
   1159                 anim.play(ObjectAnimator.ofPropertyValuesHolder(getPageAt(otherAdjacentTaskIndex),
   1160                         new PropertyListBuilder()
   1161                                 .translationX(mIsRtl ? -displacementX : displacementX)
   1162                                 .scale(1)
   1163                                 .build()));
   1164             }
   1165         }
   1166         return anim;
   1167     }
   1168 
   1169     private Animator createAnimForChild(TaskView child, float[] toScaleAndTranslation) {
   1170         AnimatorSet anim = new AnimatorSet();
   1171         anim.play(ObjectAnimator.ofFloat(child, TaskView.ZOOM_SCALE, toScaleAndTranslation[0]));
   1172         anim.play(ObjectAnimator.ofPropertyValuesHolder(child,
   1173                         new PropertyListBuilder()
   1174                                 .translationX(toScaleAndTranslation[1])
   1175                                 .translationY(toScaleAndTranslation[2])
   1176                                 .build()));
   1177         return anim;
   1178     }
   1179 
   1180     public PendingAnimation createTaskLauncherAnimation(TaskView tv, long duration) {
   1181         if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) {
   1182             throw new IllegalStateException("Another pending animation is still running");
   1183         }
   1184 
   1185         int count = getChildCount();
   1186         if (count == 0) {
   1187             return new PendingAnimation(new AnimatorSet());
   1188         }
   1189 
   1190         tv.setVisibility(INVISIBLE);
   1191         int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags();
   1192         TaskViewDrawable drawable = new TaskViewDrawable(tv, this);
   1193         getOverlay().add(drawable);
   1194 
   1195         ObjectAnimator drawableAnim =
   1196                 ObjectAnimator.ofFloat(drawable, TaskViewDrawable.PROGRESS, 1, 0);
   1197         drawableAnim.setInterpolator(LINEAR);
   1198         drawableAnim.addUpdateListener((animator) -> {
   1199             // Once we pass a certain threshold, update the sysui flags to match the target tasks'
   1200             // flags
   1201             mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW,
   1202                     animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD
   1203                             ? targetSysUiFlags
   1204                             : 0);
   1205         });
   1206 
   1207         AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv,
   1208                 drawable.getClipAnimationHelper());
   1209         anim.play(drawableAnim);
   1210         anim.setDuration(duration);
   1211 
   1212         Consumer<Boolean> onTaskLaunchFinish = (result) -> {
   1213             onTaskLaunched(result);
   1214             tv.setVisibility(VISIBLE);
   1215             getOverlay().remove(drawable);
   1216         };
   1217 
   1218         mPendingAnimation = new PendingAnimation(anim);
   1219         mPendingAnimation.addEndListener((onEndListener) -> {
   1220             if (onEndListener.isSuccess) {
   1221                 Consumer<Boolean> onLaunchResult = (result) -> {
   1222                     onTaskLaunchFinish.accept(result);
   1223                     if (!result) {
   1224                         tv.notifyTaskLaunchFailed(TAG);
   1225                     }
   1226                 };
   1227                 tv.launchTask(false, onLaunchResult, getHandler());
   1228                 Task task = tv.getTask();
   1229                 if (task != null) {
   1230                     mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
   1231                             onEndListener.logAction, Direction.DOWN, indexOfChild(tv),
   1232                             TaskUtils.getComponentKeyForTask(task.key));
   1233                 }
   1234             } else {
   1235                 onTaskLaunchFinish.accept(false);
   1236             }
   1237             mPendingAnimation = null;
   1238         });
   1239         return mPendingAnimation;
   1240     }
   1241 
   1242     public abstract boolean shouldUseMultiWindowTaskSizeStrategy();
   1243 
   1244     protected void onTaskLaunched(boolean success) {
   1245         resetTaskVisuals();
   1246     }
   1247 
   1248     @Override
   1249     protected void notifyPageSwitchListener(int prevPage) {
   1250         super.notifyPageSwitchListener(prevPage);
   1251         loadVisibleTaskData();
   1252     }
   1253 
   1254     @Override
   1255     protected String getCurrentPageDescription() {
   1256         return "";
   1257     }
   1258 
   1259     private int additionalScrollForClearAllButton() {
   1260         return (int) getResources().getDimension(
   1261                 R.dimen.clear_all_container_width) - getPaddingEnd();
   1262     }
   1263 
   1264     @Override
   1265     protected int computeMaxScrollX() {
   1266         if (getChildCount() == 0) {
   1267             return super.computeMaxScrollX();
   1268         }
   1269 
   1270         // Allow a clear_all_container_width-sized gap after the last task.
   1271         return super.computeMaxScrollX() + (mIsRtl ? 0 : additionalScrollForClearAllButton());
   1272     }
   1273 
   1274     @Override
   1275     protected int offsetForPageScrolls() {
   1276         return mIsRtl ? additionalScrollForClearAllButton() : 0;
   1277     }
   1278 
   1279     public void setClearAllButton(View clearAllButton) {
   1280         mClearAllButton = clearAllButton;
   1281         updateClearAllButtonAlpha();
   1282     }
   1283 
   1284     private void onChildViewsChanged() {
   1285         final int childCount = getChildCount();
   1286         mClearAllButton.setVisibility(childCount == 0 ? INVISIBLE : VISIBLE);
   1287         setFocusable(childCount != 0);
   1288     }
   1289 
   1290     public void revealClearAllButton() {
   1291         setCurrentPage(getChildCount() - 1); // Loads tasks info if needed.
   1292         scrollTo(mIsRtl ? 0 : computeMaxScrollX(), 0);
   1293     }
   1294 
   1295     @Override
   1296     public boolean performAccessibilityAction(int action, Bundle arguments) {
   1297         if (getChildCount() > 0) {
   1298             switch (action) {
   1299                 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
   1300                     if (!mIsClearAllButtonFullyRevealed && getCurrentPage() == getPageCount() - 1) {
   1301                         revealClearAllButton();
   1302                         return true;
   1303                     }
   1304                 }
   1305                 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
   1306                     if (mIsClearAllButtonFullyRevealed) {
   1307                         setCurrentPage(getChildCount() - 1);
   1308                         return true;
   1309                     }
   1310                 }
   1311                 break;
   1312             }
   1313         }
   1314         return super.performAccessibilityAction(action, arguments);
   1315     }
   1316 
   1317     @Override
   1318     public void addChildrenForAccessibility(ArrayList<View> outChildren) {
   1319         outChildren.add(mClearAllButton);
   1320         for (int i = getChildCount() - 1; i >= 0; --i) {
   1321             outChildren.add(getChildAt(i));
   1322         }
   1323     }
   1324 
   1325     @Override
   1326     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
   1327         super.onInitializeAccessibilityNodeInfo(info);
   1328 
   1329         if (getChildCount() > 0) {
   1330             info.addAction(mIsClearAllButtonFullyRevealed ?
   1331                     AccessibilityNodeInfo.ACTION_SCROLL_FORWARD :
   1332                     AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
   1333             info.setScrollable(true);
   1334         }
   1335 
   1336         final AccessibilityNodeInfo.CollectionInfo
   1337                 collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain(
   1338                 1, getChildCount(), false,
   1339                 AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE);
   1340         info.setCollectionInfo(collectionInfo);
   1341     }
   1342 
   1343     @Override
   1344     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
   1345         super.onInitializeAccessibilityEvent(event);
   1346 
   1347         event.setScrollable(getPageCount() > 0);
   1348 
   1349         if (!mIsClearAllButtonFullyRevealed
   1350                 && event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
   1351             final int childCount = getChildCount();
   1352             final int[] visibleTasks = getVisibleChildrenRange();
   1353             event.setFromIndex(childCount - visibleTasks[1] - 1);
   1354             event.setToIndex(childCount - visibleTasks[0] - 1);
   1355             event.setItemCount(childCount);
   1356         }
   1357     }
   1358 
   1359     @Override
   1360     public CharSequence getAccessibilityClassName() {
   1361         // To hear position-in-list related feedback from Talkback.
   1362         return ListView.class.getName();
   1363     }
   1364 
   1365     @Override
   1366     protected boolean isPageOrderFlipped() {
   1367         return true;
   1368     }
   1369 
   1370     public boolean performTaskAccessibilityActionExtra(int action) {
   1371         return false;
   1372     }
   1373 }
   1374