• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
RecentsView(Context context, AttributeSet attrs, int defStyleAttr)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 
isRtl()278     public boolean isRtl() {
279         return mIsRtl;
280     }
281 
updateThumbnail(int taskId, ThumbnailData thumbnailData)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
onWindowVisibilityChanged(int visibility)291     protected void onWindowVisibilityChanged(int visibility) {
292         super.onWindowVisibilityChanged(visibility);
293         updateTaskStackListenerState();
294     }
295 
296     @Override
onAttachedToWindow()297     protected void onAttachedToWindow() {
298         super.onAttachedToWindow();
299         updateTaskStackListenerState();
300         mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
301         ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
302     }
303 
304     @Override
onDetachedFromWindow()305     protected void onDetachedFromWindow() {
306         super.onDetachedFromWindow();
307         updateTaskStackListenerState();
308         mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
309         ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
310     }
311 
312     @Override
onViewRemoved(View child)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 
isTaskViewVisible(TaskView tv)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 
getTaskView(int taskId)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 
setOverviewStateEnabled(boolean enabled)342     public void setOverviewStateEnabled(boolean enabled) {
343         mOverviewStateEnabled = enabled;
344         updateTaskStackListenerState();
345     }
346 
setNextPageSwitchRunnable(Runnable r)347     public void setNextPageSwitchRunnable(Runnable r) {
348         mNextPageSwitchRunnable = r;
349     }
350 
351     @Override
onPageEndTransition()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 
getScrollEnd()363     private int getScrollEnd() {
364         return mIsRtl ? 0 : mMaxScrollX;
365     }
366 
calculateClearAllButtonAlpha()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 
updateClearAllButtonAlpha()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
onScrollChanged(int l, int t, int oldl, int oldt)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
restoreScrollOnLayout()408     protected void restoreScrollOnLayout() {
409         if (mIsClearAllButtonFullyRevealed) {
410             scrollAndForceFinish(getScrollEnd());
411         } else {
412             super.restoreScrollOnLayout();
413         }
414     }
415 
416     @Override
onTouchEvent(MotionEvent ev)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 
applyLoadPlan(RecentsTaskLoadPlan loadPlan)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 
onTaskStackUpdated()481     protected void onTaskStackUpdated() { }
482 
resetTaskVisuals()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 
updateTaskStackListenerState()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
setInsets(Rect insets)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 
getTaskSize(DeviceProfile dp, Rect outRect)524     protected abstract void getTaskSize(DeviceProfile dp, Rect outRect);
525 
getTaskSize(Rect outRect)526     public void getTaskSize(Rect outRect) {
527         getTaskSize(mActivity.getDeviceProfile(), outRect);
528     }
529 
530     @Override
computeScrollHelper()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      */
updateCurveProperties()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      */
loadVisibleTaskData()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      */
unloadVisibleTaskData()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 
onAllTasksRemoved()632     protected abstract void onAllTasksRemoved();
633 
reset()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      */
reloadIfNeeded()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      */
showTask(int runningTaskId)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      */
setRunningTaskHidden(boolean isHidden)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      */
setCurrentTask(int runningTaskId)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 
showNextTask()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 
getQuickScrubController()725     public QuickScrubController getQuickScrubController() {
726         return mQuickScrubController;
727     }
728 
setRunningTaskIconScaledDown(boolean isScaledDown, boolean animate)729     public void setRunningTaskIconScaledDown(boolean isScaledDown, boolean animate) {
730         if (mRunningTaskIconScaledDown == isScaledDown) {
731             return;
732         }
733         mRunningTaskIconScaledDown = isScaledDown;
734         applyIconScale(animate);
735     }
736 
applyIconScale(boolean animate)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 
setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp)749     public void setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp) {
750         mSwipeDownShouldLaunchApp = swipeDownShouldLaunchApp;
751     }
752 
shouldSwipeDownLaunchApp()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          */
onPageScroll(ScrollState scrollState)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 
addIgnoreResetTask(TaskView taskView)774     public void addIgnoreResetTask(TaskView taskView) {
775         mIgnoreResetTaskViews.add(taskView);
776     }
777 
removeIgnoreResetTask(TaskView taskView)778     public void removeIgnoreResetTask(TaskView taskView) {
779         mIgnoreResetTaskViews.remove(taskView);
780     }
781 
addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration)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 
removeTask(Task task, int index, PendingAnimation.OnEndListener onEndListener, boolean shouldLog)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 
createTaskDismissAnimation(TaskView taskView, boolean animateTaskView, boolean shouldRemoveTask, long duration)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 
createAllTasksDismissAnimation(long duration)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 
addAnim(ObjectAnimator anim, long duration, TimeInterpolator interpolator, AnimatorSet set)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 
snapToPageRelative(int delta, boolean cycle)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 
runDismissAnimation(PendingAnimation pendingAnim)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 
dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask)952     public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) {
953         runDismissAnimation(createTaskDismissAnimation(taskView, animateTaskView, removeTask,
954                 DISMISS_TASK_DURATION));
955     }
956 
dismissAllTasks()957     public void dismissAllTasks() {
958         runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION));
959     }
960 
961     @Override
dispatchKeyEvent(KeyEvent event)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
onFocusChanged(boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect)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 
getContentAlpha()1007     public float getContentAlpha() {
1008         return mContentAlpha;
1009     }
1010 
setContentAlpha(float alpha)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 
getAdjacentScaleAndTranslation(TaskView currTask, float currTaskToScale, float currTaskToTranslationY)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
onViewAdded(View child)1037     public void onViewAdded(View child) {
1038         super.onViewAdded(child);
1039         child.setAlpha(mContentAlpha);
1040         onChildViewsChanged();
1041     }
1042 
1043     @Override
getPageAt(int index)1044     public TaskView getPageAt(int index) {
1045         return (TaskView) getChildAt(index);
1046     }
1047 
updateEmptyMessage()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
onLayout(boolean changed, int left, int top, int right, int bottom)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 
updateEmptyStateUi(boolean sizeChanged)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
verifyDrawable(Drawable who)1098     protected boolean verifyDrawable(Drawable who) {
1099         return super.verifyDrawable(who) || (mShowEmptyMessage && who == mEmptyIcon);
1100     }
1101 
maybeDrawEmptyMessage(Canvas canvas)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      */
createAdjacentPageAnimForTaskLaunch( TaskView tv, ClipAnimationHelper clipAnimationHelper)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 
createAnimForChild(TaskView child, float[] toScaleAndTranslation)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 
createTaskLauncherAnimation(TaskView tv, long duration)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 
shouldUseMultiWindowTaskSizeStrategy()1242     public abstract boolean shouldUseMultiWindowTaskSizeStrategy();
1243 
onTaskLaunched(boolean success)1244     protected void onTaskLaunched(boolean success) {
1245         resetTaskVisuals();
1246     }
1247 
1248     @Override
notifyPageSwitchListener(int prevPage)1249     protected void notifyPageSwitchListener(int prevPage) {
1250         super.notifyPageSwitchListener(prevPage);
1251         loadVisibleTaskData();
1252     }
1253 
1254     @Override
getCurrentPageDescription()1255     protected String getCurrentPageDescription() {
1256         return "";
1257     }
1258 
additionalScrollForClearAllButton()1259     private int additionalScrollForClearAllButton() {
1260         return (int) getResources().getDimension(
1261                 R.dimen.clear_all_container_width) - getPaddingEnd();
1262     }
1263 
1264     @Override
computeMaxScrollX()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
offsetForPageScrolls()1275     protected int offsetForPageScrolls() {
1276         return mIsRtl ? additionalScrollForClearAllButton() : 0;
1277     }
1278 
setClearAllButton(View clearAllButton)1279     public void setClearAllButton(View clearAllButton) {
1280         mClearAllButton = clearAllButton;
1281         updateClearAllButtonAlpha();
1282     }
1283 
onChildViewsChanged()1284     private void onChildViewsChanged() {
1285         final int childCount = getChildCount();
1286         mClearAllButton.setVisibility(childCount == 0 ? INVISIBLE : VISIBLE);
1287         setFocusable(childCount != 0);
1288     }
1289 
revealClearAllButton()1290     public void revealClearAllButton() {
1291         setCurrentPage(getChildCount() - 1); // Loads tasks info if needed.
1292         scrollTo(mIsRtl ? 0 : computeMaxScrollX(), 0);
1293     }
1294 
1295     @Override
performAccessibilityAction(int action, Bundle arguments)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
addChildrenForAccessibility(ArrayList<View> outChildren)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
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)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
onInitializeAccessibilityEvent(AccessibilityEvent event)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
getAccessibilityClassName()1360     public CharSequence getAccessibilityClassName() {
1361         // To hear position-in-list related feedback from Talkback.
1362         return ListView.class.getName();
1363     }
1364 
1365     @Override
isPageOrderFlipped()1366     protected boolean isPageOrderFlipped() {
1367         return true;
1368     }
1369 
performTaskAccessibilityActionExtra(int action)1370     public boolean performTaskAccessibilityActionExtra(int action) {
1371         return false;
1372     }
1373 }
1374