• 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 androidx.dynamicanimation.animation.DynamicAnimation.MIN_VISIBLE_CHANGE_PIXELS;
20 
21 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
22 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
23 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
24 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
25 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
26 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
27 import static com.android.launcher3.Utilities.squaredHypot;
28 import static com.android.launcher3.Utilities.squaredTouchSlop;
29 import static com.android.launcher3.anim.Interpolators.ACCEL;
30 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
31 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
32 import static com.android.launcher3.anim.Interpolators.LINEAR;
33 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
34 import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
35 import static com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController.SUCCESS_TRANSITION_PROGRESS;
36 import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
37 import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CLEAR_ALL_BUTTON;
38 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
39 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
40 
41 import android.animation.Animator;
42 import android.animation.AnimatorSet;
43 import android.animation.LayoutTransition;
44 import android.animation.LayoutTransition.TransitionListener;
45 import android.animation.ObjectAnimator;
46 import android.animation.TimeInterpolator;
47 import android.animation.ValueAnimator;
48 import android.annotation.TargetApi;
49 import android.app.ActivityManager;
50 import android.content.ComponentName;
51 import android.content.Context;
52 import android.content.Intent;
53 import android.graphics.Canvas;
54 import android.graphics.Matrix;
55 import android.graphics.Point;
56 import android.graphics.Rect;
57 import android.graphics.RectF;
58 import android.graphics.Typeface;
59 import android.graphics.drawable.Drawable;
60 import android.os.Build;
61 import android.os.Handler;
62 import android.text.Layout;
63 import android.text.StaticLayout;
64 import android.text.TextPaint;
65 import android.util.AttributeSet;
66 import android.util.FloatProperty;
67 import android.util.SparseBooleanArray;
68 import android.view.HapticFeedbackConstants;
69 import android.view.KeyEvent;
70 import android.view.LayoutInflater;
71 import android.view.MotionEvent;
72 import android.view.View;
73 import android.view.ViewDebug;
74 import android.view.ViewGroup;
75 import android.view.accessibility.AccessibilityEvent;
76 import android.view.accessibility.AccessibilityNodeInfo;
77 import android.widget.ListView;
78 
79 import androidx.annotation.Nullable;
80 import androidx.dynamicanimation.animation.SpringForce;
81 
82 import com.android.launcher3.BaseActivity;
83 import com.android.launcher3.DeviceProfile;
84 import com.android.launcher3.Insettable;
85 import com.android.launcher3.InvariantDeviceProfile;
86 import com.android.launcher3.LauncherState;
87 import com.android.launcher3.PagedView;
88 import com.android.launcher3.R;
89 import com.android.launcher3.Utilities;
90 import com.android.launcher3.anim.AnimatorPlaybackController;
91 import com.android.launcher3.anim.PropertyListBuilder;
92 import com.android.launcher3.anim.SpringObjectAnimator;
93 import com.android.launcher3.config.FeatureFlags;
94 import com.android.launcher3.graphics.RotationMode;
95 import com.android.launcher3.userevent.nano.LauncherLogProto;
96 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
97 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
98 import com.android.launcher3.util.OverScroller;
99 import com.android.launcher3.util.PendingAnimation;
100 import com.android.launcher3.util.Themes;
101 import com.android.launcher3.util.ViewPool;
102 import com.android.quickstep.RecentsAnimationWrapper;
103 import com.android.quickstep.RecentsModel;
104 import com.android.quickstep.RecentsModel.TaskThumbnailChangeListener;
105 import com.android.quickstep.TaskThumbnailCache;
106 import com.android.quickstep.TaskUtils;
107 import com.android.quickstep.util.ClipAnimationHelper;
108 import com.android.systemui.shared.recents.model.Task;
109 import com.android.systemui.shared.recents.model.ThumbnailData;
110 import com.android.systemui.shared.system.ActivityManagerWrapper;
111 import com.android.systemui.shared.system.BackgroundExecutor;
112 import com.android.systemui.shared.system.LauncherEventUtil;
113 import com.android.systemui.shared.system.PackageManagerWrapper;
114 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
115 import com.android.systemui.shared.system.TaskStackChangeListener;
116 
117 import java.util.ArrayList;
118 import java.util.function.Consumer;
119 
120 /**
121  * A list of recent tasks.
122  */
123 @TargetApi(Build.VERSION_CODES.P)
124 public abstract class RecentsView<T extends BaseActivity> extends PagedView implements Insettable,
125         TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
126         InvariantDeviceProfile.OnIDPChangeListener, TaskThumbnailChangeListener {
127 
128     private static final String TAG = RecentsView.class.getSimpleName();
129 
130     public static final FloatProperty<RecentsView> CONTENT_ALPHA =
131             new FloatProperty<RecentsView>("contentAlpha") {
132                 @Override
133                 public void setValue(RecentsView view, float v) {
134                     view.setContentAlpha(v);
135                 }
136 
137                 @Override
138                 public Float get(RecentsView view) {
139                     return view.getContentAlpha();
140                 }
141             };
142 
143     public static final FloatProperty<RecentsView> FULLSCREEN_PROGRESS =
144             new FloatProperty<RecentsView>("fullscreenProgress") {
145                 @Override
146                 public void setValue(RecentsView recentsView, float v) {
147                     recentsView.setFullscreenProgress(v);
148                 }
149 
150                 @Override
151                 public Float get(RecentsView recentsView) {
152                     return recentsView.mFullscreenProgress;
153                 }
154             };
155 
156     protected RecentsAnimationWrapper mRecentsAnimationWrapper;
157     protected ClipAnimationHelper mClipAnimationHelper;
158     protected SyncRtSurfaceTransactionApplierCompat mSyncTransactionApplier;
159     protected int mTaskWidth;
160     protected int mTaskHeight;
161     protected boolean mEnableDrawingLiveTile = false;
162     protected final Rect mTempRect = new Rect();
163     protected final RectF mTempRectF = new RectF();
164 
165     private static final int DISMISS_TASK_DURATION = 300;
166     private static final int ADDITION_TASK_DURATION = 200;
167     // The threshold at which we update the SystemUI flags when animating from the task into the app
168     public static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.85f;
169 
170     protected final T mActivity;
171     private final float mFastFlingVelocity;
172     private final RecentsModel mModel;
173     private final int mTaskTopMargin;
174     private final ClearAllButton mClearAllButton;
175     private final Rect mClearAllButtonDeadZoneRect = new Rect();
176     private final Rect mTaskViewDeadZoneRect = new Rect();
177     protected final ClipAnimationHelper mTempClipAnimationHelper;
178 
179     private final ScrollState mScrollState = new ScrollState();
180     // Keeps track of the previously known visible tasks for purposes of loading/unloading task data
181     private final SparseBooleanArray mHasVisibleTaskData = new SparseBooleanArray();
182 
183     private final InvariantDeviceProfile mIdp;
184 
185     private final ViewPool<TaskView> mTaskViewPool;
186 
187     private boolean mDwbToastShown;
188     private boolean mDisallowScrollToClearAll;
189     private boolean mOverlayEnabled;
190     private boolean mFreezeViewVisibility;
191 
192     /**
193      * TODO: Call reloadIdNeeded in onTaskStackChanged.
194      */
195     private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
196         @Override
197         public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
198             if (!mHandleTaskStackChanges) {
199                 return;
200             }
201             // Check this is for the right user
202             if (!checkCurrentOrManagedUserId(userId, getContext())) {
203                 return;
204             }
205 
206             // Remove the task immediately from the task list
207             TaskView taskView = getTaskView(taskId);
208             if (taskView != null) {
209                 removeView(taskView);
210             }
211         }
212 
213         @Override
214         public void onActivityUnpinned() {
215             if (!mHandleTaskStackChanges) {
216                 return;
217             }
218 
219             reloadIfNeeded();
220             enableLayoutTransitions();
221         }
222 
223         @Override
224         public void onTaskRemoved(int taskId) {
225             if (!mHandleTaskStackChanges) {
226                 return;
227             }
228 
229             BackgroundExecutor.get().submit(() -> {
230                 TaskView taskView = getTaskView(taskId);
231                 if (taskView == null) {
232                     return;
233                 }
234                 Handler handler = taskView.getHandler();
235                 if (handler == null) {
236                     return;
237                 }
238 
239                 // TODO: Add callbacks from AM reflecting adding/removing from the recents list, and
240                 //       remove all these checks
241                 Task.TaskKey taskKey = taskView.getTask().key;
242                 if (PackageManagerWrapper.getInstance().getActivityInfo(taskKey.getComponent(),
243                         taskKey.userId) == null) {
244                     // The package was uninstalled
245                     handler.post(() ->
246                             dismissTask(taskView, true /* animate */, false /* removeTask */));
247                 } else {
248                     mModel.findTaskWithId(taskKey.id, (key) -> {
249                         if (key == null) {
250                             // The task was removed from the recents list
251                             handler.post(() -> dismissTask(taskView, true /* animate */,
252                                     false /* removeTask */));
253                         }
254                     });
255                 }
256             });
257         }
258 
259         @Override
260         public void onPinnedStackAnimationStarted() {
261             // Needed for activities that auto-enter PiP, which will not trigger a remote
262             // animation to be created
263             mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
264         }
265     };
266 
267     // Used to keep track of the last requested task list id, so that we do not request to load the
268     // tasks again if we have already requested it and the task list has not changed
269     private int mTaskListChangeId = -1;
270 
271     // Only valid until the launcher state changes to NORMAL
272     private int mRunningTaskId = -1;
273     private boolean mRunningTaskTileHidden;
274     private Task mTmpRunningTask;
275 
276     private boolean mRunningTaskIconScaledDown = false;
277 
278     private boolean mOverviewStateEnabled;
279     private boolean mHandleTaskStackChanges;
280     private boolean mSwipeDownShouldLaunchApp;
281     private boolean mTouchDownToStartHome;
282     private final float mSquaredTouchSlop;
283     private int mDownX;
284     private int mDownY;
285 
286     private PendingAnimation mPendingAnimation;
287     private LayoutTransition mLayoutTransition;
288 
289     @ViewDebug.ExportedProperty(category = "launcher")
290     private float mContentAlpha = 1;
291     @ViewDebug.ExportedProperty(category = "launcher")
292     private float mFullscreenProgress = 0;
293 
294     // Keeps track of task id whose visual state should not be reset
295     private int mIgnoreResetTaskId = -1;
296 
297     // Variables for empty state
298     private final Drawable mEmptyIcon;
299     private final CharSequence mEmptyMessage;
300     private final TextPaint mEmptyMessagePaint;
301     private final Point mLastMeasureSize = new Point();
302     private final int mEmptyMessagePadding;
303     private boolean mShowEmptyMessage;
304     private Layout mEmptyTextLayout;
305     private LiveTileOverlay mLiveTileOverlay;
306 
307     private BaseActivity.MultiWindowModeChangedListener mMultiWindowModeChangedListener =
308             (inMultiWindowMode) -> {
309         if (!inMultiWindowMode && mOverviewStateEnabled) {
310             // TODO: Re-enable layout transitions for addition of the unpinned task
311             reloadIfNeeded();
312         }
313     };
314 
RecentsView(Context context, AttributeSet attrs, int defStyleAttr)315     public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
316         super(context, attrs, defStyleAttr);
317         setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing));
318         setEnableFreeScroll(true);
319 
320         mFastFlingVelocity = getResources()
321                 .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
322         mActivity = (T) BaseActivity.fromContext(context);
323         mModel = RecentsModel.INSTANCE.get(context);
324         mIdp = InvariantDeviceProfile.INSTANCE.get(context);
325         mTempClipAnimationHelper = new ClipAnimationHelper(context);
326 
327         mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
328                 .inflate(R.layout.overview_clear_all_button, this, false);
329         mClearAllButton.setOnClickListener(this::dismissAllTasks);
330 
331         mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */,
332                 10 /* initial size */);
333 
334         mIsRtl = !Utilities.isRtl(getResources());
335         setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
336         mTaskTopMargin = getResources()
337                 .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
338         mSquaredTouchSlop = squaredTouchSlop(context);
339 
340         mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents);
341         mEmptyIcon.setCallback(this);
342         mEmptyMessage = context.getText(R.string.recents_empty_message);
343         mEmptyMessagePaint = new TextPaint();
344         mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary));
345         mEmptyMessagePaint.setTextSize(getResources()
346                 .getDimension(R.dimen.recents_empty_message_text_size));
347         mEmptyMessagePaint.setTypeface(Typeface.create(Themes.getDefaultBodyFont(context),
348                 Typeface.NORMAL));
349         mEmptyMessagePadding = getResources()
350                 .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
351         setWillNotDraw(false);
352         updateEmptyMessage();
353 
354         // Initialize quickstep specific cache params here, as this is constructed only once
355         mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5);
356     }
357 
getScroller()358     public OverScroller getScroller() {
359         return mScroller;
360     }
361 
isRtl()362     public boolean isRtl() {
363         return mIsRtl;
364     }
365 
366     @Override
onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData)367     public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
368         if (mHandleTaskStackChanges) {
369             TaskView taskView = getTaskView(taskId);
370             if (taskView != null) {
371                 Task task = taskView.getTask();
372                 taskView.getThumbnail().setThumbnail(task, thumbnailData);
373                 return task;
374             }
375         }
376         return null;
377     }
378 
updateThumbnail(int taskId, ThumbnailData thumbnailData)379     public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData) {
380         TaskView taskView = getTaskView(taskId);
381         if (taskView != null) {
382             taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData);
383         }
384         return taskView;
385     }
386 
387     @Override
onWindowVisibilityChanged(int visibility)388     protected void onWindowVisibilityChanged(int visibility) {
389         super.onWindowVisibilityChanged(visibility);
390         updateTaskStackListenerState();
391     }
392 
393     @Override
onIdpChanged(int changeFlags, InvariantDeviceProfile idp)394     public void onIdpChanged(int changeFlags, InvariantDeviceProfile idp) {
395         if ((changeFlags & CHANGE_FLAG_ICON_PARAMS) == 0) {
396             return;
397         }
398         mModel.getIconCache().clear();
399         reset();
400     }
401 
402     @Override
onAttachedToWindow()403     protected void onAttachedToWindow() {
404         super.onAttachedToWindow();
405         updateTaskStackListenerState();
406         mModel.getThumbnailCache().getHighResLoadingState().addCallback(this);
407         mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
408         ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
409         mSyncTransactionApplier = new SyncRtSurfaceTransactionApplierCompat(this);
410         RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
411         mIdp.addOnChangeListener(this);
412     }
413 
414     @Override
onDetachedFromWindow()415     protected void onDetachedFromWindow() {
416         super.onDetachedFromWindow();
417         updateTaskStackListenerState();
418         mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this);
419         mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
420         ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
421         mSyncTransactionApplier = null;
422         RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
423         mIdp.removeOnChangeListener(this);
424     }
425 
426     @Override
onViewRemoved(View child)427     public void onViewRemoved(View child) {
428         super.onViewRemoved(child);
429 
430         // Clear the task data for the removed child if it was visible
431         if (child != mClearAllButton) {
432             TaskView taskView = (TaskView) child;
433             mHasVisibleTaskData.delete(taskView.getTask().key.id);
434             mTaskViewPool.recycle(taskView);
435         }
436     }
437 
isTaskViewVisible(TaskView tv)438     public boolean isTaskViewVisible(TaskView tv) {
439         // For now, just check if it's the active task or an adjacent task
440         return Math.abs(indexOfChild(tv) - getNextPage()) <= 1;
441     }
442 
getTaskView(int taskId)443     public TaskView getTaskView(int taskId) {
444         for (int i = 0; i < getTaskViewCount(); i++) {
445             TaskView tv = (TaskView) getChildAt(i);
446             if (tv.getTask() != null && tv.getTask().key != null && tv.getTask().key.id == taskId) {
447                 return tv;
448             }
449         }
450         return null;
451     }
452 
setOverviewStateEnabled(boolean enabled)453     public void setOverviewStateEnabled(boolean enabled) {
454         mOverviewStateEnabled = enabled;
455         updateTaskStackListenerState();
456     }
457 
onDigitalWellbeingToastShown()458     public void onDigitalWellbeingToastShown() {
459         if (!mDwbToastShown) {
460             mDwbToastShown = true;
461             mActivity.getUserEventDispatcher().logActionTip(
462                     LauncherEventUtil.VISIBLE,
463                     LauncherLogProto.TipType.DWB_TOAST);
464         }
465     }
466 
467     @Override
onPageEndTransition()468     protected void onPageEndTransition() {
469         super.onPageEndTransition();
470         if (getNextPage() > 0) {
471             setSwipeDownShouldLaunchApp(true);
472         }
473     }
474 
475     @Override
onTouchEvent(MotionEvent ev)476     public boolean onTouchEvent(MotionEvent ev) {
477         super.onTouchEvent(ev);
478         final int x = (int) ev.getX();
479         final int y = (int) ev.getY();
480         switch (ev.getAction()) {
481             case MotionEvent.ACTION_UP:
482                 if (mTouchDownToStartHome) {
483                     startHome();
484                 }
485                 mTouchDownToStartHome = false;
486                 break;
487             case MotionEvent.ACTION_CANCEL:
488                 mTouchDownToStartHome = false;
489                 break;
490             case MotionEvent.ACTION_MOVE:
491                 // Passing the touch slop will not allow dismiss to home
492                 if (mTouchDownToStartHome &&
493                         (isHandlingTouch() ||
494                                 squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop)) {
495                     mTouchDownToStartHome = false;
496                 }
497                 break;
498             case MotionEvent.ACTION_DOWN:
499                 // Touch down anywhere but the deadzone around the visible clear all button and
500                 // between the task views will start home on touch up
501                 if (!isHandlingTouch()) {
502                     if (mShowEmptyMessage) {
503                         mTouchDownToStartHome = true;
504                     } else {
505                         updateDeadZoneRects();
506                         final boolean clearAllButtonDeadZoneConsumed =
507                                 mClearAllButton.getAlpha() == 1
508                                         && mClearAllButtonDeadZoneRect.contains(x, y);
509                         final boolean cameFromNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
510                         if (!clearAllButtonDeadZoneConsumed && !cameFromNavBar
511                                 && !mTaskViewDeadZoneRect.contains(x + getScrollX(), y)) {
512                             mTouchDownToStartHome = true;
513                         }
514                     }
515                 }
516                 mDownX = x;
517                 mDownY = y;
518                 break;
519         }
520 
521 
522         // Do not let touch escape to siblings below this view.
523         return isHandlingTouch() || shouldStealTouchFromSiblingsBelow(ev);
524     }
525 
shouldStealTouchFromSiblingsBelow(MotionEvent ev)526     protected boolean shouldStealTouchFromSiblingsBelow(MotionEvent ev) {
527         return true;
528     }
529 
applyLoadPlan(ArrayList<Task> tasks)530     private void applyLoadPlan(ArrayList<Task> tasks) {
531         if (mPendingAnimation != null) {
532             mPendingAnimation.addEndListener((onEndListener) -> applyLoadPlan(tasks));
533             return;
534         }
535 
536         if (tasks == null || tasks.isEmpty()) {
537             removeAllViews();
538             onTaskStackUpdated();
539             return;
540         }
541 
542         int oldChildCount = getChildCount();
543 
544         // Unload existing visible task data
545         unloadVisibleTaskData();
546 
547         TaskView ignoreRestTaskView =
548                 mIgnoreResetTaskId == -1 ? null : getTaskView(mIgnoreResetTaskId);
549 
550         final int requiredTaskCount = tasks.size();
551         if (getTaskViewCount() != requiredTaskCount) {
552             if (oldChildCount > 0) {
553                 removeView(mClearAllButton);
554             }
555             for (int i = getChildCount(); i < requiredTaskCount; i++) {
556                 addView(mTaskViewPool.getView());
557             }
558             while (getChildCount() > requiredTaskCount) {
559                 removeView(getChildAt(getChildCount() - 1));
560             }
561             if (requiredTaskCount > 0) {
562                 addView(mClearAllButton);
563             }
564         }
565 
566         // Rebind and reset all task views
567         for (int i = requiredTaskCount - 1; i >= 0; i--) {
568             final int pageIndex = requiredTaskCount - i - 1;
569             final Task task = tasks.get(i);
570             final TaskView taskView = (TaskView) getChildAt(pageIndex);
571             taskView.bind(task);
572         }
573         TaskView runningTaskView = getRunningTaskView();
574         if (runningTaskView != null) {
575             setCurrentPage(indexOfChild(runningTaskView));
576         }
577 
578         if (mIgnoreResetTaskId != -1 && getTaskView(mIgnoreResetTaskId) != ignoreRestTaskView) {
579             // If the taskView mapping is changing, do not preserve the visuals. Since we are
580             // mostly preserving the first task, and new taskViews are added to the end, it should
581             // generally map to the same task.
582             mIgnoreResetTaskId = -1;
583         }
584         resetTaskVisuals();
585         onTaskStackUpdated();
586         updateEnabledOverlays();
587     }
588 
getTaskViewCount()589     public int getTaskViewCount() {
590         // Account for the clear all button.
591         int childCount = getChildCount();
592         return childCount == 0 ? 0 : childCount - 1;
593     }
594 
onTaskStackUpdated()595     protected void onTaskStackUpdated() { }
596 
resetTaskVisuals()597     public void resetTaskVisuals() {
598         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
599             TaskView taskView = (TaskView) getChildAt(i);
600             if (mIgnoreResetTaskId != taskView.getTask().key.id) {
601                 taskView.resetVisualProperties();
602             }
603         }
604         if (mRunningTaskTileHidden) {
605             setRunningTaskHidden(mRunningTaskTileHidden);
606         }
607 
608         // Force apply the scale.
609         if (mIgnoreResetTaskId != mRunningTaskId) {
610             applyRunningTaskIconScale();
611         }
612 
613         updateCurveProperties();
614         // Update the set of visible task's data
615         loadVisibleTaskData();
616     }
617 
setFullscreenProgress(float fullscreenProgress)618     public void setFullscreenProgress(float fullscreenProgress) {
619         mFullscreenProgress = fullscreenProgress;
620         int taskCount = getTaskViewCount();
621         for (int i = 0; i < taskCount; i++) {
622             getTaskViewAt(i).setFullscreenProgress(mFullscreenProgress);
623         }
624     }
625 
updateTaskStackListenerState()626     private void updateTaskStackListenerState() {
627         boolean handleTaskStackChanges = mOverviewStateEnabled && isAttachedToWindow()
628                 && getWindowVisibility() == VISIBLE;
629         if (handleTaskStackChanges != mHandleTaskStackChanges) {
630             mHandleTaskStackChanges = handleTaskStackChanges;
631             if (handleTaskStackChanges) {
632                 reloadIfNeeded();
633             }
634         }
635     }
636 
637     @Override
setInsets(Rect insets)638     public void setInsets(Rect insets) {
639         mInsets.set(insets);
640         DeviceProfile dp = mActivity.getDeviceProfile();
641         getTaskSize(dp, mTempRect);
642         mTaskWidth = mTempRect.width();
643         mTaskHeight = mTempRect.height();
644 
645         mTempRect.top -= mTaskTopMargin;
646         setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top,
647                 dp.widthPx - mInsets.right - mTempRect.right,
648                 dp.heightPx - mInsets.bottom - mTempRect.bottom);
649     }
650 
getTaskSize(DeviceProfile dp, Rect outRect)651     protected abstract void getTaskSize(DeviceProfile dp, Rect outRect);
652 
getTaskSize(Rect outRect)653     public void getTaskSize(Rect outRect) {
654         getTaskSize(mActivity.getDeviceProfile(), outRect);
655     }
656 
657     @Override
computeScrollHelper()658     protected boolean computeScrollHelper() {
659         boolean scrolling = super.computeScrollHelper();
660         boolean isFlingingFast = false;
661         updateCurveProperties();
662         if (scrolling || isHandlingTouch()) {
663             if (scrolling) {
664                 // Check if we are flinging quickly to disable high res thumbnail loading
665                 isFlingingFast = mScroller.getCurrVelocity() > mFastFlingVelocity;
666             }
667 
668             // After scrolling, update the visible task's data
669             loadVisibleTaskData();
670         }
671 
672         // Update the high res thumbnail loader state
673         mModel.getThumbnailCache().getHighResLoadingState().setFlingingFast(isFlingingFast);
674         return scrolling;
675     }
676 
677     /**
678      * Scales and adjusts translation of adjacent pages as if on a curved carousel.
679      */
updateCurveProperties()680     public void updateCurveProperties() {
681         if (getPageCount() == 0 || getPageAt(0).getMeasuredWidth() == 0) {
682             return;
683         }
684         int scrollX = getScrollX();
685         final int halfPageWidth = getNormalChildWidth() / 2;
686         final int screenCenter = mInsets.left + getPaddingLeft() + scrollX + halfPageWidth;
687         final int halfScreenWidth = getMeasuredWidth() / 2;
688         final int pageSpacing = mPageSpacing;
689         mScrollState.scrollFromEdge = mIsRtl ? scrollX : (mMaxScrollX - scrollX);
690 
691         final int pageCount = getPageCount();
692         for (int i = 0; i < pageCount; i++) {
693             View page = getPageAt(i);
694             float pageCenter = page.getLeft() + page.getTranslationX() + halfPageWidth;
695             float distanceFromScreenCenter = screenCenter - pageCenter;
696             float distanceToReachEdge = halfScreenWidth + halfPageWidth + pageSpacing;
697             mScrollState.linearInterpolation = Math.min(1,
698                     Math.abs(distanceFromScreenCenter) / distanceToReachEdge);
699             ((PageCallbacks) page).onPageScroll(mScrollState);
700         }
701     }
702 
703     /**
704      * Iterates through all thet asks, and loads the associated task data for newly visible tasks,
705      * and unloads the associated task data for tasks that are no longer visible.
706      */
loadVisibleTaskData()707     public void loadVisibleTaskData() {
708         if (!mOverviewStateEnabled || mTaskListChangeId == -1) {
709             // Skip loading visible task data if we've already left the overview state, or if the
710             // task list hasn't been loaded yet (the task views will not reflect the task list)
711             return;
712         }
713 
714         int centerPageIndex = getPageNearestToCenterOfScreen();
715         int numChildren = getTaskViewCount();
716         int lower = Math.max(0, centerPageIndex - 2);
717         int upper = Math.min(centerPageIndex + 2, numChildren - 1);
718 
719         // Update the task data for the in/visible children
720         for (int i = 0; i < numChildren; i++) {
721             TaskView taskView = (TaskView) getChildAt(i);
722             Task task = taskView.getTask();
723             boolean visible = lower <= i && i <= upper;
724             if (visible) {
725                 if (task == mTmpRunningTask) {
726                     // Skip loading if this is the task that we are animating into
727                     continue;
728                 }
729                 if (!mHasVisibleTaskData.get(task.key.id)) {
730                     taskView.onTaskListVisibilityChanged(true /* visible */);
731                 }
732                 mHasVisibleTaskData.put(task.key.id, visible);
733             } else {
734                 if (mHasVisibleTaskData.get(task.key.id)) {
735                     taskView.onTaskListVisibilityChanged(false /* visible */);
736                 }
737                 mHasVisibleTaskData.delete(task.key.id);
738             }
739         }
740     }
741 
742     /**
743      * Unloads any associated data from the currently visible tasks
744      */
unloadVisibleTaskData()745     private void unloadVisibleTaskData() {
746         for (int i = 0; i < mHasVisibleTaskData.size(); i++) {
747             if (mHasVisibleTaskData.valueAt(i)) {
748                 TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i));
749                 if (taskView != null) {
750                     taskView.onTaskListVisibilityChanged(false /* visible */);
751                 }
752             }
753         }
754         mHasVisibleTaskData.clear();
755     }
756 
757     @Override
onHighResLoadingStateChanged(boolean enabled)758     public void onHighResLoadingStateChanged(boolean enabled) {
759         // Whenever the high res loading state changes, poke each of the visible tasks to see if
760         // they want to updated their thumbnail state
761         for (int i = 0; i < mHasVisibleTaskData.size(); i++) {
762             if (mHasVisibleTaskData.valueAt(i)) {
763                 TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i));
764                 if (taskView != null) {
765                     // Poke the view again, which will trigger it to load high res if the state
766                     // is enabled
767                     taskView.onTaskListVisibilityChanged(true /* visible */);
768                 }
769             }
770         }
771     }
772 
startHome()773     public abstract void startHome();
774 
reset()775     public void reset() {
776         setCurrentTask(-1);
777         mIgnoreResetTaskId = -1;
778         mTaskListChangeId = -1;
779 
780         mRecentsAnimationWrapper = null;
781         mClipAnimationHelper = null;
782 
783         unloadVisibleTaskData();
784         setCurrentPage(0);
785         mDwbToastShown = false;
786         mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, 0);
787     }
788 
getRunningTaskView()789     public @Nullable TaskView getRunningTaskView() {
790         return getTaskView(mRunningTaskId);
791     }
792 
getRunningTaskIndex()793     public int getRunningTaskIndex() {
794         TaskView tv = getRunningTaskView();
795         return tv == null ? -1 : indexOfChild(tv);
796     }
797 
798     /**
799      * Reloads the view if anything in recents changed.
800      */
reloadIfNeeded()801     public void reloadIfNeeded() {
802         if (!mModel.isTaskListValid(mTaskListChangeId)) {
803             mTaskListChangeId = mModel.getTasks(this::applyLoadPlan);
804         }
805     }
806 
807     /**
808      * Called when a gesture from an app is starting.
809      */
onGestureAnimationStart(int runningTaskId)810     public void onGestureAnimationStart(int runningTaskId) {
811         // This needs to be called before the other states are set since it can create the task view
812         showCurrentTask(runningTaskId);
813         setEnableFreeScroll(false);
814         setEnableDrawingLiveTile(false);
815         setRunningTaskHidden(true);
816         setRunningTaskIconScaledDown(true);
817     }
818 
819     /**
820      * Called only when a swipe-up gesture from an app has completed. Only called after
821      * {@link #onGestureAnimationStart} and {@link #onGestureAnimationEnd()}.
822      */
onSwipeUpAnimationSuccess()823     public void onSwipeUpAnimationSuccess() {
824         if (getRunningTaskView() != null) {
825             float startProgress = ENABLE_QUICKSTEP_LIVE_TILE.get()
826                     ? mLiveTileOverlay.cancelIconAnimation()
827                     : 0f;
828             animateUpRunningTaskIconScale(startProgress);
829         }
830         setSwipeDownShouldLaunchApp(true);
831     }
832 
833     /**
834      * Called when a gesture from an app has finished.
835      */
onGestureAnimationEnd()836     public void onGestureAnimationEnd() {
837         setEnableFreeScroll(true);
838         setEnableDrawingLiveTile(true);
839         setOnScrollChangeListener(null);
840         setRunningTaskViewShowScreenshot(true);
841         setRunningTaskHidden(false);
842         animateUpRunningTaskIconScale();
843     }
844 
845     /**
846      * Creates a task view (if necessary) to represent the task with the {@param runningTaskId}.
847      *
848      * All subsequent calls to reload will keep the task as the first item until {@link #reset()}
849      * is called.  Also scrolls the view to this task.
850      */
showCurrentTask(int runningTaskId)851     public void showCurrentTask(int runningTaskId) {
852         if (getChildCount() == 0) {
853             // Add an empty view for now until the task plan is loaded and applied
854             final TaskView taskView = mTaskViewPool.getView();
855             addView(taskView);
856             addView(mClearAllButton);
857 
858             // The temporary running task is only used for the duration between the start of the
859             // gesture and the task list is loaded and applied
860             mTmpRunningTask = new Task(new Task.TaskKey(runningTaskId, 0, new Intent(),
861                     new ComponentName(getContext(), getClass()), 0, 0), null, null, "", "", 0, 0,
862                     false, true, false, false, new ActivityManager.TaskDescription(), 0,
863                     new ComponentName("", ""), false);
864             taskView.bind(mTmpRunningTask);
865         }
866 
867         boolean runningTaskTileHidden = mRunningTaskTileHidden;
868         setCurrentTask(runningTaskId);
869         setCurrentPage(getRunningTaskIndex());
870         setRunningTaskViewShowScreenshot(false);
871         setRunningTaskHidden(runningTaskTileHidden);
872 
873         // Reload the task list
874         mTaskListChangeId = mModel.getTasks(this::applyLoadPlan);
875     }
876 
877     /**
878      * Sets the running task id, cleaning up the old running task if necessary.
879      * @param runningTaskId
880      */
setCurrentTask(int runningTaskId)881     public void setCurrentTask(int runningTaskId) {
882         if (mRunningTaskId == runningTaskId) {
883             return;
884         }
885 
886         if (mRunningTaskId != -1) {
887             // Reset the state on the old running task view
888             setRunningTaskIconScaledDown(false);
889             setRunningTaskViewShowScreenshot(true);
890             setRunningTaskHidden(false);
891         }
892         mRunningTaskId = runningTaskId;
893     }
894 
895     /**
896      * Hides the tile associated with {@link #mRunningTaskId}
897      */
setRunningTaskHidden(boolean isHidden)898     public void setRunningTaskHidden(boolean isHidden) {
899         mRunningTaskTileHidden = isHidden;
900         TaskView runningTask = getRunningTaskView();
901         if (runningTask != null) {
902             runningTask.setStableAlpha(isHidden ? 0 : mContentAlpha);
903         }
904     }
905 
setRunningTaskViewShowScreenshot(boolean showScreenshot)906     private void setRunningTaskViewShowScreenshot(boolean showScreenshot) {
907         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
908             TaskView runningTaskView = getRunningTaskView();
909             if (runningTaskView != null) {
910                 runningTaskView.setShowScreenshot(showScreenshot);
911             }
912         }
913     }
914 
showNextTask()915     public void showNextTask() {
916         TaskView runningTaskView = getRunningTaskView();
917         if (runningTaskView == null) {
918             // Launch the first task
919             if (getTaskViewCount() > 0) {
920                 getTaskViewAt(0).launchTask(true /* animate */);
921             }
922         } else {
923             TaskView nextTaskView = getNextTaskView();
924             if (nextTaskView != null) {
925                 nextTaskView.launchTask(true /* animate */);
926             } else {
927                 runningTaskView.launchTask(true /* animate */);
928             }
929         }
930     }
931 
setRunningTaskIconScaledDown(boolean isScaledDown)932     public void setRunningTaskIconScaledDown(boolean isScaledDown) {
933         if (mRunningTaskIconScaledDown != isScaledDown) {
934             mRunningTaskIconScaledDown = isScaledDown;
935             applyRunningTaskIconScale();
936         }
937     }
938 
applyRunningTaskIconScale()939     private void applyRunningTaskIconScale() {
940         TaskView firstTask = getRunningTaskView();
941         if (firstTask != null) {
942             firstTask.setIconScaleAndDim(mRunningTaskIconScaledDown ? 0 : 1);
943         }
944     }
945 
animateUpRunningTaskIconScale()946     public void animateUpRunningTaskIconScale() {
947         animateUpRunningTaskIconScale(0);
948     }
949 
animateUpRunningTaskIconScale(float startProgress)950     public void animateUpRunningTaskIconScale(float startProgress) {
951         mRunningTaskIconScaledDown = false;
952         TaskView firstTask = getRunningTaskView();
953         if (firstTask != null) {
954             firstTask.animateIconScaleAndDimIntoView();
955             firstTask.setIconScaleAnimStartProgress(startProgress);
956         }
957     }
958 
enableLayoutTransitions()959     private void enableLayoutTransitions() {
960         if (mLayoutTransition == null) {
961             mLayoutTransition = new LayoutTransition();
962             mLayoutTransition.enableTransitionType(LayoutTransition.APPEARING);
963             mLayoutTransition.setDuration(ADDITION_TASK_DURATION);
964             mLayoutTransition.setStartDelay(LayoutTransition.APPEARING, 0);
965 
966             mLayoutTransition.addTransitionListener(new TransitionListener() {
967                 @Override
968                 public void startTransition(LayoutTransition transition, ViewGroup viewGroup,
969                     View view, int i) {
970                 }
971 
972                 @Override
973                 public void endTransition(LayoutTransition transition, ViewGroup viewGroup,
974                     View view, int i) {
975                     // When the unpinned task is added, snap to first page and disable transitions
976                     if (view instanceof TaskView) {
977                         snapToPage(0);
978                         disableLayoutTransitions();
979                     }
980 
981                 }
982             });
983         }
984         setLayoutTransition(mLayoutTransition);
985     }
986 
disableLayoutTransitions()987     private void disableLayoutTransitions() {
988         setLayoutTransition(null);
989     }
990 
setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp)991     public void setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp) {
992         mSwipeDownShouldLaunchApp = swipeDownShouldLaunchApp;
993     }
994 
shouldSwipeDownLaunchApp()995     public boolean shouldSwipeDownLaunchApp() {
996         return mSwipeDownShouldLaunchApp;
997     }
998 
999     public interface PageCallbacks {
1000 
1001         /**
1002          * Updates the page UI based on scroll params.
1003          */
onPageScroll(ScrollState scrollState)1004         default void onPageScroll(ScrollState scrollState) {}
1005     }
1006 
1007     public static class ScrollState {
1008 
1009         /**
1010          * The progress from 0 to 1, where 0 is the center
1011          * of the screen and 1 is the edge of the screen.
1012          */
1013         public float linearInterpolation;
1014 
1015         /**
1016          * The amount by which all the content is scrolled relative to the end of the list.
1017          */
1018         public float scrollFromEdge;
1019     }
1020 
setIgnoreResetTask(int taskId)1021     public void setIgnoreResetTask(int taskId) {
1022         mIgnoreResetTaskId = taskId;
1023     }
1024 
clearIgnoreResetTask(int taskId)1025     public void clearIgnoreResetTask(int taskId) {
1026         if (mIgnoreResetTaskId == taskId) {
1027             mIgnoreResetTaskId = -1;
1028         }
1029     }
1030 
addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration)1031     private void addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration) {
1032         addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim);
1033         if (QUICKSTEP_SPRINGS.get() && taskView instanceof TaskView)
1034             addAnim(new SpringObjectAnimator<>(taskView, VIEW_TRANSLATE_Y,
1035                             MIN_VISIBLE_CHANGE_PIXELS, SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY,
1036                             SpringForce.STIFFNESS_MEDIUM,
1037                             0, -taskView.getHeight()),
1038                     duration, LINEAR, anim);
1039         else {
1040             addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()),
1041                     duration, LINEAR, anim);
1042         }
1043     }
1044 
removeTask(Task task, int index, PendingAnimation.OnEndListener onEndListener, boolean shouldLog)1045     private void removeTask(Task task, int index, PendingAnimation.OnEndListener onEndListener,
1046                             boolean shouldLog) {
1047         if (task != null) {
1048             ActivityManagerWrapper.getInstance().removeTask(task.key.id);
1049             if (shouldLog) {
1050                 mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
1051                         onEndListener.logAction, Direction.UP, index,
1052                         TaskUtils.getLaunchComponentKeyForTask(task.key));
1053             }
1054         }
1055     }
1056 
createTaskDismissAnimation(TaskView taskView, boolean animateTaskView, boolean shouldRemoveTask, long duration)1057     public PendingAnimation createTaskDismissAnimation(TaskView taskView, boolean animateTaskView,
1058             boolean shouldRemoveTask, long duration) {
1059         if (mPendingAnimation != null) {
1060             mPendingAnimation.finish(false, Touch.SWIPE);
1061         }
1062         AnimatorSet anim = new AnimatorSet();
1063         PendingAnimation pendingAnimation = new PendingAnimation(anim);
1064 
1065         int count = getPageCount();
1066         if (count == 0) {
1067             return pendingAnimation;
1068         }
1069 
1070         int[] oldScroll = new int[count];
1071         getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
1072 
1073         int[] newScroll = new int[count];
1074         getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView);
1075 
1076         int taskCount = getTaskViewCount();
1077         int scrollDiffPerPage = 0;
1078         if (count > 1) {
1079             scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]);
1080         }
1081         int draggedIndex = indexOfChild(taskView);
1082 
1083         boolean needsCurveUpdates = false;
1084         for (int i = 0; i < count; i++) {
1085             View child = getChildAt(i);
1086             if (child == taskView) {
1087                 if (animateTaskView) {
1088                     addDismissedTaskAnimations(taskView, anim, duration);
1089                 }
1090             } else {
1091                 // If we just take newScroll - oldScroll, everything to the right of dragged task
1092                 // translates to the left. We need to offset this in some cases:
1093                 // - In RTL, add page offset to all pages, since we want pages to move to the right
1094                 // Additionally, add a page offset if:
1095                 // - Current page is rightmost page (leftmost for RTL)
1096                 // - Dragging an adjacent page on the left side (right side for RTL)
1097                 int offset = mIsRtl ? scrollDiffPerPage : 0;
1098                 if (mCurrentPage == draggedIndex) {
1099                     int lastPage = taskCount - 1;
1100                     if (mCurrentPage == lastPage) {
1101                         offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
1102                     }
1103                 } else {
1104                     // Dragging an adjacent page.
1105                     int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR)
1106                     if (draggedIndex == negativeAdjacent) {
1107                         offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
1108                     }
1109                 }
1110                 int scrollDiff = newScroll[i] - oldScroll[i] + offset;
1111                 if (scrollDiff != 0) {
1112                     if (QUICKSTEP_SPRINGS.get() && child instanceof TaskView) {
1113                         addAnim(new SpringObjectAnimator<>(child, VIEW_TRANSLATE_X,
1114                                 MIN_VISIBLE_CHANGE_PIXELS, SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY,
1115                                 SpringForce.STIFFNESS_MEDIUM,
1116                                 0, scrollDiff), duration, ACCEL, anim);
1117                     } else {
1118                         addAnim(ObjectAnimator.ofFloat(child, TRANSLATION_X, scrollDiff), duration,
1119                                 ACCEL, anim);
1120                     }
1121 
1122                     needsCurveUpdates = true;
1123                 }
1124             }
1125         }
1126 
1127         if (needsCurveUpdates) {
1128             ValueAnimator va = ValueAnimator.ofFloat(0, 1);
1129             va.addUpdateListener((a) -> updateCurveProperties());
1130             anim.play(va);
1131         }
1132 
1133         // Add a tiny bit of translation Z, so that it draws on top of other views
1134         if (animateTaskView) {
1135             taskView.setTranslationZ(0.1f);
1136         }
1137 
1138         mPendingAnimation = pendingAnimation;
1139         mPendingAnimation.addEndListener(new Consumer<PendingAnimation.OnEndListener>() {
1140             @Override
1141             public void accept(PendingAnimation.OnEndListener onEndListener) {
1142                 if (ENABLE_QUICKSTEP_LIVE_TILE.get() &&
1143                         taskView.isRunningTask() && onEndListener.isSuccess) {
1144                     finishRecentsAnimation(true /* toHome */, () -> onEnd(onEndListener));
1145                 } else {
1146                     onEnd(onEndListener);
1147                 }
1148             }
1149 
1150             private void onEnd(PendingAnimation.OnEndListener onEndListener) {
1151                 if (onEndListener.isSuccess) {
1152                     if (shouldRemoveTask) {
1153                         removeTask(taskView.getTask(), draggedIndex, onEndListener, true);
1154                     }
1155 
1156                     int pageToSnapTo = mCurrentPage;
1157                     if (draggedIndex < pageToSnapTo ||
1158                             pageToSnapTo == (getTaskViewCount() - 1)) {
1159                         pageToSnapTo -= 1;
1160                     }
1161                     removeView(taskView);
1162 
1163                     if (getTaskViewCount() == 0) {
1164                         removeView(mClearAllButton);
1165                         startHome();
1166                     } else {
1167                         snapToPageImmediately(pageToSnapTo);
1168                     }
1169                 }
1170                 resetTaskVisuals();
1171                 mPendingAnimation = null;
1172             }
1173         });
1174         return pendingAnimation;
1175     }
1176 
createAllTasksDismissAnimation(long duration)1177     public PendingAnimation createAllTasksDismissAnimation(long duration) {
1178         if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) {
1179             throw new IllegalStateException("Another pending animation is still running");
1180         }
1181         AnimatorSet anim = new AnimatorSet();
1182         PendingAnimation pendingAnimation = new PendingAnimation(anim);
1183 
1184         int count = getTaskViewCount();
1185         for (int i = 0; i < count; i++) {
1186             addDismissedTaskAnimations(getChildAt(i), anim, duration);
1187         }
1188 
1189         mPendingAnimation = pendingAnimation;
1190         mPendingAnimation.addEndListener((onEndListener) -> {
1191             if (onEndListener.isSuccess) {
1192                 // Remove all the task views now
1193                 ActivityManagerWrapper.getInstance().removeAllRecentTasks();
1194                 removeAllViews();
1195                 startHome();
1196             }
1197             mPendingAnimation = null;
1198         });
1199         return pendingAnimation;
1200     }
1201 
addAnim(Animator anim, long duration, TimeInterpolator interpolator, AnimatorSet set)1202     private static void addAnim(Animator anim, long duration,
1203             TimeInterpolator interpolator, AnimatorSet set) {
1204         anim.setDuration(duration).setInterpolator(interpolator);
1205         set.play(anim);
1206     }
1207 
snapToPageRelative(int pageCount, int delta, boolean cycle)1208     private boolean snapToPageRelative(int pageCount, int delta, boolean cycle) {
1209         if (pageCount == 0) {
1210             return false;
1211         }
1212         final int newPageUnbound = getNextPage() + delta;
1213         if (!cycle && (newPageUnbound < 0 || newPageUnbound >= pageCount)) {
1214             return false;
1215         }
1216         snapToPage((newPageUnbound + pageCount) % pageCount);
1217         getChildAt(getNextPage()).requestFocus();
1218         return true;
1219     }
1220 
runDismissAnimation(PendingAnimation pendingAnim)1221     private void runDismissAnimation(PendingAnimation pendingAnim) {
1222         AnimatorPlaybackController controller = AnimatorPlaybackController.wrap(
1223                 pendingAnim.anim, DISMISS_TASK_DURATION);
1224         controller.dispatchOnStart();
1225         controller.setEndAction(() -> pendingAnim.finish(true, Touch.SWIPE));
1226         controller.getAnimationPlayer().setInterpolator(FAST_OUT_SLOW_IN);
1227         controller.start();
1228     }
1229 
dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask)1230     public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) {
1231         runDismissAnimation(createTaskDismissAnimation(taskView, animateTaskView, removeTask,
1232                 DISMISS_TASK_DURATION));
1233     }
1234 
1235     @SuppressWarnings("unused")
dismissAllTasks(View view)1236     private void dismissAllTasks(View view) {
1237         runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION));
1238         mActivity.getUserEventDispatcher().logActionOnControl(TAP, CLEAR_ALL_BUTTON);
1239     }
1240 
dismissCurrentTask()1241     private void dismissCurrentTask() {
1242         TaskView taskView = getTaskView(getNextPage());
1243         if (taskView != null) {
1244             dismissTask(taskView, true /*animateTaskView*/, true /*removeTask*/);
1245         }
1246     }
1247 
1248     @Override
dispatchKeyEvent(KeyEvent event)1249     public boolean dispatchKeyEvent(KeyEvent event) {
1250         if (event.getAction() == KeyEvent.ACTION_DOWN) {
1251             switch (event.getKeyCode()) {
1252                 case KeyEvent.KEYCODE_TAB:
1253                     return snapToPageRelative(getTaskViewCount(), event.isShiftPressed() ? -1 : 1,
1254                             event.isAltPressed() /* cycle */);
1255                 case KeyEvent.KEYCODE_DPAD_RIGHT:
1256                     return snapToPageRelative(getPageCount(), mIsRtl ? -1 : 1, false /* cycle */);
1257                 case KeyEvent.KEYCODE_DPAD_LEFT:
1258                     return snapToPageRelative(getPageCount(), mIsRtl ? 1 : -1, false /* cycle */);
1259                 case KeyEvent.KEYCODE_DEL:
1260                 case KeyEvent.KEYCODE_FORWARD_DEL:
1261                     dismissCurrentTask();
1262                     return true;
1263                 case KeyEvent.KEYCODE_NUMPAD_DOT:
1264                     if (event.isAltPressed()) {
1265                         // Numpad DEL pressed while holding Alt.
1266                         dismissCurrentTask();
1267                         return true;
1268                     }
1269             }
1270         }
1271         return super.dispatchKeyEvent(event);
1272     }
1273 
1274     @Override
onFocusChanged(boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect)1275     protected void onFocusChanged(boolean gainFocus, int direction,
1276             @Nullable Rect previouslyFocusedRect) {
1277         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
1278         if (gainFocus && getChildCount() > 0) {
1279             switch (direction) {
1280                 case FOCUS_FORWARD:
1281                     setCurrentPage(0);
1282                     break;
1283                 case FOCUS_BACKWARD:
1284                 case FOCUS_RIGHT:
1285                 case FOCUS_LEFT:
1286                     setCurrentPage(getChildCount() - 1);
1287                     break;
1288             }
1289         }
1290     }
1291 
getContentAlpha()1292     public float getContentAlpha() {
1293         return mContentAlpha;
1294     }
1295 
setContentAlpha(float alpha)1296     public void setContentAlpha(float alpha) {
1297         if (alpha == mContentAlpha) {
1298             return;
1299         }
1300         alpha = Utilities.boundToRange(alpha, 0, 1);
1301         mContentAlpha = alpha;
1302         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
1303             TaskView child = getTaskViewAt(i);
1304             if (!mRunningTaskTileHidden || child.getTask().key.id != mRunningTaskId) {
1305                 child.setStableAlpha(alpha);
1306             }
1307         }
1308         mClearAllButton.setContentAlpha(mContentAlpha);
1309 
1310         int alphaInt = Math.round(alpha * 255);
1311         mEmptyMessagePaint.setAlpha(alphaInt);
1312         mEmptyIcon.setAlpha(alphaInt);
1313 
1314         if (alpha > 0) {
1315             setVisibility(VISIBLE);
1316         } else if (!mFreezeViewVisibility) {
1317             setVisibility(GONE);
1318         }
1319     }
1320 
1321     /**
1322      * Freezes the view visibility change. When frozen, the view will not change its visibility
1323      * to gone due to alpha changes.
1324      */
setFreezeViewVisibility(boolean freezeViewVisibility)1325     public void setFreezeViewVisibility(boolean freezeViewVisibility) {
1326         if (mFreezeViewVisibility != freezeViewVisibility) {
1327             mFreezeViewVisibility = freezeViewVisibility;
1328 
1329             if (!mFreezeViewVisibility) {
1330                 setVisibility(mContentAlpha > 0 ? VISIBLE : GONE);
1331             }
1332         }
1333     }
1334 
1335     @Override
onViewAdded(View child)1336     public void onViewAdded(View child) {
1337         super.onViewAdded(child);
1338         child.setAlpha(mContentAlpha);
1339     }
1340 
1341     /**
1342      * @return The most recent task that is older than the currently running task. If there is
1343      * currently no running task or there is no task older than it, then return null.
1344      */
1345     @Nullable
getNextTaskView()1346     public TaskView getNextTaskView() {
1347         TaskView runningTaskView = getRunningTaskView();
1348         if (runningTaskView == null) {
1349             return null;
1350         }
1351         return getTaskViewAt(indexOfChild(runningTaskView) + 1);
1352     }
1353 
getTaskViewAt(int index)1354     public TaskView getTaskViewAt(int index) {
1355         View child = getChildAt(index);
1356         return child == mClearAllButton ? null : (TaskView) child;
1357     }
1358 
updateEmptyMessage()1359     public void updateEmptyMessage() {
1360         boolean isEmpty = getChildCount() == 0;
1361         boolean hasSizeChanged = mLastMeasureSize.x != getWidth()
1362                 || mLastMeasureSize.y != getHeight();
1363         if (isEmpty == mShowEmptyMessage && !hasSizeChanged) {
1364             return;
1365         }
1366         setContentDescription(isEmpty ? mEmptyMessage : "");
1367         mShowEmptyMessage = isEmpty;
1368         updateEmptyStateUi(hasSizeChanged);
1369         invalidate();
1370     }
1371 
1372     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)1373     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1374         super.onLayout(changed, left, top, right, bottom);
1375         updateEmptyStateUi(changed);
1376 
1377         // Set the pivot points to match the task preview center
1378         setPivotY(((mInsets.top + getPaddingTop() + mTaskTopMargin)
1379                 + (getHeight() - mInsets.bottom - getPaddingBottom())) / 2);
1380         setPivotX(((mInsets.left + getPaddingLeft())
1381                 + (getWidth() - mInsets.right - getPaddingRight())) / 2);
1382     }
1383 
updateDeadZoneRects()1384     private void updateDeadZoneRects() {
1385         // Get the deadzone rect surrounding the clear all button to not dismiss overview to home
1386         mClearAllButtonDeadZoneRect.setEmpty();
1387         if (mClearAllButton.getWidth() > 0) {
1388             int verticalMargin = getResources()
1389                     .getDimensionPixelSize(R.dimen.recents_clear_all_deadzone_vertical_margin);
1390             mClearAllButton.getHitRect(mClearAllButtonDeadZoneRect);
1391             mClearAllButtonDeadZoneRect.inset(-getPaddingRight() / 2, -verticalMargin);
1392         }
1393 
1394         // Get the deadzone rect between the task views
1395         mTaskViewDeadZoneRect.setEmpty();
1396         int count = getTaskViewCount();
1397         if (count > 0) {
1398             final View taskView = getTaskViewAt(0);
1399             getTaskViewAt(count - 1).getHitRect(mTaskViewDeadZoneRect);
1400             mTaskViewDeadZoneRect.union(taskView.getLeft(), taskView.getTop(), taskView.getRight(),
1401                     taskView.getBottom());
1402         }
1403     }
1404 
updateEmptyStateUi(boolean sizeChanged)1405     private void updateEmptyStateUi(boolean sizeChanged) {
1406         boolean hasValidSize = getWidth() > 0 && getHeight() > 0;
1407         if (sizeChanged && hasValidSize) {
1408             mEmptyTextLayout = null;
1409             mLastMeasureSize.set(getWidth(), getHeight());
1410         }
1411 
1412         if (mShowEmptyMessage && hasValidSize && mEmptyTextLayout == null) {
1413             int availableWidth = mLastMeasureSize.x - mEmptyMessagePadding - mEmptyMessagePadding;
1414             mEmptyTextLayout = StaticLayout.Builder.obtain(mEmptyMessage, 0, mEmptyMessage.length(),
1415                     mEmptyMessagePaint, availableWidth)
1416                     .setAlignment(Layout.Alignment.ALIGN_CENTER)
1417                     .build();
1418             int totalHeight = mEmptyTextLayout.getHeight()
1419                     + mEmptyMessagePadding + mEmptyIcon.getIntrinsicHeight();
1420 
1421             int top = (mLastMeasureSize.y - totalHeight) / 2;
1422             int left = (mLastMeasureSize.x - mEmptyIcon.getIntrinsicWidth()) / 2;
1423             mEmptyIcon.setBounds(left, top, left + mEmptyIcon.getIntrinsicWidth(),
1424                     top + mEmptyIcon.getIntrinsicHeight());
1425         }
1426     }
1427 
1428     @Override
verifyDrawable(Drawable who)1429     protected boolean verifyDrawable(Drawable who) {
1430         return super.verifyDrawable(who) || (mShowEmptyMessage && who == mEmptyIcon);
1431     }
1432 
maybeDrawEmptyMessage(Canvas canvas)1433     protected void maybeDrawEmptyMessage(Canvas canvas) {
1434         if (mShowEmptyMessage && mEmptyTextLayout != null) {
1435             // Offset to center in the visible (non-padded) part of RecentsView
1436             mTempRect.set(mInsets.left + getPaddingLeft(), mInsets.top + getPaddingTop(),
1437                     mInsets.right + getPaddingRight(), mInsets.bottom + getPaddingBottom());
1438             canvas.save();
1439             canvas.translate(getScrollX() + (mTempRect.left - mTempRect.right) / 2,
1440                     (mTempRect.top - mTempRect.bottom) / 2);
1441             mEmptyIcon.draw(canvas);
1442             canvas.translate(mEmptyMessagePadding,
1443                     mEmptyIcon.getBounds().bottom + mEmptyMessagePadding);
1444             mEmptyTextLayout.draw(canvas);
1445             canvas.restore();
1446         }
1447     }
1448 
1449     /**
1450      * Animate adjacent tasks off screen while scaling up.
1451      *
1452      * If launching one of the adjacent tasks, parallax the center task and other adjacent task
1453      * to the right.
1454      */
createAdjacentPageAnimForTaskLaunch( TaskView tv, ClipAnimationHelper clipAnimationHelper)1455     public AnimatorSet createAdjacentPageAnimForTaskLaunch(
1456             TaskView tv, ClipAnimationHelper clipAnimationHelper) {
1457         AnimatorSet anim = new AnimatorSet();
1458 
1459         int taskIndex = indexOfChild(tv);
1460         int centerTaskIndex = getCurrentPage();
1461         boolean launchingCenterTask = taskIndex == centerTaskIndex;
1462 
1463         LauncherState.ScaleAndTranslation toScaleAndTranslation = clipAnimationHelper
1464                 .getScaleAndTranslation();
1465         float toScale = toScaleAndTranslation.scale;
1466         float toTranslationY = toScaleAndTranslation.translationY;
1467         if (launchingCenterTask) {
1468             RecentsView recentsView = tv.getRecentsView();
1469             anim.play(ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY, toScale));
1470             anim.play(ObjectAnimator.ofFloat(recentsView, TRANSLATION_Y, toTranslationY));
1471             anim.play(ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS, 1));
1472         } else {
1473             // We are launching an adjacent task, so parallax the center and other adjacent task.
1474             float displacementX = tv.getWidth() * (toScale - tv.getCurveScale());
1475             anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex), TRANSLATION_X,
1476                     mIsRtl ? -displacementX : displacementX));
1477 
1478             int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - taskIndex);
1479             if (otherAdjacentTaskIndex >= 0 && otherAdjacentTaskIndex < getPageCount()) {
1480                 anim.play(new PropertyListBuilder()
1481                         .translationX(mIsRtl ? -displacementX : displacementX)
1482                         .scale(1)
1483                         .build(getPageAt(otherAdjacentTaskIndex)));
1484             }
1485         }
1486         return anim;
1487     }
1488 
createTaskLauncherAnimation(TaskView tv, long duration)1489     public PendingAnimation createTaskLauncherAnimation(TaskView tv, long duration) {
1490         if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) {
1491             throw new IllegalStateException("Another pending animation is still running");
1492         }
1493 
1494         int count = getChildCount();
1495         if (count == 0) {
1496             return new PendingAnimation(new AnimatorSet());
1497         }
1498 
1499         int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags();
1500         final boolean[] passedOverviewThreshold = new boolean[] {false};
1501         ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1);
1502         progressAnim.setInterpolator(LINEAR);
1503         progressAnim.addUpdateListener(animator -> {
1504             // Once we pass a certain threshold, update the sysui flags to match the target
1505             // tasks' flags
1506             mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW,
1507                     animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD
1508                             ? targetSysUiFlags
1509                             : 0);
1510 
1511             onTaskLaunchAnimationUpdate(animator.getAnimatedFraction(), tv);
1512 
1513             // Passing the threshold from taskview to fullscreen app will vibrate
1514             final boolean passed = animator.getAnimatedFraction() >=
1515                     SUCCESS_TRANSITION_PROGRESS;
1516             if (passed != passedOverviewThreshold[0]) {
1517                 passedOverviewThreshold[0] = passed;
1518                 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
1519                         HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
1520             }
1521         });
1522 
1523         ClipAnimationHelper clipAnimationHelper = new ClipAnimationHelper(mActivity);
1524         clipAnimationHelper.fromTaskThumbnailView(tv.getThumbnail(), this);
1525         clipAnimationHelper.prepareAnimation(mActivity.getDeviceProfile(), true /* isOpening */);
1526         AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv, clipAnimationHelper);
1527         anim.play(progressAnim);
1528         anim.setDuration(duration);
1529 
1530         Consumer<Boolean> onTaskLaunchFinish = this::onTaskLaunched;
1531 
1532         mPendingAnimation = new PendingAnimation(anim);
1533         mPendingAnimation.addEndListener((onEndListener) -> {
1534             if (onEndListener.isSuccess) {
1535                 Consumer<Boolean> onLaunchResult = (result) -> {
1536                     onTaskLaunchFinish.accept(result);
1537                     if (!result) {
1538                         tv.notifyTaskLaunchFailed(TAG);
1539                     }
1540                 };
1541                 tv.launchTask(false, onLaunchResult, getHandler());
1542                 Task task = tv.getTask();
1543                 if (task != null) {
1544                     mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
1545                             onEndListener.logAction, Direction.DOWN, indexOfChild(tv),
1546                             TaskUtils.getLaunchComponentKeyForTask(task.key));
1547                 }
1548             } else {
1549                 onTaskLaunchFinish.accept(false);
1550             }
1551             mPendingAnimation = null;
1552         });
1553         return mPendingAnimation;
1554     }
1555 
onTaskLaunchAnimationUpdate(float progress, TaskView tv)1556     protected void onTaskLaunchAnimationUpdate(float progress, TaskView tv) {
1557     }
1558 
shouldUseMultiWindowTaskSizeStrategy()1559     public abstract boolean shouldUseMultiWindowTaskSizeStrategy();
1560 
onTaskLaunched(boolean success)1561     protected void onTaskLaunched(boolean success) {
1562         if (success) {
1563             resetTaskVisuals();
1564         }
1565     }
1566 
1567     @Override
notifyPageSwitchListener(int prevPage)1568     protected void notifyPageSwitchListener(int prevPage) {
1569         super.notifyPageSwitchListener(prevPage);
1570         loadVisibleTaskData();
1571         updateEnabledOverlays();
1572     }
1573 
1574     @Override
getCurrentPageDescription()1575     protected String getCurrentPageDescription() {
1576         return "";
1577     }
1578 
1579     @Override
addChildrenForAccessibility(ArrayList<View> outChildren)1580     public void addChildrenForAccessibility(ArrayList<View> outChildren) {
1581         // Add children in reverse order
1582         for (int i = getChildCount() - 1; i >= 0; --i) {
1583             outChildren.add(getChildAt(i));
1584         }
1585     }
1586 
1587     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)1588     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1589         super.onInitializeAccessibilityNodeInfo(info);
1590         final AccessibilityNodeInfo.CollectionInfo
1591                 collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain(
1592                 1, getTaskViewCount(), false,
1593                 AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE);
1594         info.setCollectionInfo(collectionInfo);
1595     }
1596 
1597     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)1598     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1599         super.onInitializeAccessibilityEvent(event);
1600 
1601         final int taskViewCount = getTaskViewCount();
1602         event.setScrollable(taskViewCount > 0);
1603 
1604         if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
1605             final int[] visibleTasks = getVisibleChildrenRange();
1606             event.setFromIndex(taskViewCount - visibleTasks[1] - 1);
1607             event.setToIndex(taskViewCount - visibleTasks[0] - 1);
1608             event.setItemCount(taskViewCount);
1609         }
1610     }
1611 
1612     @Override
getAccessibilityClassName()1613     public CharSequence getAccessibilityClassName() {
1614         // To hear position-in-list related feedback from Talkback.
1615         return ListView.class.getName();
1616     }
1617 
1618     @Override
isPageOrderFlipped()1619     protected boolean isPageOrderFlipped() {
1620         return true;
1621     }
1622 
setEnableDrawingLiveTile(boolean enableDrawingLiveTile)1623     public void setEnableDrawingLiveTile(boolean enableDrawingLiveTile) {
1624         mEnableDrawingLiveTile = enableDrawingLiveTile;
1625     }
1626 
redrawLiveTile(boolean mightNeedToRefill)1627     public void redrawLiveTile(boolean mightNeedToRefill) { }
1628 
setRecentsAnimationWrapper(RecentsAnimationWrapper recentsAnimationWrapper)1629     public void setRecentsAnimationWrapper(RecentsAnimationWrapper recentsAnimationWrapper) {
1630         mRecentsAnimationWrapper = recentsAnimationWrapper;
1631     }
1632 
setClipAnimationHelper(ClipAnimationHelper clipAnimationHelper)1633     public void setClipAnimationHelper(ClipAnimationHelper clipAnimationHelper) {
1634         mClipAnimationHelper = clipAnimationHelper;
1635     }
1636 
setLiveTileOverlay(LiveTileOverlay liveTileOverlay)1637     public void setLiveTileOverlay(LiveTileOverlay liveTileOverlay) {
1638         mLiveTileOverlay = liveTileOverlay;
1639     }
1640 
updateLiveTileIcon(Drawable icon)1641     public void updateLiveTileIcon(Drawable icon) {
1642         if (mLiveTileOverlay != null) {
1643             mLiveTileOverlay.setIcon(icon);
1644         }
1645     }
1646 
finishRecentsAnimation(boolean toRecents, Runnable onFinishComplete)1647     public void finishRecentsAnimation(boolean toRecents, Runnable onFinishComplete) {
1648         if (mRecentsAnimationWrapper == null) {
1649             if (onFinishComplete != null) {
1650                 onFinishComplete.run();
1651             }
1652             return;
1653         }
1654 
1655         mRecentsAnimationWrapper.finish(toRecents, onFinishComplete);
1656     }
1657 
setDisallowScrollToClearAll(boolean disallowScrollToClearAll)1658     public void setDisallowScrollToClearAll(boolean disallowScrollToClearAll) {
1659         if (mDisallowScrollToClearAll != disallowScrollToClearAll) {
1660             mDisallowScrollToClearAll = disallowScrollToClearAll;
1661             updateMinAndMaxScrollX();
1662         }
1663     }
1664 
1665     @Override
computeMinScrollX()1666     protected int computeMinScrollX() {
1667         if (mIsRtl && mDisallowScrollToClearAll) {
1668             // We aren't showing the clear all button, so use the leftmost task as the min scroll.
1669             return getScrollForPage(getTaskViewCount() - 1);
1670         }
1671         return super.computeMinScrollX();
1672     }
1673 
1674     @Override
computeMaxScrollX()1675     protected int computeMaxScrollX() {
1676         if (!mIsRtl && mDisallowScrollToClearAll) {
1677             // We aren't showing the clear all button, so use the rightmost task as the max scroll.
1678             return getScrollForPage(getTaskViewCount() - 1);
1679         }
1680         return super.computeMaxScrollX();
1681     }
1682 
getClearAllButton()1683     public ClearAllButton getClearAllButton() {
1684         return mClearAllButton;
1685     }
1686 
1687     /**
1688      * @return How many pixels the running task is offset on the x-axis due to the current scrollX.
1689      */
getScrollOffset()1690     public float getScrollOffset() {
1691         int startScroll = getScrollForPage(getRunningTaskIndex());
1692         int offsetX = startScroll - getScrollX();
1693         offsetX *= getScaleX();
1694         return offsetX;
1695     }
1696 
getEventDispatcher(RotationMode rotationMode)1697     public Consumer<MotionEvent> getEventDispatcher(RotationMode rotationMode) {
1698         if (rotationMode.isTransposed) {
1699             Matrix transform = new Matrix();
1700             transform.setRotate(-rotationMode.surfaceRotation);
1701 
1702             if (getWidth() > 0 && getHeight() > 0) {
1703                 float scale = ((float) getWidth()) / getHeight();
1704                 transform.postScale(scale, 1 / scale);
1705             }
1706 
1707             Matrix inverse = new Matrix();
1708             transform.invert(inverse);
1709             return e -> {
1710                 e.transform(transform);
1711                 super.onTouchEvent(e);
1712                 e.transform(inverse);
1713             };
1714         } else {
1715             return super::onTouchEvent;
1716         }
1717     }
1718 
getTempClipAnimationHelper()1719     public ClipAnimationHelper getTempClipAnimationHelper() {
1720         return mTempClipAnimationHelper;
1721     }
1722 
updateEnabledOverlays()1723     private void updateEnabledOverlays() {
1724         int overlayEnabledPage = mOverlayEnabled ? getNextPage() : -1;
1725         int taskCount = getTaskViewCount();
1726         for (int i = 0; i < taskCount; i++) {
1727             ((TaskView) getChildAt(i)).setOverlayEnabled(i == overlayEnabledPage);
1728         }
1729     }
1730 
setOverlayEnabled(boolean overlayEnabled)1731     public void setOverlayEnabled(boolean overlayEnabled) {
1732         if (mOverlayEnabled != overlayEnabled) {
1733             mOverlayEnabled = overlayEnabled;
1734             updateEnabledOverlays();
1735         }
1736     }
1737 }
1738