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 android.view.Surface.ROTATION_0;
20 import static android.view.View.MeasureSpec.EXACTLY;
21 import static android.view.View.MeasureSpec.makeMeasureSpec;
22 
23 import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU;
24 import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
25 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
26 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
27 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
28 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
29 import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION;
30 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
31 import static com.android.launcher3.Utilities.mapToRange;
32 import static com.android.launcher3.Utilities.squaredHypot;
33 import static com.android.launcher3.Utilities.squaredTouchSlop;
34 import static com.android.launcher3.anim.Interpolators.ACCEL;
35 import static com.android.launcher3.anim.Interpolators.ACCEL_0_75;
36 import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
37 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
38 import static com.android.launcher3.anim.Interpolators.FINAL_FRAME;
39 import static com.android.launcher3.anim.Interpolators.LINEAR;
40 import static com.android.launcher3.anim.Interpolators.clampToProgress;
41 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
42 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_CLEAR_ALL;
43 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP;
44 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN;
45 import static com.android.launcher3.statehandlers.DepthController.DEPTH;
46 import static com.android.launcher3.testing.TestProtocol.TASK_VIEW_ID_CRASH;
47 import static com.android.launcher3.touch.PagedOrientationHandler.CANVAS_TRANSLATE;
48 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
49 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
50 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
51 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
52 import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
53 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
54 import static com.android.quickstep.views.ClearAllButton.DISMISS_ALPHA;
55 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NON_ZERO_ROTATION;
56 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_RECENTS;
57 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_TASKS;
58 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_SPLIT_SCREEN;
59 
60 import android.animation.Animator;
61 import android.animation.AnimatorListenerAdapter;
62 import android.animation.AnimatorSet;
63 import android.animation.LayoutTransition;
64 import android.animation.LayoutTransition.TransitionListener;
65 import android.animation.ObjectAnimator;
66 import android.animation.PropertyValuesHolder;
67 import android.animation.ValueAnimator;
68 import android.annotation.TargetApi;
69 import android.app.ActivityManager.RunningTaskInfo;
70 import android.content.Context;
71 import android.content.LocusId;
72 import android.content.res.Configuration;
73 import android.graphics.BlendMode;
74 import android.graphics.Canvas;
75 import android.graphics.Color;
76 import android.graphics.Matrix;
77 import android.graphics.Point;
78 import android.graphics.PointF;
79 import android.graphics.Rect;
80 import android.graphics.RectF;
81 import android.graphics.Typeface;
82 import android.graphics.drawable.Drawable;
83 import android.os.Build;
84 import android.os.Bundle;
85 import android.os.SystemClock;
86 import android.os.UserHandle;
87 import android.os.VibrationEffect;
88 import android.text.Layout;
89 import android.text.StaticLayout;
90 import android.text.TextPaint;
91 import android.util.AttributeSet;
92 import android.util.FloatProperty;
93 import android.util.Log;
94 import android.util.Pair;
95 import android.util.SparseBooleanArray;
96 import android.view.HapticFeedbackConstants;
97 import android.view.KeyEvent;
98 import android.view.LayoutInflater;
99 import android.view.MotionEvent;
100 import android.view.View;
101 import android.view.ViewDebug;
102 import android.view.ViewGroup;
103 import android.view.ViewTreeObserver.OnScrollChangedListener;
104 import android.view.accessibility.AccessibilityEvent;
105 import android.view.accessibility.AccessibilityNodeInfo;
106 import android.view.animation.Interpolator;
107 import android.widget.ListView;
108 import android.widget.OverScroller;
109 import android.widget.Toast;
110 
111 import androidx.annotation.NonNull;
112 import androidx.annotation.Nullable;
113 import androidx.annotation.UiThread;
114 import androidx.core.graphics.ColorUtils;
115 
116 import com.android.launcher3.BaseActivity;
117 import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener;
118 import com.android.launcher3.DeviceProfile;
119 import com.android.launcher3.Insettable;
120 import com.android.launcher3.InvariantDeviceProfile;
121 import com.android.launcher3.PagedView;
122 import com.android.launcher3.R;
123 import com.android.launcher3.Utilities;
124 import com.android.launcher3.anim.AnimatorListeners;
125 import com.android.launcher3.anim.AnimatorPlaybackController;
126 import com.android.launcher3.anim.PendingAnimation;
127 import com.android.launcher3.anim.SpringProperty;
128 import com.android.launcher3.compat.AccessibilityManagerCompat;
129 import com.android.launcher3.config.FeatureFlags;
130 import com.android.launcher3.icons.cache.HandlerRunnable;
131 import com.android.launcher3.statehandlers.DepthController;
132 import com.android.launcher3.statemanager.BaseState;
133 import com.android.launcher3.statemanager.StatefulActivity;
134 import com.android.launcher3.touch.OverScroll;
135 import com.android.launcher3.touch.PagedOrientationHandler;
136 import com.android.launcher3.util.DynamicResource;
137 import com.android.launcher3.util.IntArray;
138 import com.android.launcher3.util.IntSet;
139 import com.android.launcher3.util.MultiValueAlpha;
140 import com.android.launcher3.util.ResourceBasedOverride.Overrides;
141 import com.android.launcher3.util.RunnableList;
142 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
143 import com.android.launcher3.util.SplitConfigurationOptions.StagedSplitBounds;
144 import com.android.launcher3.util.Themes;
145 import com.android.launcher3.util.TranslateEdgeEffect;
146 import com.android.launcher3.util.ViewPool;
147 import com.android.quickstep.AnimatedFloat;
148 import com.android.quickstep.BaseActivityInterface;
149 import com.android.quickstep.GestureState;
150 import com.android.quickstep.RecentsAnimationController;
151 import com.android.quickstep.RecentsAnimationTargets;
152 import com.android.quickstep.RecentsModel;
153 import com.android.quickstep.RecentsModel.TaskVisualsChangeListener;
154 import com.android.quickstep.RemoteAnimationTargets;
155 import com.android.quickstep.RemoteTargetGluer;
156 import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle;
157 import com.android.quickstep.SystemUiProxy;
158 import com.android.quickstep.TaskOverlayFactory;
159 import com.android.quickstep.TaskThumbnailCache;
160 import com.android.quickstep.TaskViewUtils;
161 import com.android.quickstep.ViewUtils;
162 import com.android.quickstep.util.GroupTask;
163 import com.android.quickstep.util.LayoutUtils;
164 import com.android.quickstep.util.RecentsOrientedState;
165 import com.android.quickstep.util.SplitScreenBounds;
166 import com.android.quickstep.util.SplitSelectStateController;
167 import com.android.quickstep.util.SurfaceTransactionApplier;
168 import com.android.quickstep.util.TaskViewSimulator;
169 import com.android.quickstep.util.TransformParams;
170 import com.android.quickstep.util.VibratorWrapper;
171 import com.android.systemui.plugins.ResourceProvider;
172 import com.android.systemui.shared.recents.model.Task;
173 import com.android.systemui.shared.recents.model.Task.TaskKey;
174 import com.android.systemui.shared.recents.model.ThumbnailData;
175 import com.android.systemui.shared.system.ActivityManagerWrapper;
176 import com.android.systemui.shared.system.PackageManagerWrapper;
177 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
178 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
179 import com.android.systemui.shared.system.TaskStackChangeListener;
180 import com.android.systemui.shared.system.TaskStackChangeListeners;
181 import com.android.wm.shell.pip.IPipAnimationListener;
182 
183 import java.util.ArrayList;
184 import java.util.HashMap;
185 import java.util.List;
186 import java.util.Objects;
187 import java.util.function.Consumer;
188 
189 /**
190  * A list of recent tasks.
191  */
192 @TargetApi(Build.VERSION_CODES.R)
193 public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_TYPE>,
194         STATE_TYPE extends BaseState<STATE_TYPE>> extends PagedView implements Insettable,
195         TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
196         TaskVisualsChangeListener, SplitScreenBounds.OnChangeListener {
197 
198     private static final String TAG = "RecentsView";
199     private static final boolean DEBUG = false;
200 
201     // TODO(b/184899234): We use this timeout to wait a fixed period after switching to the
202     // screenshot when dismissing the current live task to ensure the app can try and get stopped.
203     private static final int REMOVE_TASK_WAIT_FOR_APP_STOP_MS = 100;
204 
205     public static final FloatProperty<RecentsView> CONTENT_ALPHA =
206             new FloatProperty<RecentsView>("contentAlpha") {
207                 @Override
208                 public void setValue(RecentsView view, float v) {
209                     view.setContentAlpha(v);
210                 }
211 
212                 @Override
213                 public Float get(RecentsView view) {
214                     return view.getContentAlpha();
215                 }
216             };
217 
218     public static final FloatProperty<RecentsView> FULLSCREEN_PROGRESS =
219             new FloatProperty<RecentsView>("fullscreenProgress") {
220                 @Override
221                 public void setValue(RecentsView recentsView, float v) {
222                     recentsView.setFullscreenProgress(v);
223                 }
224 
225                 @Override
226                 public Float get(RecentsView recentsView) {
227                     return recentsView.mFullscreenProgress;
228                 }
229             };
230 
231     public static final FloatProperty<RecentsView> TASK_MODALNESS =
232             new FloatProperty<RecentsView>("taskModalness") {
233                 @Override
234                 public void setValue(RecentsView recentsView, float v) {
235                     recentsView.setTaskModalness(v);
236                 }
237 
238                 @Override
239                 public Float get(RecentsView recentsView) {
240                     return recentsView.mTaskModalness;
241                 }
242             };
243 
244     public static final FloatProperty<RecentsView> ADJACENT_PAGE_HORIZONTAL_OFFSET =
245             new FloatProperty<RecentsView>("adjacentPageHorizontalOffset") {
246                 @Override
247                 public void setValue(RecentsView recentsView, float v) {
248                     if (recentsView.mAdjacentPageHorizontalOffset != v) {
249                         recentsView.mAdjacentPageHorizontalOffset = v;
250                         recentsView.updatePageOffsets();
251                     }
252                 }
253 
254                 @Override
255                 public Float get(RecentsView recentsView) {
256                     return recentsView.mAdjacentPageHorizontalOffset;
257                 }
258             };
259 
260     public static final int SCROLL_VIBRATION_PRIMITIVE =
261             Utilities.ATLEAST_S ? VibrationEffect.Composition.PRIMITIVE_LOW_TICK : -1;
262     public static final float SCROLL_VIBRATION_PRIMITIVE_SCALE = 0.6f;
263     public static final VibrationEffect SCROLL_VIBRATION_FALLBACK =
264             VibratorWrapper.EFFECT_TEXTURE_TICK;
265 
266     /**
267      * Can be used to tint the color of the RecentsView to simulate a scrim that can views
268      * excluded from. Really should be a proper scrim.
269      * TODO(b/187528071): Remove this and replace with a real scrim.
270      */
271     private static final FloatProperty<RecentsView> COLOR_TINT =
272             new FloatProperty<RecentsView>("colorTint") {
273                 @Override
274                 public void setValue(RecentsView recentsView, float v) {
275                     recentsView.setColorTint(v);
276                 }
277 
278                 @Override
279                 public Float get(RecentsView recentsView) {
280                     return recentsView.getColorTint();
281                 }
282             };
283 
284     /**
285      * Even though {@link TaskView} has distinct offsetTranslationX/Y and resistance property, they
286      * are currently both used to apply secondary translation. Should their use cases change to be
287      * more specific, we'd want to create a similar FloatProperty just for a TaskView's
288      * offsetX/Y property
289      */
290     public static final FloatProperty<RecentsView> TASK_SECONDARY_TRANSLATION =
291             new FloatProperty<RecentsView>("taskSecondaryTranslation") {
292                 @Override
293                 public void setValue(RecentsView recentsView, float v) {
294                     recentsView.setTaskViewsResistanceTranslation(v);
295                 }
296 
297                 @Override
298                 public Float get(RecentsView recentsView) {
299                     return recentsView.mTaskViewsSecondaryTranslation;
300                 }
301             };
302 
303     /**
304      * Even though {@link TaskView} has distinct offsetTranslationX/Y and resistance property, they
305      * are currently both used to apply secondary translation. Should their use cases change to be
306      * more specific, we'd want to create a similar FloatProperty just for a TaskView's
307      * offsetX/Y property
308      */
309     public static final FloatProperty<RecentsView> TASK_PRIMARY_SPLIT_TRANSLATION =
310             new FloatProperty<RecentsView>("taskPrimarySplitTranslation") {
311                 @Override
312                 public void setValue(RecentsView recentsView, float v) {
313                     recentsView.setTaskViewsPrimarySplitTranslation(v);
314                 }
315 
316                 @Override
317                 public Float get(RecentsView recentsView) {
318                     return recentsView.mTaskViewsPrimarySplitTranslation;
319                 }
320             };
321 
322     public static final FloatProperty<RecentsView> TASK_SECONDARY_SPLIT_TRANSLATION =
323             new FloatProperty<RecentsView>("taskSecondarySplitTranslation") {
324                 @Override
325                 public void setValue(RecentsView recentsView, float v) {
326                     recentsView.setTaskViewsSecondarySplitTranslation(v);
327                 }
328 
329                 @Override
330                 public Float get(RecentsView recentsView) {
331                     return recentsView.mTaskViewsSecondarySplitTranslation;
332                 }
333             };
334 
335     /** Same as normal SCALE_PROPERTY, but also updates page offsets that depend on this scale. */
336     public static final FloatProperty<RecentsView> RECENTS_SCALE_PROPERTY =
337             new FloatProperty<RecentsView>("recentsScale") {
338                 @Override
339                 public void setValue(RecentsView view, float scale) {
340                     view.setScaleX(scale);
341                     view.setScaleY(scale);
342                     view.mLastComputedTaskStartPushOutDistance = null;
343                     view.mLastComputedTaskEndPushOutDistance = null;
344                     view.runActionOnRemoteHandles(new Consumer<RemoteTargetHandle>() {
345                         @Override
346                         public void accept(RemoteTargetHandle remoteTargetHandle) {
347                             remoteTargetHandle.getTaskViewSimulator().recentsViewScale.value =
348                                     scale;
349                         }
350                     });
351                     view.setTaskViewsResistanceTranslation(view.mTaskViewsSecondaryTranslation);
352                     view.updateTaskViewsSnapshotRadius();
353                     view.updatePageOffsets();
354                 }
355 
356                 @Override
357                 public Float get(RecentsView view) {
358                     return view.getScaleX();
359                 }
360             };
361 
362     public static final FloatProperty<RecentsView> RECENTS_GRID_PROGRESS =
363             new FloatProperty<RecentsView>("recentsGrid") {
364                 @Override
365                 public void setValue(RecentsView view, float gridProgress) {
366                     view.setGridProgress(gridProgress);
367                 }
368 
369                 @Override
370                 public Float get(RecentsView view) {
371                     return view.mGridProgress;
372                 }
373             };
374 
375     // OverScroll constants
376     private static final int OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION = 270;
377 
378     private static final int DISMISS_TASK_DURATION = 300;
379     private static final int ADDITION_TASK_DURATION = 200;
380     private static final float INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET = 0.55f;
381     private static final float ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET = 0.05f;
382     private static final float ANIMATION_DISMISS_PROGRESS_MIDPOINT = 0.5f;
383     private static final float END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET = 0.75f;
384 
385     private static final float SIGNIFICANT_MOVE_THRESHOLD_TABLET = 0.15f;
386 
387     protected final RecentsOrientedState mOrientationState;
388     protected final BaseActivityInterface<STATE_TYPE, ACTIVITY_TYPE> mSizeStrategy;
389     @Nullable
390     protected RecentsAnimationController mRecentsAnimationController;
391     @Nullable
392     protected SurfaceTransactionApplier mSyncTransactionApplier;
393     protected int mTaskWidth;
394     protected int mTaskHeight;
395     // Used to position the top of a task in the top row of the grid
396     private float mTaskGridVerticalDiff;
397     // The vertical space one grid task takes + space between top and bottom row.
398     private float mTopBottomRowHeightDiff;
399     // mTaskGridVerticalDiff and mTopBottomRowHeightDiff summed together provides the top
400     // position for bottom row of grid tasks.
401 
402     @Nullable
403     protected RemoteTargetHandle[] mRemoteTargetHandles;
404     protected final Rect mLastComputedTaskSize = new Rect();
405     protected final Rect mLastComputedGridSize = new Rect();
406     protected final Rect mLastComputedGridTaskSize = new Rect();
407     // How much a task that is directly offscreen will be pushed out due to RecentsView scale/pivot.
408     @Nullable
409     protected Float mLastComputedTaskStartPushOutDistance = null;
410     @Nullable
411     protected Float mLastComputedTaskEndPushOutDistance = null;
412     protected boolean mEnableDrawingLiveTile = false;
413     protected final Rect mTempRect = new Rect();
414     protected final RectF mTempRectF = new RectF();
415     private final PointF mTempPointF = new PointF();
416     private final Matrix mTempMatrix = new Matrix();
417     private final float[] mTempFloat = new float[1];
418     private final List<OnScrollChangedListener> mScrollListeners = new ArrayList<>();
419 
420     // The threshold at which we update the SystemUI flags when animating from the task into the app
421     public static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.85f;
422 
423     protected final ACTIVITY_TYPE mActivity;
424     private final float mFastFlingVelocity;
425     private final int mScrollHapticMinGapMillis;
426     private final RecentsModel mModel;
427     private final int mSplitPlaceholderSize;
428     private final ClearAllButton mClearAllButton;
429     private final Rect mClearAllButtonDeadZoneRect = new Rect();
430     private final Rect mTaskViewDeadZoneRect = new Rect();
431     /**
432      * Reflects if Recents is currently in the middle of a gesture
433      */
434     private boolean mGestureActive;
435 
436     // Keeps track of the previously known visible tasks for purposes of loading/unloading task data
437     private final SparseBooleanArray mHasVisibleTaskData = new SparseBooleanArray();
438 
439     private final InvariantDeviceProfile mIdp;
440 
441     /**
442      * Getting views should be done via {@link #getTaskViewFromPool(boolean)}
443      */
444     private final ViewPool<TaskView> mTaskViewPool;
445     private final ViewPool<GroupedTaskView> mGroupedTaskViewPool;
446 
447     private final TaskOverlayFactory mTaskOverlayFactory;
448 
449     protected boolean mDisallowScrollToClearAll;
450     private boolean mOverlayEnabled;
451     protected boolean mFreezeViewVisibility;
452     private boolean mOverviewGridEnabled;
453     private boolean mOverviewFullscreenEnabled;
454 
455     private float mAdjacentPageHorizontalOffset = 0;
456     protected float mTaskViewsSecondaryTranslation = 0;
457     protected float mTaskViewsPrimarySplitTranslation = 0;
458     protected float mTaskViewsSecondarySplitTranslation = 0;
459     // Progress from 0 to 1 where 0 is a carousel and 1 is a 2 row grid.
460     private float mGridProgress = 0;
461     private boolean mShowAsGridLastOnLayout = false;
462     private final IntSet mTopRowIdSet = new IntSet();
463 
464     // The GestureEndTarget that is still in progress.
465     @Nullable
466     protected GestureState.GestureEndTarget mCurrentGestureEndTarget;
467 
468     // TODO(b/187528071): Remove these and replace with a real scrim.
469     private float mColorTint;
470     private final int mTintingColor;
471     @Nullable
472     private ObjectAnimator mTintingAnimator;
473 
474     private int mOverScrollShift = 0;
475     private long mScrollLastHapticTimestamp;
476 
477     /**
478      * TODO: Call reloadIdNeeded in onTaskStackChanged.
479      */
480     private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
481         @Override
482         public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
483             if (!mHandleTaskStackChanges) {
484                 return;
485             }
486             // Check this is for the right user
487             if (!checkCurrentOrManagedUserId(userId, getContext())) {
488                 return;
489             }
490 
491             // Remove the task immediately from the task list
492             TaskView taskView = getTaskViewByTaskId(taskId);
493             if (taskView != null) {
494                 removeView(taskView);
495             }
496         }
497 
498         @Override
499         public void onActivityUnpinned() {
500             if (!mHandleTaskStackChanges) {
501                 return;
502             }
503 
504             reloadIfNeeded();
505             enableLayoutTransitions();
506         }
507 
508         @Override
509         public void onTaskRemoved(int taskId) {
510             if (!mHandleTaskStackChanges) {
511                 return;
512             }
513 
514             TaskView taskView = getTaskViewByTaskId(taskId);
515             if (taskView == null) {
516                 return;
517             }
518             Task.TaskKey taskKey = taskView.getTask().key;
519             UI_HELPER_EXECUTOR.execute(new HandlerRunnable<>(
520                     UI_HELPER_EXECUTOR.getHandler(),
521                     () -> PackageManagerWrapper.getInstance()
522                             .getActivityInfo(taskKey.getComponent(), taskKey.userId) == null,
523                     MAIN_EXECUTOR,
524                     apkRemoved -> {
525                         if (apkRemoved) {
526                             dismissTask(taskId);
527                         } else {
528                             mModel.isTaskRemoved(taskKey.id, taskRemoved -> {
529                                 if (taskRemoved) {
530                                     dismissTask(taskId);
531                                 }
532                             });
533                         }
534                     }));
535         }
536     };
537 
538     private final PinnedStackAnimationListener mIPipAnimationListener =
539             new PinnedStackAnimationListener();
540     private int mPipCornerRadius;
541 
542     // Used to keep track of the last requested task list id, so that we do not request to load the
543     // tasks again if we have already requested it and the task list has not changed
544     private int mTaskListChangeId = -1;
545 
546     // Only valid until the launcher state changes to NORMAL
547     /**
548      * ID for the current running TaskView view, unique amongst TaskView instances. ID's are set
549      * through {@link #getTaskViewFromPool(boolean)} and incremented by {@link #mTaskViewIdCount}
550      */
551     protected int mRunningTaskViewId = -1;
552     private int mTaskViewIdCount;
553     private final int[] INVALID_TASK_IDS = new int[]{-1, -1};
554     protected boolean mRunningTaskTileHidden;
555     @Nullable
556     private Task[] mTmpRunningTasks;
557     protected int mFocusedTaskViewId = -1;
558 
559     private boolean mTaskIconScaledDown = false;
560     private boolean mRunningTaskShowScreenshot = false;
561 
562     private boolean mOverviewStateEnabled;
563     private boolean mHandleTaskStackChanges;
564     private boolean mSwipeDownShouldLaunchApp;
565     private boolean mTouchDownToStartHome;
566     private final float mSquaredTouchSlop;
567     private int mDownX;
568     private int mDownY;
569 
570     @Nullable
571     private PendingAnimation mPendingAnimation;
572     @Nullable
573     private LayoutTransition mLayoutTransition;
574 
575     @ViewDebug.ExportedProperty(category = "launcher")
576     protected float mContentAlpha = 1;
577     @ViewDebug.ExportedProperty(category = "launcher")
578     protected float mFullscreenProgress = 0;
579     /**
580      * How modal is the current task to be displayed, 1 means the task is fully modal and no other
581      * tasks are show. 0 means the task is displays in context in the list with other tasks.
582      */
583     @ViewDebug.ExportedProperty(category = "launcher")
584     protected float mTaskModalness = 0;
585 
586     // Keeps track of task id whose visual state should not be reset
587     private int mIgnoreResetTaskId = -1;
588     protected boolean mLoadPlanEverApplied;
589 
590     // Variables for empty state
591     private final Drawable mEmptyIcon;
592     private final CharSequence mEmptyMessage;
593     private final TextPaint mEmptyMessagePaint;
594     private final Point mLastMeasureSize = new Point();
595     private final int mEmptyMessagePadding;
596     private boolean mShowEmptyMessage;
597     @Nullable
598     private OnEmptyMessageUpdatedListener mOnEmptyMessageUpdatedListener;
599     @Nullable
600     private Layout mEmptyTextLayout;
601 
602     /**
603      * Placeholder view indicating where the first split screen selected app will be placed
604      */
605     private SplitSelectStateController mSplitSelectStateController;
606     /**
607      * The first task that split screen selection was initiated with. When split select state is
608      * initialized, we create a
609      * {@link #createTaskDismissAnimation(TaskView, boolean, boolean, long, boolean)} for this
610      * TaskView but don't actually remove the task since the user might back out. As such, we also
611      * ensure this View doesn't go back into the {@link #mTaskViewPool},
612      * see {@link #onViewRemoved(View)}
613      */
614     @Nullable
615     private TaskView mSplitHiddenTaskView;
616     @Nullable
617     private TaskView mSecondSplitHiddenTaskView;
618     @Nullable
619     private StagedSplitBounds mSplitBoundsConfig;
620     private final Toast mSplitToast = Toast.makeText(getContext(),
621             R.string.toast_split_select_app, Toast.LENGTH_SHORT);
622     private final Toast mSplitUnsupportedToast = Toast.makeText(getContext(),
623             R.string.toast_split_app_unsupported, Toast.LENGTH_SHORT);
624 
625     /**
626      * Keeps track of the index of the TaskView that split screen was initialized with so we know
627      * where to insert it back into list of taskViews in case user backs out of entering split
628      * screen.
629      * NOTE: This index is the index while {@link #mSplitHiddenTaskView} was a child of recentsView,
630      * this doesn't get adjusted to reflect the new child count after the taskView is dismissed/
631      * removed from recentsView
632      */
633     private int mSplitHiddenTaskViewIndex = -1;
634     @Nullable
635     private FloatingTaskView mFirstFloatingTaskView;
636     @Nullable
637     private FloatingTaskView mSecondFloatingTaskView;
638 
639     /**
640      * The task to be removed and immediately re-added. Should not be added to task pool.
641      */
642     @Nullable
643     private TaskView mMovingTaskView;
644 
645     private OverviewActionsView mActionsView;
646 
647     private MultiWindowModeChangedListener mMultiWindowModeChangedListener =
648             new MultiWindowModeChangedListener() {
649                 @Override
650                 public void onMultiWindowModeChanged(boolean inMultiWindowMode) {
651                     mOrientationState.setMultiWindowMode(inMultiWindowMode);
652                     setLayoutRotation(mOrientationState.getTouchRotation(),
653                             mOrientationState.getDisplayRotation());
654                     updateChildTaskOrientations();
655                     if (!inMultiWindowMode && mOverviewStateEnabled) {
656                         // TODO: Re-enable layout transitions for addition of the unpinned task
657                         reloadIfNeeded();
658                     }
659                 }
660             };
661 
662     @Nullable
663     private RunnableList mSideTaskLaunchCallback;
664     @Nullable
665     private TaskLaunchListener mTaskLaunchListener;
666 
RecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, BaseActivityInterface sizeStrategy)667     public RecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
668             BaseActivityInterface sizeStrategy) {
669         super(context, attrs, defStyleAttr);
670         setEnableFreeScroll(true);
671         mSizeStrategy = sizeStrategy;
672         mActivity = BaseActivity.fromContext(context);
673         mOrientationState = new RecentsOrientedState(
674                 context, mSizeStrategy, this::animateRecentsRotationInPlace);
675         final int rotation = mActivity.getDisplay().getRotation();
676         mOrientationState.setRecentsRotation(rotation);
677 
678         mScrollHapticMinGapMillis = getResources()
679                 .getInteger(R.integer.recentsScrollHapticMinGapMillis);
680         mFastFlingVelocity = getResources()
681                 .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
682         mModel = RecentsModel.INSTANCE.get(context);
683         mIdp = InvariantDeviceProfile.INSTANCE.get(context);
684 
685         mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
686                 .inflate(R.layout.overview_clear_all_button, this, false);
687         mClearAllButton.setOnClickListener(this::dismissAllTasks);
688         mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */,
689                 10 /* initial size */);
690         mGroupedTaskViewPool = new ViewPool<>(context, this,
691                 R.layout.task_grouped, 20 /* max size */, 10 /* initial size */);
692 
693         mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
694         setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
695         mSplitPlaceholderSize = getResources().getDimensionPixelSize(
696                 R.dimen.split_placeholder_size);
697         mSquaredTouchSlop = squaredTouchSlop(context);
698 
699         mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents);
700         mEmptyIcon.setCallback(this);
701         mEmptyMessage = context.getText(R.string.recents_empty_message);
702         mEmptyMessagePaint = new TextPaint();
703         mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary));
704         mEmptyMessagePaint.setTextSize(getResources()
705                 .getDimension(R.dimen.recents_empty_message_text_size));
706         mEmptyMessagePaint.setTypeface(Typeface.create(Themes.getDefaultBodyFont(context),
707                 Typeface.NORMAL));
708         mEmptyMessagePaint.setAntiAlias(true);
709         mEmptyMessagePadding = getResources()
710                 .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
711         setWillNotDraw(false);
712         updateEmptyMessage();
713         mOrientationHandler = mOrientationState.getOrientationHandler();
714 
715         mTaskOverlayFactory = Overrides.getObject(
716                 TaskOverlayFactory.class,
717                 context.getApplicationContext(),
718                 R.string.task_overlay_factory_class);
719 
720         // Initialize quickstep specific cache params here, as this is constructed only once
721         mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5);
722 
723         mTintingColor = getForegroundScrimDimColor(context);
724     }
725 
getScroller()726     public OverScroller getScroller() {
727         return mScroller;
728     }
729 
isRtl()730     public boolean isRtl() {
731         return mIsRtl;
732     }
733 
734     @Override
initEdgeEffect()735     protected void initEdgeEffect() {
736         mEdgeGlowLeft = new TranslateEdgeEffect(getContext());
737         mEdgeGlowRight = new TranslateEdgeEffect(getContext());
738     }
739 
740     @Override
drawEdgeEffect(Canvas canvas)741     protected void drawEdgeEffect(Canvas canvas) {
742         // Do not draw edge effect
743     }
744 
745     @Override
dispatchDraw(Canvas canvas)746     protected void dispatchDraw(Canvas canvas) {
747         // Draw overscroll
748         if (mAllowOverScroll && (!mEdgeGlowRight.isFinished() || !mEdgeGlowLeft.isFinished())) {
749             final int restoreCount = canvas.save();
750 
751             int primarySize = mOrientationHandler.getPrimaryValue(getWidth(), getHeight());
752             int scroll = OverScroll.dampedScroll(getUndampedOverScrollShift(), primarySize);
753             mOrientationHandler.setPrimary(canvas, CANVAS_TRANSLATE, scroll);
754 
755             if (mOverScrollShift != scroll) {
756                 mOverScrollShift = scroll;
757                 dispatchScrollChanged();
758             }
759 
760             super.dispatchDraw(canvas);
761             canvas.restoreToCount(restoreCount);
762         } else {
763             if (mOverScrollShift != 0) {
764                 mOverScrollShift = 0;
765                 dispatchScrollChanged();
766             }
767             super.dispatchDraw(canvas);
768         }
769         if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
770                 && mRemoteTargetHandles != null) {
771             redrawLiveTile();
772         }
773     }
774 
getUndampedOverScrollShift()775     private float getUndampedOverScrollShift() {
776         final int width = getWidth();
777         final int height = getHeight();
778         int primarySize = mOrientationHandler.getPrimaryValue(width, height);
779         int secondarySize = mOrientationHandler.getSecondaryValue(width, height);
780 
781         float effectiveShift = 0;
782         if (!mEdgeGlowLeft.isFinished()) {
783             mEdgeGlowLeft.setSize(secondarySize, primarySize);
784             if (((TranslateEdgeEffect) mEdgeGlowLeft).getTranslationShift(mTempFloat)) {
785                 effectiveShift = mTempFloat[0];
786                 postInvalidateOnAnimation();
787             }
788         }
789         if (!mEdgeGlowRight.isFinished()) {
790             mEdgeGlowRight.setSize(secondarySize, primarySize);
791             if (((TranslateEdgeEffect) mEdgeGlowRight).getTranslationShift(mTempFloat)) {
792                 effectiveShift -= mTempFloat[0];
793                 postInvalidateOnAnimation();
794             }
795         }
796 
797         return effectiveShift * primarySize;
798     }
799 
800     /**
801      * Returns the view shift due to overscroll
802      */
getOverScrollShift()803     public int getOverScrollShift() {
804         return mOverScrollShift;
805     }
806 
807     @Override
808     @Nullable
onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData)809     public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
810         if (mHandleTaskStackChanges) {
811             TaskView taskView = getTaskViewByTaskId(taskId);
812             if (taskView != null) {
813                 for (TaskView.TaskIdAttributeContainer container :
814                         taskView.getTaskIdAttributeContainers()) {
815                     if (container == null || taskId != container.getTask().key.id) {
816                         continue;
817                     }
818                     container.getThumbnailView().setThumbnail(container.getTask(), thumbnailData);
819                 }
820             }
821         }
822         return null;
823     }
824 
825     @Override
onTaskIconChanged(String pkg, UserHandle user)826     public void onTaskIconChanged(String pkg, UserHandle user) {
827         for (int i = 0; i < getTaskViewCount(); i++) {
828             TaskView tv = requireTaskViewAt(i);
829             Task task = tv.getTask();
830             if (task != null && task.key != null && pkg.equals(task.key.getPackageName())
831                     && task.key.userId == user.getIdentifier()) {
832                 task.icon = null;
833                 if (tv.getIconView().getDrawable() != null) {
834                     tv.onTaskListVisibilityChanged(true /* visible */);
835                 }
836             }
837         }
838     }
839 
840     /**
841      * Update the thumbnail of the task.
842      * @param refreshNow Refresh immediately if it's true.
843      */
844     @Nullable
updateThumbnail(int taskId, ThumbnailData thumbnailData, boolean refreshNow)845     public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData, boolean refreshNow) {
846         TaskView taskView = getTaskViewByTaskId(taskId);
847         if (taskView != null) {
848             taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData, refreshNow);
849         }
850         return taskView;
851     }
852 
853     @Override
onWindowVisibilityChanged(int visibility)854     protected void onWindowVisibilityChanged(int visibility) {
855         super.onWindowVisibilityChanged(visibility);
856         updateTaskStackListenerState();
857     }
858 
init(OverviewActionsView actionsView, SplitSelectStateController splitController)859     public void init(OverviewActionsView actionsView, SplitSelectStateController splitController) {
860         mActionsView = actionsView;
861         mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
862         mSplitSelectStateController = splitController;
863     }
864 
getSplitPlaceholder()865     public SplitSelectStateController getSplitPlaceholder() {
866         return mSplitSelectStateController;
867     }
868 
isSplitSelectionActive()869     public boolean isSplitSelectionActive() {
870         return mSplitSelectStateController.isSplitSelectActive();
871     }
872 
873     @Override
onAttachedToWindow()874     protected void onAttachedToWindow() {
875         super.onAttachedToWindow();
876         updateTaskStackListenerState();
877         mModel.getThumbnailCache().getHighResLoadingState().addCallback(this);
878         mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
879         TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
880         mSyncTransactionApplier = new SurfaceTransactionApplier(this);
881         runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams()
882                 .setSyncTransactionApplier(mSyncTransactionApplier));
883         RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
884         mIPipAnimationListener.setActivityAndRecentsView(mActivity, this);
885         SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(
886                 mIPipAnimationListener);
887         mOrientationState.initListeners();
888         SplitScreenBounds.INSTANCE.addOnChangeListener(this);
889         mTaskOverlayFactory.initListeners();
890     }
891 
892     @Override
onDetachedFromWindow()893     protected void onDetachedFromWindow() {
894         super.onDetachedFromWindow();
895         updateTaskStackListenerState();
896         mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this);
897         mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
898         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
899         mSyncTransactionApplier = null;
900         runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams()
901                 .setSyncTransactionApplier(null));
902         executeSideTaskLaunchCallback();
903         RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
904         SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(null);
905         SplitScreenBounds.INSTANCE.removeOnChangeListener(this);
906         mIPipAnimationListener.setActivityAndRecentsView(null, null);
907         mOrientationState.destroyListeners();
908         mTaskOverlayFactory.removeListeners();
909     }
910 
911     @Override
onViewRemoved(View child)912     public void onViewRemoved(View child) {
913         super.onViewRemoved(child);
914 
915         // Clear the task data for the removed child if it was visible unless:
916         // - It's the initial taskview for entering split screen, we only pretend to dismiss the
917         // task
918         // - It's the focused task to be moved to the front, we immediately re-add the task
919         if (child instanceof TaskView && child != mSplitHiddenTaskView
920                 && child != mMovingTaskView) {
921             TaskView taskView = (TaskView) child;
922             for (int i : taskView.getTaskIds()) {
923                 mHasVisibleTaskData.delete(i);
924             }
925             if (child instanceof GroupedTaskView) {
926                 mGroupedTaskViewPool.recycle((GroupedTaskView)taskView);
927             } else {
928                 mTaskViewPool.recycle(taskView);
929             }
930             taskView.setTaskViewId(-1);
931             mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
932         }
933     }
934 
935     @Override
onViewAdded(View child)936     public void onViewAdded(View child) {
937         super.onViewAdded(child);
938         child.setAlpha(mContentAlpha);
939         // RecentsView is set to RTL in the constructor when system is using LTR. Here we set the
940         // child direction back to match system settings.
941         child.setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_LTR : View.LAYOUT_DIRECTION_RTL);
942         mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, false);
943         updateEmptyMessage();
944     }
945 
946     @Override
draw(Canvas canvas)947     public void draw(Canvas canvas) {
948         maybeDrawEmptyMessage(canvas);
949         super.draw(canvas);
950     }
951 
addSideTaskLaunchCallback(RunnableList callback)952     public void addSideTaskLaunchCallback(RunnableList callback) {
953         if (mSideTaskLaunchCallback == null) {
954             mSideTaskLaunchCallback = new RunnableList();
955         }
956         mSideTaskLaunchCallback.add(callback::executeAllAndDestroy);
957     }
958 
959     /**
960      * This is a one-time callback when touching in live tile mode. It's reset to null right
961      * after it's called.
962      */
setTaskLaunchListener(TaskLaunchListener taskLaunchListener)963     public void setTaskLaunchListener(TaskLaunchListener taskLaunchListener) {
964         mTaskLaunchListener = taskLaunchListener;
965     }
966 
onTaskLaunchedInLiveTileMode()967     public void onTaskLaunchedInLiveTileMode() {
968         if (mTaskLaunchListener != null) {
969             mTaskLaunchListener.onTaskLaunched();
970             mTaskLaunchListener = null;
971         }
972     }
973 
executeSideTaskLaunchCallback()974     private void executeSideTaskLaunchCallback() {
975         if (mSideTaskLaunchCallback != null) {
976             mSideTaskLaunchCallback.executeAllAndDestroy();
977             mSideTaskLaunchCallback = null;
978         }
979     }
980 
981     /**
982      * TODO(b/195675206) Check both taskIDs from runningTaskViewId
983      *  and launch if either of them is {@param taskId}
984      */
launchSideTaskInLiveTileModeForRestartedApp(int taskId)985     public void launchSideTaskInLiveTileModeForRestartedApp(int taskId) {
986         int runningTaskViewId = getTaskViewIdFromTaskId(taskId);
987         if (mRunningTaskViewId != -1 && mRunningTaskViewId == runningTaskViewId) {
988             TransformParams params = mRemoteTargetHandles[0].getTransformParams();
989             RemoteAnimationTargets targets = params.getTargetSet();
990             if (targets != null && targets.findTask(taskId) != null) {
991                 launchSideTaskInLiveTileMode(taskId, targets.apps, targets.wallpapers,
992                         targets.nonApps);
993             }
994         }
995     }
996 
launchSideTaskInLiveTileMode(int taskId, RemoteAnimationTargetCompat[] apps, RemoteAnimationTargetCompat[] wallpaper, RemoteAnimationTargetCompat[] nonApps)997     public void launchSideTaskInLiveTileMode(int taskId, RemoteAnimationTargetCompat[] apps,
998             RemoteAnimationTargetCompat[] wallpaper, RemoteAnimationTargetCompat[] nonApps) {
999         AnimatorSet anim = new AnimatorSet();
1000         TaskView taskView = getTaskViewByTaskId(taskId);
1001         if (taskView == null || !isTaskViewVisible(taskView)) {
1002             // TODO: Refine this animation.
1003             SurfaceTransactionApplier surfaceApplier =
1004                     new SurfaceTransactionApplier(mActivity.getDragLayer());
1005             ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
1006             appAnimator.setDuration(RECENTS_LAUNCH_DURATION);
1007             appAnimator.setInterpolator(ACCEL_DEACCEL);
1008             appAnimator.addUpdateListener(valueAnimator -> {
1009                 float percent = valueAnimator.getAnimatedFraction();
1010                 SurfaceParams.Builder builder = new SurfaceParams.Builder(
1011                         apps[apps.length - 1].leash);
1012                 Matrix matrix = new Matrix();
1013                 matrix.postScale(percent, percent);
1014                 matrix.postTranslate(mActivity.getDeviceProfile().widthPx * (1 - percent) / 2,
1015                         mActivity.getDeviceProfile().heightPx * (1 - percent) / 2);
1016                 builder.withAlpha(percent).withMatrix(matrix);
1017                 surfaceApplier.scheduleApply(builder.build());
1018             });
1019             anim.play(appAnimator);
1020             anim.addListener(new AnimatorListenerAdapter() {
1021                 @Override
1022                 public void onAnimationEnd(Animator animation) {
1023                     finishRecentsAnimation(false /* toRecents */, null);
1024                 }
1025             });
1026         } else {
1027             TaskViewUtils.composeRecentsLaunchAnimator(anim, taskView, apps, wallpaper, nonApps,
1028                     true /* launcherClosing */, mActivity.getStateManager(), this,
1029                     getDepthController());
1030         }
1031         anim.start();
1032     }
1033 
isTaskViewVisible(TaskView tv)1034     public boolean isTaskViewVisible(TaskView tv) {
1035         if (showAsGrid()) {
1036             int screenStart = mOrientationHandler.getPrimaryScroll(this);
1037             int screenEnd = screenStart + mOrientationHandler.getMeasuredSize(this);
1038             return isTaskViewWithinBounds(tv, screenStart, screenEnd);
1039         } else {
1040             // For now, just check if it's the active task or an adjacent task
1041             return Math.abs(indexOfChild(tv) - getNextPage()) <= 1;
1042         }
1043     }
1044 
isTaskViewFullyVisible(TaskView tv)1045     public boolean isTaskViewFullyVisible(TaskView tv) {
1046         if (showAsGrid()) {
1047             int screenStart = mOrientationHandler.getPrimaryScroll(this);
1048             int screenEnd = screenStart + mOrientationHandler.getMeasuredSize(this);
1049             return isTaskViewFullyWithinBounds(tv, screenStart, screenEnd);
1050         } else {
1051             // For now, just check if it's the active task
1052             return indexOfChild(tv) == getNextPage();
1053         }
1054     }
1055 
1056     @Nullable
getLastGridTaskView()1057     private TaskView getLastGridTaskView() {
1058         return getLastGridTaskView(getTopRowIdArray(), getBottomRowIdArray());
1059     }
1060 
1061     @Nullable
getLastGridTaskView(IntArray topRowIdArray, IntArray bottomRowIdArray)1062     private TaskView getLastGridTaskView(IntArray topRowIdArray, IntArray bottomRowIdArray) {
1063         if (topRowIdArray.isEmpty() && bottomRowIdArray.isEmpty()) {
1064             return null;
1065         }
1066         int lastTaskViewId = topRowIdArray.size() >= bottomRowIdArray.size() ? topRowIdArray.get(
1067                 topRowIdArray.size() - 1) : bottomRowIdArray.get(bottomRowIdArray.size() - 1);
1068         return getTaskViewFromTaskViewId(lastTaskViewId);
1069     }
1070 
getSnapToLastTaskScrollDiff()1071     private int getSnapToLastTaskScrollDiff() {
1072         // Snap to a position where ClearAll is just invisible.
1073         int screenStart = mOrientationHandler.getPrimaryScroll(this);
1074         int clearAllWidth = mOrientationHandler.getPrimarySize(mClearAllButton);
1075         int clearAllScroll = getScrollForPage(indexOfChild(mClearAllButton));
1076         int targetScroll = clearAllScroll + (mIsRtl ? clearAllWidth : -clearAllWidth);
1077         return screenStart - targetScroll;
1078     }
1079 
getSnapToFocusedTaskScrollDiff(boolean isClearAllHidden)1080     private int getSnapToFocusedTaskScrollDiff(boolean isClearAllHidden) {
1081         int screenStart = mOrientationHandler.getPrimaryScroll(this);
1082         int targetScroll = getScrollForPage(indexOfChild(getFocusedTaskView()));
1083         if (!isClearAllHidden) {
1084             int clearAllWidth = mOrientationHandler.getPrimarySize(mClearAllButton);
1085             int taskGridHorizontalDiff = mLastComputedTaskSize.right - mLastComputedGridSize.right;
1086             int clearAllFocusScrollDiff =  taskGridHorizontalDiff - clearAllWidth;
1087             targetScroll += mIsRtl ? clearAllFocusScrollDiff : -clearAllFocusScrollDiff;
1088         }
1089         return screenStart - targetScroll;
1090     }
1091 
isTaskViewWithinBounds(TaskView tv, int start, int end)1092     private boolean isTaskViewWithinBounds(TaskView tv, int start, int end) {
1093         int taskStart = mOrientationHandler.getChildStart(tv) + (int) tv.getOffsetAdjustment(
1094                 showAsFullscreen(), showAsGrid());
1095         int taskSize = (int) (mOrientationHandler.getMeasuredSize(tv) * tv.getSizeAdjustment(
1096                 showAsFullscreen()));
1097         int taskEnd = taskStart + taskSize;
1098         return (taskStart >= start && taskStart <= end) || (taskEnd >= start
1099                 && taskEnd <= end);
1100     }
1101 
isTaskViewFullyWithinBounds(TaskView tv, int start, int end)1102     private boolean isTaskViewFullyWithinBounds(TaskView tv, int start, int end) {
1103         int taskStart = mOrientationHandler.getChildStart(tv) + (int) tv.getOffsetAdjustment(
1104                 showAsFullscreen(), showAsGrid());
1105         int taskSize = (int) (mOrientationHandler.getMeasuredSize(tv) * tv.getSizeAdjustment(
1106                 showAsFullscreen()));
1107         int taskEnd = taskStart + taskSize;
1108         return taskStart >= start && taskEnd <= end;
1109     }
1110 
1111     /**
1112      * Returns true if the task is in expected scroll position.
1113      *
1114      * @param taskIndex the index of the task
1115      */
isTaskInExpectedScrollPosition(int taskIndex)1116     public boolean isTaskInExpectedScrollPosition(int taskIndex) {
1117         return getScrollForPage(taskIndex) == getPagedOrientationHandler().getPrimaryScroll(this);
1118     }
1119 
1120     /**
1121      * Returns a {@link TaskView} that has taskId matching {@code taskId} or null if no match.
1122      */
1123     @Nullable
getTaskViewByTaskId(int taskId)1124     public TaskView getTaskViewByTaskId(int taskId) {
1125         if (taskId == -1) {
1126             return null;
1127         }
1128 
1129         for (int i = 0; i < getTaskViewCount(); i++) {
1130             TaskView taskView = requireTaskViewAt(i);
1131             int[] taskIds = taskView.getTaskIds();
1132             if (taskIds[0] == taskId || taskIds[1] == taskId) {
1133                 return taskView;
1134             }
1135         }
1136         return null;
1137     }
1138 
setOverviewStateEnabled(boolean enabled)1139     public void setOverviewStateEnabled(boolean enabled) {
1140         mOverviewStateEnabled = enabled;
1141         updateTaskStackListenerState();
1142         mOrientationState.setRotationWatcherEnabled(enabled);
1143         if (!enabled) {
1144             // Reset the running task when leaving overview since it can still have a reference to
1145             // its thumbnail
1146             mTmpRunningTasks = null;
1147             mSplitBoundsConfig = null;
1148         }
1149         updateLocusId();
1150     }
1151 
1152     /**
1153      * Whether the Clear All button is hidden or fully visible. Used to determine if center
1154      * displayed page is a task or the Clear All button.
1155      *
1156      * @return True = Clear All button not fully visible, center page is a task. False = Clear All
1157      * button fully visible, center page is Clear All button.
1158      */
isClearAllHidden()1159     public boolean isClearAllHidden() {
1160         return mClearAllButton.getAlpha() != 1f;
1161     }
1162 
1163     @Override
onPageBeginTransition()1164     protected void onPageBeginTransition() {
1165         super.onPageBeginTransition();
1166         mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, true);
1167     }
1168 
1169     @Override
onPageEndTransition()1170     protected void onPageEndTransition() {
1171         super.onPageEndTransition();
1172         if (isClearAllHidden()) {
1173             mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false);
1174         }
1175         if (getNextPage() > 0) {
1176             setSwipeDownShouldLaunchApp(true);
1177         }
1178     }
1179 
1180     @Override
getSignificantMoveThreshold()1181     protected float getSignificantMoveThreshold() {
1182         return mActivity.getDeviceProfile().isTablet ? SIGNIFICANT_MOVE_THRESHOLD_TABLET
1183                 : super.getSignificantMoveThreshold();
1184     }
1185 
1186     @Override
onTouchEvent(MotionEvent ev)1187     public boolean onTouchEvent(MotionEvent ev) {
1188         super.onTouchEvent(ev);
1189 
1190         if (showAsGrid()) {
1191             int taskCount = getTaskViewCount();
1192             for (int i = 0; i < taskCount; i++) {
1193                 TaskView taskView = requireTaskViewAt(i);
1194                 if (isTaskViewVisible(taskView) && taskView.offerTouchToChildren(ev)) {
1195                     // Keep consuming events to pass to delegate
1196                     return true;
1197                 }
1198             }
1199         } else {
1200             TaskView taskView = getCurrentPageTaskView();
1201             if (taskView != null && taskView.offerTouchToChildren(ev)) {
1202                 // Keep consuming events to pass to delegate
1203                 return true;
1204             }
1205         }
1206 
1207         final int x = (int) ev.getX();
1208         final int y = (int) ev.getY();
1209         switch (ev.getAction()) {
1210             case MotionEvent.ACTION_UP:
1211                 if (mTouchDownToStartHome) {
1212                     startHome();
1213                 }
1214                 mTouchDownToStartHome = false;
1215                 break;
1216             case MotionEvent.ACTION_CANCEL:
1217                 mTouchDownToStartHome = false;
1218                 break;
1219             case MotionEvent.ACTION_MOVE:
1220                 // Passing the touch slop will not allow dismiss to home
1221                 if (mTouchDownToStartHome &&
1222                         (isHandlingTouch() ||
1223                                 squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop)) {
1224                     mTouchDownToStartHome = false;
1225                 }
1226                 break;
1227             case MotionEvent.ACTION_DOWN:
1228                 // Touch down anywhere but the deadzone around the visible clear all button and
1229                 // between the task views will start home on touch up
1230                 if (!isHandlingTouch() && !isModal()) {
1231                     if (mShowEmptyMessage) {
1232                         mTouchDownToStartHome = true;
1233                     } else {
1234                         updateDeadZoneRects();
1235                         final boolean clearAllButtonDeadZoneConsumed =
1236                                 mClearAllButton.getAlpha() == 1
1237                                         && mClearAllButtonDeadZoneRect.contains(x, y);
1238                         final boolean cameFromNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
1239                         if (!clearAllButtonDeadZoneConsumed && !cameFromNavBar
1240                                 && !mTaskViewDeadZoneRect.contains(x + getScrollX(), y)) {
1241                             mTouchDownToStartHome = true;
1242                         }
1243                     }
1244                 }
1245                 mDownX = x;
1246                 mDownY = y;
1247                 break;
1248         }
1249 
1250         return isHandlingTouch();
1251     }
1252 
1253     @Override
onNotSnappingToPageInFreeScroll()1254     protected void onNotSnappingToPageInFreeScroll() {
1255         int finalPos = mScroller.getFinalX();
1256         if (finalPos > mMinScroll && finalPos < mMaxScroll) {
1257             int firstPageScroll = getScrollForPage(!mIsRtl ? 0 : getPageCount() - 1);
1258             int lastPageScroll = getScrollForPage(!mIsRtl ? getPageCount() - 1 : 0);
1259 
1260             // If scrolling ends in the half of the added space that is closer to
1261             // the end, settle to the end. Otherwise snap to the nearest page.
1262             // If flinging past one of the ends, don't change the velocity as it
1263             // will get stopped at the end anyway.
1264             int pageSnapped = finalPos < (firstPageScroll + mMinScroll) / 2
1265                     ? mMinScroll
1266                     : finalPos > (lastPageScroll + mMaxScroll) / 2
1267                             ? mMaxScroll
1268                             : getScrollForPage(mNextPage);
1269 
1270             if (showAsGrid()) {
1271                 if (isSplitSelectionActive()) {
1272                     return;
1273                 }
1274                 TaskView taskView = getTaskViewAt(mNextPage);
1275                 // Only snap to fully visible focused task.
1276                 if (taskView == null
1277                         || !taskView.isFocusedTask()
1278                         || !isTaskViewFullyVisible(taskView)) {
1279                     return;
1280                 }
1281             }
1282 
1283             mScroller.setFinalX(pageSnapped);
1284             // Ensure the scroll/snap doesn't happen too fast;
1285             int extraScrollDuration = OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION
1286                     - mScroller.getDuration();
1287             if (extraScrollDuration > 0) {
1288                 mScroller.extendDuration(extraScrollDuration);
1289             }
1290         }
1291     }
1292 
1293     @Override
onEdgeAbsorbingScroll()1294     protected void onEdgeAbsorbingScroll() {
1295         vibrateForScroll();
1296     }
1297 
1298     @Override
onScrollOverPageChanged()1299     protected void onScrollOverPageChanged() {
1300         vibrateForScroll();
1301     }
1302 
vibrateForScroll()1303     private void vibrateForScroll() {
1304         long now = SystemClock.uptimeMillis();
1305         if (now - mScrollLastHapticTimestamp > mScrollHapticMinGapMillis) {
1306             mScrollLastHapticTimestamp = now;
1307             VibratorWrapper.INSTANCE.get(mContext).vibrate(SCROLL_VIBRATION_PRIMITIVE,
1308                     SCROLL_VIBRATION_PRIMITIVE_SCALE, SCROLL_VIBRATION_FALLBACK);
1309         }
1310     }
1311 
1312     @Override
determineScrollingStart(MotionEvent ev, float touchSlopScale)1313     protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) {
1314         // Enables swiping to the left or right only if the task overlay is not modal.
1315         if (!isModal()) {
1316             super.determineScrollingStart(ev, touchSlopScale);
1317         }
1318     }
1319 
1320     /**
1321      * Moves the focused task to the front of the carousel in tablets, to minimize animation
1322      * required to focus the task in grid.
1323      */
moveFocusedTaskToFront()1324     public void moveFocusedTaskToFront() {
1325         if (!mActivity.getDeviceProfile().overviewShowAsGrid) {
1326             return;
1327         }
1328 
1329         TaskView focusedTaskView = getFocusedTaskView();
1330         if (focusedTaskView == null) {
1331             return;
1332         }
1333 
1334         if (indexOfChild(focusedTaskView) != mCurrentPage) {
1335             return;
1336         }
1337 
1338         if (mCurrentPage == 0) {
1339             return;
1340         }
1341 
1342         int primaryScroll = mOrientationHandler.getPrimaryScroll(this);
1343         int currentPageScroll = getScrollForPage(mCurrentPage);
1344         mCurrentPageScrollDiff = primaryScroll - currentPageScroll;
1345 
1346         mMovingTaskView = focusedTaskView;
1347         removeView(focusedTaskView);
1348         mMovingTaskView = null;
1349         focusedTaskView.resetPersistentViewTransforms();
1350         addView(focusedTaskView, 0);
1351         setCurrentPage(0);
1352 
1353         updateGridProperties();
1354     }
1355 
applyLoadPlan(ArrayList<GroupTask> taskGroups)1356     protected void applyLoadPlan(ArrayList<GroupTask> taskGroups) {
1357         if (mPendingAnimation != null) {
1358             mPendingAnimation.addEndListener(success -> applyLoadPlan(taskGroups));
1359             return;
1360         }
1361 
1362         mLoadPlanEverApplied = true;
1363         if (taskGroups == null || taskGroups.isEmpty()) {
1364             removeTasksViewsAndClearAllButton();
1365             onTaskStackUpdated();
1366             return;
1367         }
1368 
1369         int currentTaskId = -1;
1370         TaskView currentTaskView = getTaskViewAt(mCurrentPage);
1371         if (currentTaskView != null) {
1372             currentTaskId = currentTaskView.getTask().key.id;
1373         }
1374 
1375         // Unload existing visible task data
1376         unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
1377 
1378         TaskView ignoreResetTaskView =
1379                 mIgnoreResetTaskId == -1 ? null : getTaskViewByTaskId(mIgnoreResetTaskId);
1380 
1381         // Save running task ID if it exists before rebinding all taskViews, otherwise the task from
1382         // the runningTaskView currently bound could get assigned to another TaskView
1383         int runningTaskId = getTaskIdsForTaskViewId(mRunningTaskViewId)[0];
1384         int focusedTaskId = getTaskIdsForTaskViewId(mFocusedTaskViewId)[0];
1385 
1386         // Removing views sets the currentPage to 0, so we save this and restore it after
1387         // the new set of views are added
1388         int previousCurrentPage = mCurrentPage;
1389         removeAllViews();
1390 
1391         // Add views as children based on whether it's grouped or single task
1392         for (int i = taskGroups.size() - 1; i >= 0; i--) {
1393             GroupTask groupTask = taskGroups.get(i);
1394             boolean hasMultipleTasks = groupTask.hasMultipleTasks();
1395             TaskView taskView = getTaskViewFromPool(hasMultipleTasks);
1396             addView(taskView);
1397 
1398             if (hasMultipleTasks) {
1399                 boolean firstTaskIsLeftTopTask =
1400                         groupTask.mStagedSplitBounds.leftTopTaskId == groupTask.task1.key.id;
1401                 Task leftTopTask = firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2;
1402                 Task rightBottomTask = firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1;
1403                 ((GroupedTaskView) taskView).bind(leftTopTask, rightBottomTask, mOrientationState,
1404                         groupTask.mStagedSplitBounds);
1405             } else {
1406                 taskView.bind(groupTask.task1, mOrientationState);
1407             }
1408         }
1409         if (!taskGroups.isEmpty()) {
1410             addView(mClearAllButton);
1411         }
1412 
1413         boolean settlingOnNewTask = mNextPage != INVALID_PAGE;
1414         if (settlingOnNewTask) {
1415             // Restore mCurrentPage but don't call setCurrentPage() as that clobbers the scroll.
1416             mCurrentPage = previousCurrentPage;
1417         } else {
1418             setCurrentPage(previousCurrentPage);
1419         }
1420 
1421         // Keep same previous focused task
1422         TaskView newFocusedTaskView = getTaskViewByTaskId(focusedTaskId);
1423         // If the list changed, maybe the focused task doesn't exist anymore
1424         if (newFocusedTaskView == null && getTaskViewCount() > 0) {
1425             newFocusedTaskView = getTaskViewAt(0);
1426         }
1427         mFocusedTaskViewId = newFocusedTaskView != null ?
1428                 newFocusedTaskView.getTaskViewId() : -1;
1429         updateTaskSize();
1430         updateChildTaskOrientations();
1431 
1432         TaskView newRunningTaskView = null;
1433         if (runningTaskId != -1) {
1434             // Update mRunningTaskViewId to be the new TaskView that was assigned by binding
1435             // the full list of tasks to taskViews
1436             newRunningTaskView = getTaskViewByTaskId(runningTaskId);
1437             if (newRunningTaskView != null) {
1438                 mRunningTaskViewId = newRunningTaskView.getTaskViewId();
1439             } else {
1440                 mRunningTaskViewId = -1;
1441             }
1442         }
1443 
1444         int targetPage = -1;
1445         if (!settlingOnNewTask) {
1446             // Set the current page to the running task, but not if settling on new task.
1447             if (runningTaskId != -1) {
1448                 targetPage = indexOfChild(newRunningTaskView);
1449             } else if (getTaskViewCount() > 0) {
1450                 targetPage = indexOfChild(requireTaskViewAt(0));
1451             }
1452         } else if (currentTaskId != -1) {
1453             currentTaskView = getTaskViewByTaskId(currentTaskId);
1454             if (currentTaskView != null) {
1455                 targetPage = indexOfChild(currentTaskView);
1456             }
1457         }
1458         if (targetPage != -1 && mCurrentPage != targetPage) {
1459             setCurrentPage(targetPage);
1460         }
1461 
1462         if (mIgnoreResetTaskId != -1 &&
1463                 getTaskViewByTaskId(mIgnoreResetTaskId) != ignoreResetTaskView) {
1464             // If the taskView mapping is changing, do not preserve the visuals. Since we are
1465             // mostly preserving the first task, and new taskViews are added to the end, it should
1466             // generally map to the same task.
1467             mIgnoreResetTaskId = -1;
1468         }
1469         resetTaskVisuals();
1470         onTaskStackUpdated();
1471         updateEnabledOverlays();
1472     }
1473 
isModal()1474     private boolean isModal() {
1475         return mTaskModalness > 0;
1476     }
1477 
isLoadingTasks()1478     public boolean isLoadingTasks() {
1479         return mModel.isLoadingTasksInBackground();
1480     }
1481 
removeTasksViewsAndClearAllButton()1482     private void removeTasksViewsAndClearAllButton() {
1483         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
1484             removeView(requireTaskViewAt(i));
1485         }
1486         if (indexOfChild(mClearAllButton) != -1) {
1487             removeView(mClearAllButton);
1488         }
1489     }
1490 
getTaskViewCount()1491     public int getTaskViewCount() {
1492         int taskViewCount = getChildCount();
1493         if (indexOfChild(mClearAllButton) != -1) {
1494             taskViewCount--;
1495         }
1496         return taskViewCount;
1497     }
1498 
getGroupedTaskViewCount()1499     public int getGroupedTaskViewCount() {
1500         int groupViewCount = 0;
1501         for (int i = 0; i < getChildCount(); i++) {
1502             if (getChildAt(i) instanceof GroupedTaskView) {
1503                 groupViewCount++;
1504             }
1505         }
1506         return groupViewCount;
1507     }
1508 
1509     /**
1510      * Returns the number of tasks in the top row of the overview grid.
1511      */
getTopRowTaskCountForTablet()1512     public int getTopRowTaskCountForTablet() {
1513         return mTopRowIdSet.size();
1514     }
1515 
1516     /**
1517      * Returns the number of tasks in the bottom row of the overview grid.
1518      */
getBottomRowTaskCountForTablet()1519     public int getBottomRowTaskCountForTablet() {
1520         return getTaskViewCount() - mTopRowIdSet.size() - 1;
1521     }
1522 
onTaskStackUpdated()1523     protected void onTaskStackUpdated() {
1524         // Lazily update the empty message only when the task stack is reapplied
1525         updateEmptyMessage();
1526     }
1527 
resetTaskVisuals()1528     public void resetTaskVisuals() {
1529         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
1530             TaskView taskView = requireTaskViewAt(i);
1531             if (mIgnoreResetTaskId != taskView.getTaskIds()[0]) {
1532                 taskView.resetViewTransforms();
1533                 taskView.setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
1534                 taskView.setStableAlpha(mContentAlpha);
1535                 taskView.setFullscreenProgress(mFullscreenProgress);
1536                 taskView.setModalness(mTaskModalness);
1537             }
1538         }
1539         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
1540             // resetTaskVisuals is called at the end of dismiss animation which could update
1541             // primary and secondary translation of the live tile cut out. We will need to do so
1542             // here accordingly.
1543             runActionOnRemoteHandles(remoteTargetHandle -> {
1544                 TaskViewSimulator simulator = remoteTargetHandle.getTaskViewSimulator();
1545                 simulator.taskPrimaryTranslation.value = 0;
1546                 simulator.taskSecondaryTranslation.value = 0;
1547                 simulator.fullScreenProgress.value = 0;
1548                 simulator.recentsViewScale.value = 1;
1549             });
1550             // Similar to setRunningTaskHidden below, reapply the state before runningTaskView is
1551             // null.
1552             if (!mRunningTaskShowScreenshot) {
1553                 setRunningTaskViewShowScreenshot(mRunningTaskShowScreenshot);
1554             }
1555         }
1556         if (mRunningTaskTileHidden) {
1557             setRunningTaskHidden(mRunningTaskTileHidden);
1558         }
1559 
1560         updateCurveProperties();
1561         // Update the set of visible task's data
1562         loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
1563         setTaskModalness(0);
1564         setColorTint(0);
1565     }
1566 
setFullscreenProgress(float fullscreenProgress)1567     public void setFullscreenProgress(float fullscreenProgress) {
1568         mFullscreenProgress = fullscreenProgress;
1569         int taskCount = getTaskViewCount();
1570         for (int i = 0; i < taskCount; i++) {
1571             requireTaskViewAt(i).setFullscreenProgress(mFullscreenProgress);
1572         }
1573         mClearAllButton.setFullscreenProgress(fullscreenProgress);
1574 
1575         // Fade out the actions view quickly (0.1 range)
1576         mActionsView.getFullscreenAlpha().setValue(
1577                 mapToRange(fullscreenProgress, 0, 0.1f, 1f, 0f, LINEAR));
1578     }
1579 
updateTaskStackListenerState()1580     private void updateTaskStackListenerState() {
1581         boolean handleTaskStackChanges = mOverviewStateEnabled && isAttachedToWindow()
1582                 && getWindowVisibility() == VISIBLE;
1583         if (handleTaskStackChanges != mHandleTaskStackChanges) {
1584             mHandleTaskStackChanges = handleTaskStackChanges;
1585             if (handleTaskStackChanges) {
1586                 reloadIfNeeded();
1587             }
1588         }
1589     }
1590 
1591     @Override
setInsets(Rect insets)1592     public void setInsets(Rect insets) {
1593         mInsets.set(insets);
1594 
1595         // Update DeviceProfile dependant state.
1596         DeviceProfile dp = mActivity.getDeviceProfile();
1597         setOverviewGridEnabled(
1598                 mActivity.getStateManager().getState().displayOverviewTasksAsGrid(dp));
1599         setPageSpacing(dp.overviewPageSpacing);
1600 
1601         // Propagate DeviceProfile change event.
1602         runActionOnRemoteHandles(
1603                 remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator().setDp(dp));
1604         mActionsView.setDp(dp);
1605         mOrientationState.setDeviceProfile(dp);
1606 
1607         // Update RecentsView and TaskView's DeviceProfile dependent layout.
1608         updateOrientationHandler();
1609     }
1610 
updateOrientationHandler()1611     private void updateOrientationHandler() {
1612         updateOrientationHandler(true);
1613     }
1614 
updateOrientationHandler(boolean forceRecreateDragLayerControllers)1615     private void updateOrientationHandler(boolean forceRecreateDragLayerControllers) {
1616         // Handle orientation changes.
1617         PagedOrientationHandler oldOrientationHandler = mOrientationHandler;
1618         mOrientationHandler = mOrientationState.getOrientationHandler();
1619 
1620         mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
1621         setLayoutDirection(mIsRtl
1622                 ? View.LAYOUT_DIRECTION_RTL
1623                 : View.LAYOUT_DIRECTION_LTR);
1624         mClearAllButton.setLayoutDirection(mIsRtl
1625                 ? View.LAYOUT_DIRECTION_LTR
1626                 : View.LAYOUT_DIRECTION_RTL);
1627         mClearAllButton.setRotation(mOrientationHandler.getDegreesRotated());
1628 
1629         if (forceRecreateDragLayerControllers
1630                 || !mOrientationHandler.equals(oldOrientationHandler)) {
1631             // Changed orientations, update controllers so they intercept accordingly.
1632             mActivity.getDragLayer().recreateControllers();
1633             onOrientationChanged();
1634         }
1635 
1636         boolean isInLandscape = mOrientationState.getTouchRotation() != ROTATION_0
1637                 || mOrientationState.getRecentsActivityRotation() != ROTATION_0;
1638         mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION,
1639                 !mOrientationState.isRecentsActivityRotationAllowed() && isInLandscape);
1640 
1641         // Update TaskView's DeviceProfile dependent layout.
1642         updateChildTaskOrientations();
1643 
1644         // Recalculate DeviceProfile dependent layout.
1645         updateSizeAndPadding();
1646 
1647         requestLayout();
1648         // Reapply the current page to update page scrolls.
1649         setCurrentPage(mCurrentPage);
1650     }
1651 
onOrientationChanged()1652     private void onOrientationChanged() {
1653         // If overview is in modal state when rotate, reset it to overview state without running
1654         // animation.
1655         setModalStateEnabled(false);
1656         if (isSplitSelectionActive()) {
1657             onRotateInSplitSelectionState();
1658         }
1659     }
1660 
1661     // Update task size and padding that are dependent on DeviceProfile and insets.
updateSizeAndPadding()1662     private void updateSizeAndPadding() {
1663         DeviceProfile dp = mActivity.getDeviceProfile();
1664         getTaskSize(mTempRect);
1665         mTaskWidth = mTempRect.width();
1666         mTaskHeight = mTempRect.height();
1667 
1668         mTempRect.top -= dp.overviewTaskThumbnailTopMarginPx;
1669         setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top,
1670                 dp.widthPx - mInsets.right - mTempRect.right,
1671                 dp.heightPx - mInsets.bottom - mTempRect.bottom);
1672 
1673         mSizeStrategy.calculateGridSize(mActivity, mActivity.getDeviceProfile(),
1674                 mLastComputedGridSize);
1675         mSizeStrategy.calculateGridTaskSize(mActivity, mActivity.getDeviceProfile(),
1676                 mLastComputedGridTaskSize, mOrientationHandler);
1677 
1678         mTaskGridVerticalDiff = mLastComputedGridTaskSize.top - mLastComputedTaskSize.top;
1679         mTopBottomRowHeightDiff =
1680                 mLastComputedGridTaskSize.height() + dp.overviewTaskThumbnailTopMarginPx
1681                         + dp.overviewRowSpacing;
1682 
1683         // Force TaskView to update size from thumbnail
1684         updateTaskSize();
1685     }
1686 
1687     /**
1688      * Updates TaskView scaling and translation required to support variable width.
1689      */
updateTaskSize()1690     private void updateTaskSize() {
1691         updateTaskSize(false);
1692     }
1693 
1694     /**
1695      * Updates TaskView scaling and translation required to support variable width.
1696      *
1697      * @param isTaskDismissal indicates if update was called due to task dismissal
1698      */
updateTaskSize(boolean isTaskDismissal)1699     private void updateTaskSize(boolean isTaskDismissal) {
1700         final int taskCount = getTaskViewCount();
1701         if (taskCount == 0) {
1702             return;
1703         }
1704 
1705         float accumulatedTranslationX = 0;
1706         for (int i = 0; i < taskCount; i++) {
1707             TaskView taskView = requireTaskViewAt(i);
1708             taskView.updateTaskSize();
1709             taskView.getPrimaryNonGridTranslationProperty().set(taskView, accumulatedTranslationX);
1710             taskView.getSecondaryNonGridTranslationProperty().set(taskView, 0f);
1711             // Compensate space caused by TaskView scaling.
1712             float widthDiff =
1713                     taskView.getLayoutParams().width * (1 - taskView.getNonGridScale());
1714             accumulatedTranslationX += mIsRtl ? widthDiff : -widthDiff;
1715         }
1716 
1717         mClearAllButton.setFullscreenTranslationPrimary(accumulatedTranslationX);
1718 
1719         updateGridProperties(isTaskDismissal);
1720     }
1721 
getTaskSize(Rect outRect)1722     public void getTaskSize(Rect outRect) {
1723         mSizeStrategy.calculateTaskSize(mActivity, mActivity.getDeviceProfile(), outRect);
1724         mLastComputedTaskSize.set(outRect);
1725     }
1726 
1727     /**
1728      * Returns the size of task selected to enter modal state.
1729      */
getSelectedTaskSize()1730     public Point getSelectedTaskSize() {
1731         mSizeStrategy.calculateTaskSize(mActivity, mActivity.getDeviceProfile(), mTempRect);
1732         return new Point(mTempRect.width(), mTempRect.height());
1733     }
1734 
1735     /** Gets the last computed task size */
getLastComputedTaskSize()1736     public Rect getLastComputedTaskSize() {
1737         return mLastComputedTaskSize;
1738     }
1739 
getLastComputedGridTaskSize()1740     public Rect getLastComputedGridTaskSize() {
1741         return mLastComputedGridTaskSize;
1742     }
1743 
1744     /** Gets the task size for modal state. */
getModalTaskSize(Rect outRect)1745     public void getModalTaskSize(Rect outRect) {
1746         mSizeStrategy.calculateModalTaskSize(mActivity, mActivity.getDeviceProfile(), outRect);
1747     }
1748 
1749     @Override
computeScrollHelper()1750     protected boolean computeScrollHelper() {
1751         boolean scrolling = super.computeScrollHelper();
1752         boolean isFlingingFast = false;
1753         updateCurveProperties();
1754         if (scrolling || isHandlingTouch()) {
1755             if (scrolling) {
1756                 // Check if we are flinging quickly to disable high res thumbnail loading
1757                 isFlingingFast = mScroller.getCurrVelocity() > mFastFlingVelocity;
1758             }
1759 
1760             // After scrolling, update the visible task's data
1761             loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
1762         }
1763 
1764         // Update ActionsView's visibility when scroll changes.
1765         updateActionsViewFocusedScroll();
1766 
1767         // Update the high res thumbnail loader state
1768         mModel.getThumbnailCache().getHighResLoadingState().setFlingingFast(isFlingingFast);
1769         return scrolling;
1770     }
1771 
updateActionsViewFocusedScroll()1772     private void updateActionsViewFocusedScroll() {
1773         boolean hiddenFocusedScroll;
1774         if (showAsGrid()) {
1775             TaskView focusedTaskView = getFocusedTaskView();
1776             hiddenFocusedScroll = focusedTaskView == null
1777                     || !isTaskInExpectedScrollPosition(indexOfChild(focusedTaskView));
1778         } else {
1779             hiddenFocusedScroll = false;
1780         }
1781         mActionsView.updateHiddenFlags(OverviewActionsView.HIDDEN_FOCUSED_SCROLL,
1782                 hiddenFocusedScroll);
1783     }
1784 
1785     /**
1786      * Scales and adjusts translation of adjacent pages as if on a curved carousel.
1787      */
updateCurveProperties()1788     public void updateCurveProperties() {
1789         if (getPageCount() == 0 || getPageAt(0).getMeasuredWidth() == 0) {
1790             return;
1791         }
1792         int scroll = mOrientationHandler.getPrimaryScroll(this);
1793         mClearAllButton.onRecentsViewScroll(scroll, mOverviewGridEnabled);
1794     }
1795 
1796     @Override
getDestinationPage(int scaledScroll)1797     protected int getDestinationPage(int scaledScroll) {
1798         if (!mActivity.getDeviceProfile().overviewShowAsGrid) {
1799             return super.getDestinationPage(scaledScroll);
1800         }
1801 
1802         final int childCount = getChildCount();
1803         if (mPageScrolls == null || childCount != mPageScrolls.length) {
1804             return -1;
1805         }
1806 
1807         // When in tablet with variable task width, return the page which scroll is closest to
1808         // screenStart instead of page nearest to center of screen.
1809         int minDistanceFromScreenStart = Integer.MAX_VALUE;
1810         int minDistanceFromScreenStartIndex = -1;
1811         for (int i = 0; i < childCount; ++i) {
1812             int distanceFromScreenStart = Math.abs(mPageScrolls[i] - scaledScroll);
1813             if (distanceFromScreenStart < minDistanceFromScreenStart) {
1814                 minDistanceFromScreenStart = distanceFromScreenStart;
1815                 minDistanceFromScreenStartIndex = i;
1816             }
1817         }
1818         return minDistanceFromScreenStartIndex;
1819     }
1820 
1821     /**
1822      * Iterates through all the tasks, and loads the associated task data for newly visible tasks,
1823      * and unloads the associated task data for tasks that are no longer visible.
1824      */
loadVisibleTaskData(@askView.TaskDataChanges int dataChanges)1825     public void loadVisibleTaskData(@TaskView.TaskDataChanges int dataChanges) {
1826         boolean hasLeftOverview = !mOverviewStateEnabled && mScroller.isFinished();
1827         if (hasLeftOverview || mTaskListChangeId == -1) {
1828             // Skip loading visible task data if we've already left the overview state, or if the
1829             // task list hasn't been loaded yet (the task views will not reflect the task list)
1830             return;
1831         }
1832 
1833         int lower = 0;
1834         int upper = 0;
1835         int visibleStart = 0;
1836         int visibleEnd = 0;
1837         if (showAsGrid()) {
1838             int screenStart = mOrientationHandler.getPrimaryScroll(this);
1839             int pageOrientedSize = mOrientationHandler.getMeasuredSize(this);
1840             int halfScreenSize = pageOrientedSize / 2;
1841             // Use +/- 50% screen width as visible area.
1842             visibleStart = screenStart - halfScreenSize;
1843             visibleEnd = screenStart + pageOrientedSize + halfScreenSize;
1844         } else {
1845             int centerPageIndex = getPageNearestToCenterOfScreen();
1846             int numChildren = getChildCount();
1847             lower = Math.max(0, centerPageIndex - 2);
1848             upper = Math.min(centerPageIndex + 2, numChildren - 1);
1849         }
1850 
1851         // Update the task data for the in/visible children
1852         for (int i = 0; i < getTaskViewCount(); i++) {
1853             TaskView taskView = requireTaskViewAt(i);
1854             Task task = taskView.getTask();
1855             int index = indexOfChild(taskView);
1856             boolean visible;
1857             if (showAsGrid()) {
1858                 visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd);
1859             } else {
1860                 visible = lower <= index && index <= upper;
1861             }
1862             if (visible) {
1863                 boolean skipLoadingTask = false;
1864                 if (mTmpRunningTasks != null) {
1865                     for (Task t : mTmpRunningTasks) {
1866                         if (task == t) {
1867                             // Skip loading if this is the task that we are animating into
1868                             skipLoadingTask = true;
1869                             break;
1870                         }
1871                     }
1872                 }
1873                 if (skipLoadingTask) {
1874                     continue;
1875                 }
1876                 if (!mHasVisibleTaskData.get(task.key.id)) {
1877                     // Ignore thumbnail update if it's current running task during the gesture
1878                     // We snapshot at end of gesture, it will update then
1879                     int changes = dataChanges;
1880                     if (taskView == getRunningTaskView() && mGestureActive) {
1881                         changes &= ~TaskView.FLAG_UPDATE_THUMBNAIL;
1882                     }
1883                     taskView.onTaskListVisibilityChanged(true /* visible */, changes);
1884                 }
1885                 mHasVisibleTaskData.put(task.key.id, visible);
1886             } else {
1887                 if (mHasVisibleTaskData.get(task.key.id)) {
1888                     taskView.onTaskListVisibilityChanged(false /* visible */, dataChanges);
1889                 }
1890                 mHasVisibleTaskData.delete(task.key.id);
1891             }
1892         }
1893     }
1894 
1895     /**
1896      * Unloads any associated data from the currently visible tasks
1897      */
unloadVisibleTaskData(@askView.TaskDataChanges int dataChanges)1898     private void unloadVisibleTaskData(@TaskView.TaskDataChanges int dataChanges) {
1899         for (int i = 0; i < mHasVisibleTaskData.size(); i++) {
1900             if (mHasVisibleTaskData.valueAt(i)) {
1901                 TaskView taskView = getTaskViewByTaskId(mHasVisibleTaskData.keyAt(i));
1902                 if (taskView != null) {
1903                     taskView.onTaskListVisibilityChanged(false /* visible */, dataChanges);
1904                 }
1905             }
1906         }
1907         mHasVisibleTaskData.clear();
1908     }
1909 
1910     @Override
onHighResLoadingStateChanged(boolean enabled)1911     public void onHighResLoadingStateChanged(boolean enabled) {
1912         // Whenever the high res loading state changes, poke each of the visible tasks to see if
1913         // they want to updated their thumbnail state
1914         for (int i = 0; i < mHasVisibleTaskData.size(); i++) {
1915             if (mHasVisibleTaskData.valueAt(i)) {
1916                 TaskView taskView = getTaskViewByTaskId(mHasVisibleTaskData.keyAt(i));
1917                 if (taskView != null) {
1918                     // Poke the view again, which will trigger it to load high res if the state
1919                     // is enabled
1920                     taskView.onTaskListVisibilityChanged(true /* visible */);
1921                 }
1922             }
1923         }
1924     }
1925 
startHome()1926     public abstract void startHome();
1927 
reset()1928     public void reset() {
1929         setCurrentTask(-1);
1930         mCurrentPageScrollDiff = 0;
1931         mIgnoreResetTaskId = -1;
1932         mTaskListChangeId = -1;
1933         mFocusedTaskViewId = -1;
1934 
1935         if (mRecentsAnimationController != null) {
1936             if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile) {
1937                 // We are still drawing the live tile, finish it now to clean up.
1938                 finishRecentsAnimation(true /* toRecents */, null);
1939             } else {
1940                 mRecentsAnimationController = null;
1941             }
1942         }
1943         setEnableDrawingLiveTile(false);
1944         runActionOnRemoteHandles(remoteTargetHandle -> {
1945             remoteTargetHandle.getTransformParams().setTargetSet(null);
1946             remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(false);
1947         });
1948         resetFromSplitSelectionState();
1949         mSplitSelectStateController.resetState();
1950 
1951         // These are relatively expensive and don't need to be done this frame (RecentsView isn't
1952         // visible anyway), so defer by a frame to get off the critical path, e.g. app to home.
1953         post(() -> {
1954             unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
1955             setCurrentPage(0);
1956             LayoutUtils.setViewEnabled(mActionsView, true);
1957             if (mOrientationState.setGestureActive(false)) {
1958                 updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false);
1959             }
1960         });
1961     }
1962 
getRunningTaskViewId()1963     public int getRunningTaskViewId() {
1964         return mRunningTaskViewId;
1965     }
1966 
getTaskIdsForRunningTaskView()1967     protected int[] getTaskIdsForRunningTaskView() {
1968         return getTaskIdsForTaskViewId(mRunningTaskViewId);
1969     }
1970 
getTaskIdsForTaskViewId(int taskViewId)1971     private int[] getTaskIdsForTaskViewId(int taskViewId) {
1972         // For now 2 distinct task IDs is max for split screen
1973         TaskView runningTaskView = getTaskViewFromTaskViewId(taskViewId);
1974         if (runningTaskView == null) {
1975             return INVALID_TASK_IDS;
1976         }
1977 
1978         return runningTaskView.getTaskIds();
1979     }
1980 
getRunningTaskView()1981     public @Nullable TaskView getRunningTaskView() {
1982         return getTaskViewFromTaskViewId(mRunningTaskViewId);
1983     }
1984 
getFocusedTaskView()1985     public @Nullable TaskView getFocusedTaskView() {
1986         return getTaskViewFromTaskViewId(mFocusedTaskViewId);
1987     }
1988 
1989     @Nullable
getTaskViewFromTaskViewId(int taskViewId)1990     private TaskView getTaskViewFromTaskViewId(int taskViewId) {
1991         if (taskViewId == -1) {
1992             return null;
1993         }
1994 
1995         for (int i = 0; i < getTaskViewCount(); i++) {
1996             TaskView taskView = requireTaskViewAt(i);
1997             if (taskView.getTaskViewId() == taskViewId) {
1998                 return taskView;
1999             }
2000         }
2001         return null;
2002     }
2003 
getRunningTaskIndex()2004     public int getRunningTaskIndex() {
2005         TaskView taskView = getRunningTaskView();
2006         return taskView == null ? -1 : indexOfChild(taskView);
2007     }
2008 
getHomeTaskView()2009     protected @Nullable TaskView getHomeTaskView() {
2010         return null;
2011     }
2012 
2013     /**
2014      * Handle the edge case where Recents could increment task count very high over long
2015      * period of device usage. Probably will never happen, but meh.
2016      */
getTaskViewFromPool(boolean isGrouped)2017     private <T extends TaskView> T getTaskViewFromPool(boolean isGrouped) {
2018         T taskView = isGrouped ?
2019                 (T) mGroupedTaskViewPool.getView() :
2020                 (T) mTaskViewPool.getView();
2021         taskView.setTaskViewId(mTaskViewIdCount);
2022         if (mTaskViewIdCount == Integer.MAX_VALUE) {
2023             mTaskViewIdCount = 0;
2024         } else {
2025             mTaskViewIdCount++;
2026         }
2027 
2028         return taskView;
2029     }
2030 
2031     /**
2032      * Get the index of the task view whose id matches {@param taskId}.
2033      * @return -1 if there is no task view for the task id, else the index of the task view.
2034      */
getTaskIndexForId(int taskId)2035     public int getTaskIndexForId(int taskId) {
2036         TaskView tv = getTaskViewByTaskId(taskId);
2037         return tv == null ? -1 : indexOfChild(tv);
2038     }
2039 
2040     /**
2041      * Reloads the view if anything in recents changed.
2042      */
reloadIfNeeded()2043     public void reloadIfNeeded() {
2044         if (!mModel.isTaskListValid(mTaskListChangeId)) {
2045             mTaskListChangeId = mModel.getTasks(this::applyLoadPlan);
2046         }
2047     }
2048 
2049     /**
2050      * Called when a gesture from an app is starting.
2051      */
onGestureAnimationStart(RunningTaskInfo[] runningTaskInfo)2052     public void onGestureAnimationStart(RunningTaskInfo[] runningTaskInfo) {
2053         mGestureActive = true;
2054         // This needs to be called before the other states are set since it can create the task view
2055         if (mOrientationState.setGestureActive(true)) {
2056             updateOrientationHandler();
2057         }
2058 
2059         showCurrentTask(runningTaskInfo);
2060         setEnableFreeScroll(false);
2061         setEnableDrawingLiveTile(false);
2062         setRunningTaskHidden(true);
2063         setTaskIconScaledDown(true);
2064     }
2065 
2066     /**
2067      * Called only when a swipe-up gesture from an app has completed. Only called after
2068      * {@link #onGestureAnimationStart} and {@link #onGestureAnimationEnd()}.
2069      */
onSwipeUpAnimationSuccess()2070     public void onSwipeUpAnimationSuccess() {
2071         animateUpTaskIconScale();
2072         setSwipeDownShouldLaunchApp(true);
2073     }
2074 
animateRecentsRotationInPlace(int newRotation)2075     private void animateRecentsRotationInPlace(int newRotation) {
2076         if (mOrientationState.isRecentsActivityRotationAllowed()) {
2077             // Let system take care of the rotation
2078             return;
2079         }
2080         AnimatorSet pa = setRecentsChangedOrientation(true);
2081         pa.addListener(AnimatorListeners.forSuccessCallback(() -> {
2082             setLayoutRotation(newRotation, mOrientationState.getDisplayRotation());
2083             mActivity.getDragLayer().recreateControllers();
2084             setRecentsChangedOrientation(false).start();
2085         }));
2086         pa.start();
2087     }
2088 
setRecentsChangedOrientation(boolean fadeInChildren)2089     public AnimatorSet setRecentsChangedOrientation(boolean fadeInChildren) {
2090         getRunningTaskIndex();
2091         int runningIndex = getCurrentPage();
2092         AnimatorSet as = new AnimatorSet();
2093         for (int i = 0; i < getTaskViewCount(); i++) {
2094             View taskView = requireTaskViewAt(i);
2095             if (runningIndex == i && taskView.getAlpha() != 0) {
2096                 continue;
2097             }
2098             as.play(ObjectAnimator.ofFloat(taskView, View.ALPHA, fadeInChildren ? 0 : 1));
2099         }
2100         return as;
2101     }
2102 
updateChildTaskOrientations()2103     private void updateChildTaskOrientations() {
2104         for (int i = 0; i < getTaskViewCount(); i++) {
2105             requireTaskViewAt(i).setOrientationState(mOrientationState);
2106         }
2107         TaskMenuView tv = (TaskMenuView) getTopOpenViewWithType(mActivity, TYPE_TASK_MENU);
2108         if (tv != null) {
2109             tv.onRotationChanged();
2110         }
2111     }
2112 
2113     /**
2114      * Called when a gesture from an app has finished, and an end target has been determined.
2115      */
onPrepareGestureEndAnimation( @ullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget, TaskViewSimulator[] taskViewSimulators)2116     public void onPrepareGestureEndAnimation(
2117             @Nullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget,
2118             TaskViewSimulator[] taskViewSimulators) {
2119         mCurrentGestureEndTarget = endTarget;
2120         if (endTarget == GestureState.GestureEndTarget.RECENTS) {
2121             updateGridProperties();
2122         }
2123 
2124         if (mSizeStrategy.stateFromGestureEndTarget(endTarget)
2125                 .displayOverviewTasksAsGrid(mActivity.getDeviceProfile())) {
2126             TaskView runningTaskView = getRunningTaskView();
2127             float runningTaskPrimaryGridTranslation = 0;
2128             if (runningTaskView != null) {
2129                 // Apply the grid translation to running task unless it's being snapped to
2130                 // and removes the current translation applied to the running task.
2131                 runningTaskPrimaryGridTranslation = mOrientationHandler.getPrimaryValue(
2132                         runningTaskView.getGridTranslationX(),
2133                         runningTaskView.getGridTranslationY())
2134                         - runningTaskView.getPrimaryNonGridTranslationProperty().get(
2135                         runningTaskView);
2136             }
2137             for (TaskViewSimulator tvs : taskViewSimulators) {
2138                 if (animatorSet == null) {
2139                     setGridProgress(1);
2140                     tvs.taskPrimaryTranslation.value =
2141                             runningTaskPrimaryGridTranslation;
2142                 } else {
2143                     animatorSet.play(ObjectAnimator.ofFloat(this, RECENTS_GRID_PROGRESS, 1));
2144                     animatorSet.play(tvs.taskPrimaryTranslation.animateToValue(
2145                             runningTaskPrimaryGridTranslation));
2146                 }
2147             }
2148         }
2149     }
2150 
2151     /**
2152      * Called when a gesture from an app has finished, and the animation to the target has ended.
2153      */
onGestureAnimationEnd()2154     public void onGestureAnimationEnd() {
2155         mGestureActive = false;
2156         if (mOrientationState.setGestureActive(false)) {
2157             updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false);
2158         }
2159 
2160         setEnableFreeScroll(true);
2161         setEnableDrawingLiveTile(mCurrentGestureEndTarget == GestureState.GestureEndTarget.RECENTS);
2162         if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
2163             setRunningTaskViewShowScreenshot(true);
2164         }
2165         setRunningTaskHidden(false);
2166         animateUpTaskIconScale();
2167         animateActionsViewIn();
2168 
2169         mCurrentGestureEndTarget = null;
2170     }
2171 
2172     /**
2173      * Returns true if we should add a stub taskView for the running task id
2174      */
shouldAddStubTaskView(RunningTaskInfo[] runningTaskInfos)2175     protected boolean shouldAddStubTaskView(RunningTaskInfo[] runningTaskInfos) {
2176         if (runningTaskInfos.length > 1) {
2177             TaskView primaryTaskView = getTaskViewByTaskId(runningTaskInfos[0].taskId);
2178             TaskView secondaryTaskView = getTaskViewByTaskId(runningTaskInfos[1].taskId);
2179             int leftTopTaskViewId =
2180                     (primaryTaskView == null) ? -1 : primaryTaskView.getTaskViewId();
2181             int rightBottomTaskViewId =
2182                     (secondaryTaskView == null) ? -1 : secondaryTaskView.getTaskViewId();
2183             // Add a new stub view if both taskIds don't match any taskViews
2184             return leftTopTaskViewId != rightBottomTaskViewId || leftTopTaskViewId == -1;
2185         }
2186         RunningTaskInfo runningTaskInfo = runningTaskInfos[0];
2187         return runningTaskInfo != null && getTaskViewByTaskId(runningTaskInfo.taskId) == null;
2188     }
2189 
2190     /**
2191      * Creates a task view (if necessary) to represent the task with the {@param runningTaskId}.
2192      *
2193      * All subsequent calls to reload will keep the task as the first item until {@link #reset()}
2194      * is called.  Also scrolls the view to this task.
2195      */
showCurrentTask(RunningTaskInfo[] runningTaskInfo)2196     private void showCurrentTask(RunningTaskInfo[] runningTaskInfo) {
2197         int runningTaskViewId = -1;
2198         boolean needGroupTaskView = runningTaskInfo.length > 1;
2199         RunningTaskInfo taskInfo = runningTaskInfo[0];
2200         if (shouldAddStubTaskView(runningTaskInfo)) {
2201             boolean wasEmpty = getChildCount() == 0;
2202             // Add an empty view for now until the task plan is loaded and applied
2203             final TaskView taskView;
2204             if (needGroupTaskView) {
2205                 taskView = getTaskViewFromPool(true);
2206                 RunningTaskInfo secondaryTaskInfo = runningTaskInfo[1];
2207                 mTmpRunningTasks = new Task[]{
2208                         Task.from(new TaskKey(taskInfo), taskInfo, false),
2209                         Task.from(new TaskKey(secondaryTaskInfo), secondaryTaskInfo, false)
2210                 };
2211                 addView(taskView, 0);
2212                 // When we create a placeholder task view mSplitBoundsConfig will be null, but with
2213                 // the actual app running we won't need to show the thumbnail until all the tasks
2214                 // load later anyways
2215                 ((GroupedTaskView)taskView).bind(mTmpRunningTasks[0], mTmpRunningTasks[1],
2216                         mOrientationState, mSplitBoundsConfig);
2217             } else {
2218                 taskView = getTaskViewFromPool(false);
2219                 addView(taskView, 0);
2220                 // The temporary running task is only used for the duration between the start of the
2221                 // gesture and the task list is loaded and applied
2222                 mTmpRunningTasks = new Task[]{Task.from(new TaskKey(taskInfo), taskInfo, false)};
2223                 taskView.bind(mTmpRunningTasks[0], mOrientationState);
2224             }
2225             runningTaskViewId = taskView.getTaskViewId();
2226             if (wasEmpty) {
2227                 addView(mClearAllButton);
2228             }
2229 
2230             // Measure and layout immediately so that the scroll values is updated instantly
2231             // as the user might be quick-switching
2232             measure(makeMeasureSpec(getMeasuredWidth(), EXACTLY),
2233                     makeMeasureSpec(getMeasuredHeight(), EXACTLY));
2234             layout(getLeft(), getTop(), getRight(), getBottom());
2235         } else if (getTaskViewByTaskId(taskInfo.taskId) != null) {
2236             runningTaskViewId = getTaskViewByTaskId(taskInfo.taskId).getTaskViewId();
2237         }
2238 
2239         boolean runningTaskTileHidden = mRunningTaskTileHidden;
2240         setCurrentTask(runningTaskViewId);
2241         mFocusedTaskViewId = runningTaskViewId;
2242         setCurrentPage(getRunningTaskIndex());
2243         setRunningTaskViewShowScreenshot(false);
2244         setRunningTaskHidden(runningTaskTileHidden);
2245         // Update task size after setting current task.
2246         updateTaskSize();
2247         updateChildTaskOrientations();
2248 
2249         // Reload the task list
2250         reloadIfNeeded();
2251     }
2252 
2253     /**
2254      * Sets the running task id, cleaning up the old running task if necessary.
2255      */
setCurrentTask(int runningTaskViewId)2256     public void setCurrentTask(int runningTaskViewId) {
2257         Log.d(TASK_VIEW_ID_CRASH, "currentRunningTaskViewId: " + mRunningTaskViewId
2258                 + " requestedTaskViewId: " + runningTaskViewId);
2259         if (mRunningTaskViewId == runningTaskViewId) {
2260             return;
2261         }
2262 
2263         if (mRunningTaskViewId != -1) {
2264             // Reset the state on the old running task view
2265             setTaskIconScaledDown(false);
2266             setRunningTaskViewShowScreenshot(true);
2267             setRunningTaskHidden(false);
2268         }
2269         mRunningTaskViewId = runningTaskViewId;
2270     }
2271 
getTaskViewIdFromTaskId(int taskId)2272     private int getTaskViewIdFromTaskId(int taskId) {
2273         TaskView taskView = getTaskViewByTaskId(taskId);
2274         return taskView != null ? taskView.getTaskViewId() : -1;
2275     }
2276 
2277     /**
2278      * Hides the tile associated with {@link #mRunningTaskViewId}
2279      */
setRunningTaskHidden(boolean isHidden)2280     public void setRunningTaskHidden(boolean isHidden) {
2281         mRunningTaskTileHidden = isHidden;
2282         TaskView runningTask = getRunningTaskView();
2283         if (runningTask != null) {
2284             runningTask.setStableAlpha(isHidden ? 0 : mContentAlpha);
2285             if (!isHidden) {
2286                 AccessibilityManagerCompat.sendCustomAccessibilityEvent(runningTask,
2287                         AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
2288             }
2289         }
2290     }
2291 
setRunningTaskViewShowScreenshot(boolean showScreenshot)2292     private void setRunningTaskViewShowScreenshot(boolean showScreenshot) {
2293         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
2294             mRunningTaskShowScreenshot = showScreenshot;
2295             TaskView runningTaskView = getRunningTaskView();
2296             if (runningTaskView != null) {
2297                 runningTaskView.setShowScreenshot(mRunningTaskShowScreenshot);
2298             }
2299         }
2300     }
2301 
setTaskIconScaledDown(boolean isScaledDown)2302     public void setTaskIconScaledDown(boolean isScaledDown) {
2303         if (mTaskIconScaledDown != isScaledDown) {
2304             mTaskIconScaledDown = isScaledDown;
2305             int taskCount = getTaskViewCount();
2306             for (int i = 0; i < taskCount; i++) {
2307                 requireTaskViewAt(i).setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
2308             }
2309         }
2310     }
2311 
animateActionsViewIn()2312     private void animateActionsViewIn() {
2313         ObjectAnimator anim = ObjectAnimator.ofFloat(
2314                 mActionsView.getVisibilityAlpha(), MultiValueAlpha.VALUE, 0, 1);
2315         anim.setDuration(TaskView.SCALE_ICON_DURATION);
2316         anim.start();
2317     }
2318 
animateUpTaskIconScale()2319     public void animateUpTaskIconScale() {
2320         mTaskIconScaledDown = false;
2321         int taskCount = getTaskViewCount();
2322         for (int i = 0; i < taskCount; i++) {
2323             TaskView taskView = requireTaskViewAt(i);
2324             taskView.setIconScaleAnimStartProgress(0f);
2325             taskView.animateIconScaleAndDimIntoView();
2326         }
2327     }
2328 
2329     /**
2330      * Updates TaskView and ClearAllButtion scaling and translation required to turn into grid
2331      * layout.
2332      * This method is used when no task dismissal has occurred.
2333      */
updateGridProperties()2334     private void updateGridProperties() {
2335         updateGridProperties(false, Integer.MAX_VALUE);
2336     }
2337 
2338     /**
2339      * Updates TaskView and ClearAllButtion scaling and translation required to turn into grid
2340      * layout.
2341      *
2342      * This method is used when task dismissal has occurred, but rebalance is not needed.
2343      *
2344      * @param isTaskDismissal indicates if update was called due to task dismissal
2345      */
updateGridProperties(boolean isTaskDismissal)2346     private void updateGridProperties(boolean isTaskDismissal) {
2347         updateGridProperties(isTaskDismissal, Integer.MAX_VALUE);
2348     }
2349 
2350     /**
2351      * Updates TaskView and ClearAllButton scaling and translation required to turn into grid
2352      * layout.
2353      *
2354      * This method only calculates the potential position and depends on {@link #setGridProgress} to
2355      * apply the actual scaling and translation.
2356      *
2357      * @param isTaskDismissal    indicates if update was called due to task dismissal
2358      * @param startRebalanceAfter which view index to start rebalancing from. Use Integer.MAX_VALUE
2359      *                           to skip rebalance
2360      */
updateGridProperties(boolean isTaskDismissal, int startRebalanceAfter)2361     private void updateGridProperties(boolean isTaskDismissal, int startRebalanceAfter) {
2362         int taskCount = getTaskViewCount();
2363         if (taskCount == 0) {
2364             return;
2365         }
2366 
2367         int taskTopMargin = mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx;
2368 
2369         int topRowWidth = 0;
2370         int bottomRowWidth = 0;
2371         float topAccumulatedTranslationX = 0;
2372         float bottomAccumulatedTranslationX = 0;
2373 
2374         // Contains whether the child index is in top or bottom of grid (for non-focused task)
2375         // Different from mTopRowIdSet, which contains the taskViewId of what task is in top row
2376         IntSet topSet = new IntSet();
2377         IntSet bottomSet = new IntSet();
2378 
2379         // Horizontal grid translation for each task
2380         float[] gridTranslations = new float[taskCount];
2381 
2382         int focusedTaskIndex = Integer.MAX_VALUE;
2383         int focusedTaskShift = 0;
2384         int focusedTaskWidthAndSpacing = 0;
2385         int snappedTaskRowWidth = 0;
2386         int snappedPage = getNextPage();
2387         TaskView snappedTaskView = getTaskViewAt(snappedPage);
2388         TaskView homeTaskView = getHomeTaskView();
2389         TaskView nextFocusedTaskView = null;
2390 
2391         if (!isTaskDismissal) {
2392             mTopRowIdSet.clear();
2393         }
2394         for (int i = 0; i < taskCount; i++) {
2395             TaskView taskView = requireTaskViewAt(i);
2396             int taskWidthAndSpacing = taskView.getLayoutParams().width + mPageSpacing;
2397             // Evenly distribute tasks between rows unless rearranging due to task dismissal, in
2398             // which case keep tasks in their respective rows. For the running task, don't join
2399             // the grid.
2400             if (taskView.isFocusedTask()) {
2401                 topRowWidth += taskWidthAndSpacing;
2402                 bottomRowWidth += taskWidthAndSpacing;
2403 
2404                 focusedTaskIndex = i;
2405                 focusedTaskWidthAndSpacing = taskWidthAndSpacing;
2406                 gridTranslations[i] += focusedTaskShift;
2407                 gridTranslations[i] += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
2408 
2409                 // Center view vertically in case it's from different orientation.
2410                 taskView.setGridTranslationY((mLastComputedTaskSize.height() + taskTopMargin
2411                         - taskView.getLayoutParams().height) / 2f);
2412 
2413                 if (taskView == snappedTaskView) {
2414                     // If focused task is snapped, the row width is just task width and spacing.
2415                     snappedTaskRowWidth = taskWidthAndSpacing;
2416                 }
2417             } else {
2418                 if (i > focusedTaskIndex) {
2419                     // For tasks after the focused task, shift by focused task's width and spacing.
2420                     gridTranslations[i] +=
2421                             mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing;
2422                 } else {
2423                     // For task before the focused task, accumulate the width and spacing to
2424                     // calculate the distance focused task need to shift.
2425                     focusedTaskShift += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
2426                 }
2427                 int taskViewId = taskView.getTaskViewId();
2428 
2429                 // Rebalance the grid starting after a certain index
2430                 boolean isTopRow;
2431                 if (isTaskDismissal) {
2432                     if (i > startRebalanceAfter) {
2433                         mTopRowIdSet.remove(taskViewId);
2434                         isTopRow = topRowWidth <= bottomRowWidth;
2435                     } else {
2436                         isTopRow = mTopRowIdSet.contains(taskViewId);
2437                     }
2438                 } else {
2439                     isTopRow = topRowWidth <= bottomRowWidth;
2440                 }
2441 
2442                 if (isTopRow) {
2443                     if (homeTaskView != null && nextFocusedTaskView == null) {
2444                         // TaskView will be focused when swipe up, don't count towards row width.
2445                         nextFocusedTaskView = taskView;
2446                     } else {
2447                         topRowWidth += taskWidthAndSpacing;
2448                     }
2449                     topSet.add(i);
2450                     mTopRowIdSet.add(taskViewId);
2451 
2452                     taskView.setGridTranslationY(mTaskGridVerticalDiff);
2453 
2454                     // Move horizontally into empty space.
2455                     float widthOffset = 0;
2456                     for (int j = i - 1; !topSet.contains(j) && j >= 0; j--) {
2457                         if (j == focusedTaskIndex) {
2458                             continue;
2459                         }
2460                         widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing;
2461                     }
2462 
2463                     float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset;
2464                     gridTranslations[i] += topAccumulatedTranslationX + currentTaskTranslationX;
2465                     topAccumulatedTranslationX += currentTaskTranslationX;
2466                 } else {
2467                     bottomRowWidth += taskWidthAndSpacing;
2468                     bottomSet.add(i);
2469 
2470                     // Move into bottom row.
2471                     taskView.setGridTranslationY(mTopBottomRowHeightDiff + mTaskGridVerticalDiff);
2472 
2473                     // Move horizontally into empty space.
2474                     float widthOffset = 0;
2475                     for (int j = i - 1; !bottomSet.contains(j) && j >= 0; j--) {
2476                         if (j == focusedTaskIndex) {
2477                             continue;
2478                         }
2479                         widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing;
2480                     }
2481 
2482                     float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset;
2483                     gridTranslations[i] += bottomAccumulatedTranslationX + currentTaskTranslationX;
2484                     bottomAccumulatedTranslationX += currentTaskTranslationX;
2485                 }
2486                 if (taskView == snappedTaskView) {
2487                     snappedTaskRowWidth = isTopRow ? topRowWidth : bottomRowWidth;
2488                 }
2489             }
2490         }
2491 
2492         // We need to maintain snapped task's page scroll invariant between quick switch and
2493         // overview, so we sure snapped task's grid translation is 0, and add a non-fullscreen
2494         // translationX that is the same as snapped task's full scroll adjustment.
2495         float snappedTaskNonGridScrollAdjustment = 0;
2496         float snappedTaskGridTranslationX = 0;
2497         if (snappedTaskView != null) {
2498             snappedTaskNonGridScrollAdjustment = snappedTaskView.getScrollAdjustment(
2499                     /*fullscreenEnabled=*/true, /*gridEnabled=*/false);
2500             snappedTaskGridTranslationX = gridTranslations[snappedPage];
2501         }
2502 
2503         // Use the accumulated translation of the row containing the last task.
2504         float clearAllAccumulatedTranslation = topSet.contains(taskCount - 1)
2505                 ? topAccumulatedTranslationX : bottomAccumulatedTranslationX;
2506 
2507         // If the last task is on the shorter row, ClearAllButton will embed into the shorter row
2508         // which is not what we want. Compensate the width difference of the 2 rows in that case.
2509         float shorterRowCompensation = 0;
2510         if (topRowWidth <= bottomRowWidth) {
2511             if (topSet.contains(taskCount - 1)) {
2512                 shorterRowCompensation = bottomRowWidth - topRowWidth;
2513             }
2514         } else {
2515             if (bottomSet.contains(taskCount - 1)) {
2516                 shorterRowCompensation = topRowWidth - bottomRowWidth;
2517             }
2518         }
2519         float clearAllShorterRowCompensation =
2520                 mIsRtl ? -shorterRowCompensation : shorterRowCompensation;
2521 
2522         // If the total width is shorter than one grid's width, move ClearAllButton further away
2523         // accordingly. Update longRowWidth if ClearAllButton has been moved.
2524         float clearAllShortTotalCompensation = 0;
2525         int longRowWidth = Math.max(topRowWidth, bottomRowWidth);
2526         if (longRowWidth < mLastComputedGridSize.width()) {
2527             float shortTotalCompensation = mLastComputedGridSize.width() - longRowWidth;
2528             clearAllShortTotalCompensation =
2529                     mIsRtl ? -shortTotalCompensation : shortTotalCompensation;
2530             longRowWidth = mLastComputedGridSize.width();
2531         }
2532 
2533         float clearAllTotalTranslationX =
2534                 clearAllAccumulatedTranslation + clearAllShorterRowCompensation
2535                         + clearAllShortTotalCompensation + snappedTaskNonGridScrollAdjustment;
2536         if (focusedTaskIndex < taskCount) {
2537             // Shift by focused task's width and spacing if a task is focused.
2538             clearAllTotalTranslationX +=
2539                     mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing;
2540         }
2541 
2542         // Make sure there are enough space between snapped page and ClearAllButton, for the case
2543         // of swiping up after quick switch.
2544         if (snappedTaskView != null) {
2545             int distanceFromClearAll = longRowWidth - snappedTaskRowWidth + mPageSpacing;
2546             // ClearAllButton should be off screen when snapped task is in its snapped position.
2547             int minimumDistance =
2548                     mTaskWidth - snappedTaskView.getLayoutParams().width
2549                             + (mLastComputedGridSize.width() - mTaskWidth) / 2;
2550             if (distanceFromClearAll < minimumDistance) {
2551                 int distanceDifference = minimumDistance - distanceFromClearAll;
2552                 snappedTaskGridTranslationX += mIsRtl ? distanceDifference : -distanceDifference;
2553             }
2554         }
2555 
2556         for (int i = 0; i < taskCount; i++) {
2557             TaskView taskView = requireTaskViewAt(i);
2558             taskView.setGridTranslationX(gridTranslations[i] - snappedTaskGridTranslationX
2559                     + snappedTaskNonGridScrollAdjustment);
2560         }
2561 
2562         mClearAllButton.setGridTranslationPrimary(
2563                 clearAllTotalTranslationX - snappedTaskGridTranslationX);
2564         mClearAllButton.setGridScrollOffset(
2565                 mIsRtl ? mLastComputedTaskSize.left - mLastComputedGridSize.left
2566                         : mLastComputedTaskSize.right - mLastComputedGridSize.right);
2567 
2568         setGridProgress(mGridProgress);
2569     }
2570 
isSameGridRow(TaskView taskView1, TaskView taskView2)2571     private boolean isSameGridRow(TaskView taskView1, TaskView taskView2) {
2572         if (taskView1 == null || taskView2 == null) {
2573             return false;
2574         }
2575         int taskViewId1 = taskView1.getTaskViewId();
2576         int taskViewId2 = taskView2.getTaskViewId();
2577         if (taskViewId1 == mFocusedTaskViewId || taskViewId2 == mFocusedTaskViewId) {
2578             return false;
2579         }
2580         return (mTopRowIdSet.contains(taskViewId1) && mTopRowIdSet.contains(taskViewId2)) || (
2581                 !mTopRowIdSet.contains(taskViewId1) && !mTopRowIdSet.contains(taskViewId2));
2582     }
2583 
2584     /**
2585      * Moves TaskView and ClearAllButton between carousel and 2 row grid.
2586      *
2587      * @param gridProgress 0 = carousel; 1 = 2 row grid.
2588      */
setGridProgress(float gridProgress)2589     private void setGridProgress(float gridProgress) {
2590         int taskCount = getTaskViewCount();
2591         if (taskCount == 0) {
2592             return;
2593         }
2594 
2595         mGridProgress = gridProgress;
2596 
2597         for (int i = 0; i < taskCount; i++) {
2598             requireTaskViewAt(i).setGridProgress(gridProgress);
2599         }
2600         mClearAllButton.setGridProgress(gridProgress);
2601     }
2602 
enableLayoutTransitions()2603     private void enableLayoutTransitions() {
2604         if (mLayoutTransition == null) {
2605             mLayoutTransition = new LayoutTransition();
2606             mLayoutTransition.enableTransitionType(LayoutTransition.APPEARING);
2607             mLayoutTransition.setDuration(ADDITION_TASK_DURATION);
2608             mLayoutTransition.setStartDelay(LayoutTransition.APPEARING, 0);
2609 
2610             mLayoutTransition.addTransitionListener(new TransitionListener() {
2611                 @Override
2612                 public void startTransition(LayoutTransition transition, ViewGroup viewGroup,
2613                     View view, int i) {
2614                 }
2615 
2616                 @Override
2617                 public void endTransition(LayoutTransition transition, ViewGroup viewGroup,
2618                     View view, int i) {
2619                     // When the unpinned task is added, snap to first page and disable transitions
2620                     if (view instanceof TaskView) {
2621                         snapToPage(0);
2622                         setLayoutTransition(null);
2623                     }
2624 
2625                 }
2626             });
2627         }
2628         setLayoutTransition(mLayoutTransition);
2629     }
2630 
setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp)2631     public void setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp) {
2632         mSwipeDownShouldLaunchApp = swipeDownShouldLaunchApp;
2633     }
2634 
shouldSwipeDownLaunchApp()2635     public boolean shouldSwipeDownLaunchApp() {
2636         return mSwipeDownShouldLaunchApp;
2637     }
2638 
setIgnoreResetTask(int taskId)2639     public void setIgnoreResetTask(int taskId) {
2640         mIgnoreResetTaskId = taskId;
2641     }
2642 
clearIgnoreResetTask(int taskId)2643     public void clearIgnoreResetTask(int taskId) {
2644         if (mIgnoreResetTaskId == taskId) {
2645             mIgnoreResetTaskId = -1;
2646         }
2647     }
2648 
addDismissedTaskAnimations(TaskView taskView, long duration, PendingAnimation anim)2649     private void addDismissedTaskAnimations(TaskView taskView, long duration,
2650             PendingAnimation anim) {
2651         // Use setFloat instead of setViewAlpha as we want to keep the view visible even when it's
2652         // alpha is set to 0 so that it can be recycled in the view pool properly
2653         if (ENABLE_QUICKSTEP_LIVE_TILE.get() && taskView.isRunningTask()) {
2654             runActionOnRemoteHandles(remoteTargetHandle -> {
2655                 TransformParams params = remoteTargetHandle.getTransformParams();
2656                 anim.setFloat(params, TransformParams.TARGET_ALPHA, 0,
2657                         clampToProgress(FINAL_FRAME, 0, 0.5f));
2658             });
2659         }
2660         anim.setFloat(taskView, VIEW_ALPHA, 0,
2661                 clampToProgress(isOnGridBottomRow(taskView) ? ACCEL : FINAL_FRAME, 0, 0.5f));
2662         FloatProperty<TaskView> secondaryViewTranslate =
2663                 taskView.getSecondaryDissmissTranslationProperty();
2664         int secondaryTaskDimension = mOrientationHandler.getSecondaryDimension(taskView);
2665         int verticalFactor = mOrientationHandler.getSecondaryTranslationDirectionFactor();
2666 
2667         ResourceProvider rp = DynamicResource.provider(mActivity);
2668         SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_START)
2669                 .setDampingRatio(rp.getFloat(R.dimen.dismiss_task_trans_y_damping_ratio))
2670                 .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_y_stiffness));
2671 
2672         anim.add(ObjectAnimator.ofFloat(taskView, secondaryViewTranslate,
2673                 verticalFactor * secondaryTaskDimension * 2).setDuration(duration), LINEAR, sp);
2674 
2675         if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
2676                 && taskView.isRunningTask()) {
2677             anim.addOnFrameCallback(() -> {
2678                 runActionOnRemoteHandles(
2679                         remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
2680                                 .taskSecondaryTranslation.value = mOrientationHandler
2681                                 .getSecondaryValue(taskView.getTranslationX(),
2682                                         taskView.getTranslationY()
2683                                 ));
2684                 redrawLiveTile();
2685             });
2686         }
2687     }
2688 
2689     /**
2690      * Places an {@link FloatingTaskView} on top of the thumbnail for {@link #mSplitHiddenTaskView}
2691      * and then animates it into the split position that was desired
2692      */
createInitialSplitSelectAnimation(PendingAnimation anim)2693     private void createInitialSplitSelectAnimation(PendingAnimation anim) {
2694         mOrientationHandler.getInitialSplitPlaceholderBounds(mSplitPlaceholderSize,
2695                 mActivity.getDeviceProfile(),
2696                 mSplitSelectStateController.getActiveSplitStagePosition(), mTempRect);
2697 
2698         RectF startingTaskRect = new RectF();
2699         mSplitHiddenTaskView.setVisibility(INVISIBLE);
2700         mFirstFloatingTaskView = FloatingTaskView.getFloatingTaskView(mActivity,
2701                 mSplitHiddenTaskView, startingTaskRect);
2702         mFirstFloatingTaskView.setAlpha(1);
2703         mFirstFloatingTaskView.addAnimation(anim, startingTaskRect,
2704                 mTempRect, mSplitHiddenTaskView, true /*fadeWithThumbnail*/);
2705         anim.addEndListener(success -> {
2706             if (success) {
2707                 mSplitToast.show();
2708             }
2709         });
2710     }
2711 
2712     /**
2713      * Creates a {@link PendingAnimation} for dismissing the specified {@link TaskView}.
2714      * @param dismissedTaskView the {@link TaskView} to be dismissed
2715      * @param animateTaskView whether the {@link TaskView} to be dismissed should be animated
2716      * @param shouldRemoveTask whether the associated {@link Task} should be removed from
2717      *                         ActivityManager after dismissal
2718      * @param duration duration of the animation
2719      * @param dismissingForSplitSelection task dismiss animation is used for entering split
2720      *                                    selection state from app icon
2721      */
createTaskDismissAnimation(TaskView dismissedTaskView, boolean animateTaskView, boolean shouldRemoveTask, long duration, boolean dismissingForSplitSelection)2722     public PendingAnimation createTaskDismissAnimation(TaskView dismissedTaskView,
2723             boolean animateTaskView, boolean shouldRemoveTask, long duration,
2724             boolean dismissingForSplitSelection) {
2725         if (mPendingAnimation != null) {
2726             mPendingAnimation.createPlaybackController().dispatchOnCancel().dispatchOnEnd();
2727         }
2728         PendingAnimation anim = new PendingAnimation(duration);
2729 
2730         int count = getPageCount();
2731         if (count == 0) {
2732             return anim;
2733         }
2734 
2735         boolean showAsGrid = showAsGrid();
2736         int taskCount = getTaskViewCount();
2737         int dismissedIndex = indexOfChild(dismissedTaskView);
2738         int dismissedTaskViewId = dismissedTaskView.getTaskViewId();
2739 
2740         // Grid specific properties.
2741         boolean isFocusedTaskDismissed = false;
2742         TaskView nextFocusedTaskView = null;
2743         boolean nextFocusedTaskFromTop = false;
2744         float dismissedTaskWidth = 0;
2745         float nextFocusedTaskWidth = 0;
2746 
2747         // Non-grid specific properties.
2748         int[] oldScroll = new int[count];
2749         int[] newScroll = new int[count];
2750         int scrollDiffPerPage = 0;
2751         boolean needsCurveUpdates = false;
2752 
2753         if (showAsGrid) {
2754             dismissedTaskWidth = dismissedTaskView.getLayoutParams().width + mPageSpacing;
2755             isFocusedTaskDismissed = dismissedTaskViewId == mFocusedTaskViewId;
2756             if (isFocusedTaskDismissed && !isSplitSelectionActive()) {
2757                 nextFocusedTaskFromTop =
2758                         mTopRowIdSet.size() > 0 && mTopRowIdSet.size() >= (taskCount - 1) / 2f;
2759                 // Pick the next focused task from the preferred row.
2760                 for (int i = 0; i < taskCount; i++) {
2761                     TaskView taskView = requireTaskViewAt(i);
2762                     if (taskView == dismissedTaskView) {
2763                         continue;
2764                     }
2765                     boolean isTopRow = mTopRowIdSet.contains(taskView.getTaskViewId());
2766                     if ((nextFocusedTaskFromTop && isTopRow
2767                             || (!nextFocusedTaskFromTop && !isTopRow))) {
2768                         nextFocusedTaskView = taskView;
2769                         break;
2770                     }
2771                 }
2772                 if (nextFocusedTaskView != null) {
2773                     nextFocusedTaskWidth =
2774                             nextFocusedTaskView.getLayoutParams().width + mPageSpacing;
2775                 }
2776             }
2777         } else {
2778             getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
2779             getPageScrolls(newScroll, false,
2780                     v -> v.getVisibility() != GONE && v != dismissedTaskView);
2781             if (count > 1) {
2782                 scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]);
2783             }
2784         }
2785 
2786         announceForAccessibility(getResources().getString(R.string.task_view_closed));
2787 
2788         float dismissTranslationInterpolationEnd = 1;
2789         boolean closeGapBetweenClearAll = false;
2790         boolean isClearAllHidden = isClearAllHidden();
2791         boolean snapToLastTask = false;
2792         boolean isLandscapeSplit =
2793                 mActivity.getDeviceProfile().isLandscape && isSplitSelectionActive();
2794         boolean isSplitPlaceholderFirstInGrid = isSplitPlaceholderFirstInGrid();
2795         boolean isSplitPlaceholderLastInGrid = isSplitPlaceholderLastInGrid();
2796         TaskView lastGridTaskView = showAsGrid ? getLastGridTaskView() : null;
2797         int currentPageScroll = getScrollForPage(mCurrentPage);
2798         int lastGridTaskScroll = getScrollForPage(indexOfChild(lastGridTaskView));
2799         boolean currentPageSnapsToEndOfGrid = currentPageScroll == lastGridTaskScroll;
2800         if (lastGridTaskView != null && lastGridTaskView.isVisibleToUser()) {
2801             // After dismissal, animate translation of the remaining tasks to fill any gap left
2802             // between the end of the grid and the clear all button. Only animate if the clear
2803             // all button is visible or would become visible after dismissal.
2804             float longGridRowWidthDiff = 0;
2805 
2806             int topGridRowSize = mTopRowIdSet.size();
2807             int bottomGridRowSize = taskCount - mTopRowIdSet.size() - 1;
2808             boolean topRowLonger = topGridRowSize > bottomGridRowSize;
2809             boolean bottomRowLonger = bottomGridRowSize > topGridRowSize;
2810             boolean dismissedTaskFromTop = mTopRowIdSet.contains(dismissedTaskViewId);
2811             boolean dismissedTaskFromBottom = !dismissedTaskFromTop && !isFocusedTaskDismissed;
2812             float gapWidth = 0;
2813             if ((topRowLonger && dismissedTaskFromTop)
2814                     || (bottomRowLonger && dismissedTaskFromBottom)) {
2815                 gapWidth = dismissedTaskWidth;
2816             } else if ((topRowLonger && nextFocusedTaskFromTop)
2817                     || (bottomRowLonger && !nextFocusedTaskFromTop)) {
2818                 gapWidth = nextFocusedTaskWidth;
2819             }
2820             if (gapWidth > 0) {
2821                 if (taskCount > 2) {
2822                     // Compensate the removed gap.
2823                     longGridRowWidthDiff += mIsRtl ? -gapWidth : gapWidth;
2824                     if (isClearAllHidden) {
2825                         // If ClearAllButton isn't fully shown, snap to the last task.
2826                         snapToLastTask = true;
2827                     }
2828                 } else {
2829                     // If only focused task will be left, snap to focused task instead.
2830                     longGridRowWidthDiff += getSnapToFocusedTaskScrollDiff(isClearAllHidden);
2831                 }
2832             }
2833             if (mClearAllButton.getAlpha() != 0f && isLandscapeSplit) {
2834                 // ClearAllButton will not be available in split select, snap to last task instead.
2835                 snapToLastTask = true;
2836             }
2837             if (snapToLastTask) {
2838                 longGridRowWidthDiff += getSnapToLastTaskScrollDiff();
2839                 if (isSplitPlaceholderLastInGrid) {
2840                     // Shift all the tasks to make space for split placeholder.
2841                     longGridRowWidthDiff += mIsRtl ? mSplitPlaceholderSize : -mSplitPlaceholderSize;
2842                 }
2843             } else if (isLandscapeSplit && currentPageSnapsToEndOfGrid) {
2844                 // Use last task as reference point for scroll diff and snapping calculation as it's
2845                 // the only invariant point in landscape split screen.
2846                 snapToLastTask = true;
2847             }
2848 
2849             // If we need to animate the grid to compensate the clear all gap, we split the second
2850             // half of the dismiss pending animation (in which the non-dismissed tasks slide into
2851             // place) in half again, making the first quarter the existing non-dismissal sliding
2852             // and the second quarter this new animation of gap filling. This is due to the fact
2853             // that PendingAnimation is a single animation, not a sequence of animations, so we
2854             // fake it using interpolation.
2855             if (longGridRowWidthDiff != 0) {
2856                 closeGapBetweenClearAll = true;
2857                 // Stagger the offsets of each additional task for a delayed animation. We use
2858                 // half here as this animation is half of half of an animation (1/4th).
2859                 float halfAdditionalDismissTranslationOffset =
2860                         (0.5f * ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET);
2861                 dismissTranslationInterpolationEnd = Utilities.boundToRange(
2862                         END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
2863                                 + (taskCount - 1) * halfAdditionalDismissTranslationOffset,
2864                         END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET, 1);
2865                 for (int i = 0; i < taskCount; i++) {
2866                     TaskView taskView = requireTaskViewAt(i);
2867                     anim.setFloat(taskView, TaskView.GRID_END_TRANSLATION_X, longGridRowWidthDiff,
2868                             clampToProgress(LINEAR, dismissTranslationInterpolationEnd, 1));
2869                     dismissTranslationInterpolationEnd = Utilities.boundToRange(
2870                             dismissTranslationInterpolationEnd
2871                                     - halfAdditionalDismissTranslationOffset,
2872                             END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET, 1);
2873                     if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
2874                             && taskView.isRunningTask()) {
2875                         anim.addOnFrameCallback(() -> {
2876                             runActionOnRemoteHandles(
2877                                     remoteTargetHandle ->
2878                                             remoteTargetHandle.getTaskViewSimulator()
2879                                                     .taskPrimaryTranslation.value =
2880                                                     TaskView.GRID_END_TRANSLATION_X.get(taskView));
2881                             redrawLiveTile();
2882                         });
2883                     }
2884                 }
2885 
2886                 // Change alpha of clear all if translating grid to hide it
2887                 if (isClearAllHidden) {
2888                     anim.setFloat(mClearAllButton, DISMISS_ALPHA, 0, LINEAR);
2889                     anim.addListener(new AnimatorListenerAdapter() {
2890                         @Override
2891                         public void onAnimationEnd(Animator animation) {
2892                             super.onAnimationEnd(animation);
2893                             mClearAllButton.setDismissAlpha(1);
2894                         }
2895                     });
2896                 }
2897             }
2898         }
2899 
2900         int distanceFromDismissedTask = 0;
2901         for (int i = 0; i < count; i++) {
2902             View child = getChildAt(i);
2903             if (child == dismissedTaskView) {
2904                 if (animateTaskView) {
2905                     if (dismissingForSplitSelection) {
2906                         createInitialSplitSelectAnimation(anim);
2907                     } else {
2908                         addDismissedTaskAnimations(dismissedTaskView, duration, anim);
2909                     }
2910                 }
2911             } else if (!showAsGrid) {
2912                 // Compute scroll offsets from task dismissal for animation.
2913                 // If we just take newScroll - oldScroll, everything to the right of dragged task
2914                 // translates to the left. We need to offset this in some cases:
2915                 // - In RTL, add page offset to all pages, since we want pages to move to the right
2916                 // Additionally, add a page offset if:
2917                 // - Current page is rightmost page (leftmost for RTL)
2918                 // - Dragging an adjacent page on the left side (right side for RTL)
2919                 int offset = mIsRtl ? scrollDiffPerPage : 0;
2920                 if (mCurrentPage == dismissedIndex) {
2921                     int lastPage = taskCount - 1;
2922                     if (mCurrentPage == lastPage) {
2923                         offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
2924                     }
2925                 } else {
2926                     // Dismissing an adjacent page.
2927                     int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR)
2928                     if (dismissedIndex == negativeAdjacent) {
2929                         offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
2930                     }
2931                 }
2932 
2933                 int scrollDiff = newScroll[i] - oldScroll[i] + offset;
2934                 if (scrollDiff != 0) {
2935                     FloatProperty translationProperty = child instanceof TaskView
2936                             ? ((TaskView) child).getPrimaryDismissTranslationProperty()
2937                             : mOrientationHandler.getPrimaryViewTranslate();
2938 
2939                     float additionalDismissDuration =
2940                             ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * Math.abs(
2941                                     i - dismissedIndex);
2942                     anim.setFloat(child, translationProperty, scrollDiff, clampToProgress(LINEAR,
2943                             Utilities.boundToRange(INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
2944                                     + additionalDismissDuration, 0f, 1f), 1));
2945                     if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
2946                             && child instanceof TaskView
2947                             && ((TaskView) child).isRunningTask()) {
2948                         anim.addOnFrameCallback(() -> {
2949                             runActionOnRemoteHandles(
2950                                     remoteTargetHandle ->
2951                                             remoteTargetHandle.getTaskViewSimulator()
2952                                                     .taskPrimaryTranslation.value =
2953                                                     mOrientationHandler.getPrimaryValue(
2954                                                             child.getTranslationX(),
2955                                                             child.getTranslationY()
2956                                                     ));
2957                             redrawLiveTile();
2958                         });
2959                     }
2960                     needsCurveUpdates = true;
2961                 }
2962             } else if (child instanceof TaskView) {
2963                 TaskView taskView = (TaskView) child;
2964                 if (isFocusedTaskDismissed) {
2965                     if (nextFocusedTaskView != null &&
2966                             !isSameGridRow(taskView, nextFocusedTaskView)) {
2967                         continue;
2968                     }
2969                 } else {
2970                     if (i < dismissedIndex || !isSameGridRow(taskView, dismissedTaskView)) {
2971                         continue;
2972                     }
2973                 }
2974                 // Animate task with index >= dismissed index and in the same row as the
2975                 // dismissed index or next focused index. Offset successive task dismissal
2976                 // durations for a staggered effect.
2977                 float animationStartProgress = Utilities.boundToRange(
2978                         INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
2979                                 + ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
2980                                 * ++distanceFromDismissedTask, 0f,
2981                         dismissTranslationInterpolationEnd);
2982                 if (taskView == nextFocusedTaskView) {
2983                     // Enlarge the task to be focused next, and translate into focus position.
2984                     float scale = mTaskWidth / (float) mLastComputedGridTaskSize.width();
2985                     anim.setFloat(taskView, TaskView.SNAPSHOT_SCALE, scale,
2986                             clampToProgress(LINEAR, animationStartProgress,
2987                                     dismissTranslationInterpolationEnd));
2988                     anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(),
2989                             mIsRtl ? dismissedTaskWidth : -dismissedTaskWidth,
2990                             clampToProgress(LINEAR, animationStartProgress,
2991                                     dismissTranslationInterpolationEnd));
2992                     float secondaryTranslation = -mTaskGridVerticalDiff;
2993                     if (!nextFocusedTaskFromTop) {
2994                         secondaryTranslation -= mTopBottomRowHeightDiff;
2995                     }
2996                     anim.setFloat(taskView, taskView.getSecondaryDissmissTranslationProperty(),
2997                             secondaryTranslation, clampToProgress(LINEAR, animationStartProgress,
2998                                     dismissTranslationInterpolationEnd));
2999                     anim.setFloat(taskView, TaskView.FOCUS_TRANSITION, 0f,
3000                             clampToProgress(LINEAR, 0f, ANIMATION_DISMISS_PROGRESS_MIDPOINT));
3001                 } else {
3002                     float primaryTranslation =
3003                             nextFocusedTaskView != null ? nextFocusedTaskWidth : dismissedTaskWidth;
3004                     if (isFocusedTaskDismissed && nextFocusedTaskView == null) {
3005                         // Moves less if focused task is not in scroll position.
3006                         int focusedTaskScroll = getScrollForPage(dismissedIndex);
3007                         int primaryScroll = mOrientationHandler.getPrimaryScroll(this);
3008                         int focusedTaskScrollDiff = primaryScroll - focusedTaskScroll;
3009                         primaryTranslation +=
3010                                 mIsRtl ? focusedTaskScrollDiff : -focusedTaskScrollDiff;
3011                         if (isSplitPlaceholderFirstInGrid) {
3012                             // Moves less if split placeholder is at the start.
3013                             primaryTranslation +=
3014                                     mIsRtl ? -mSplitPlaceholderSize : mSplitPlaceholderSize;
3015                         }
3016                     }
3017 
3018                     anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(),
3019                             mIsRtl ? primaryTranslation : -primaryTranslation,
3020                             clampToProgress(LINEAR, animationStartProgress,
3021                                     dismissTranslationInterpolationEnd));
3022                 }
3023             }
3024         }
3025 
3026         if (needsCurveUpdates) {
3027             anim.addOnFrameCallback(this::updateCurveProperties);
3028         }
3029 
3030         // Add a tiny bit of translation Z, so that it draws on top of other views
3031         if (animateTaskView) {
3032             dismissedTaskView.setTranslationZ(0.1f);
3033         }
3034 
3035         mPendingAnimation = anim;
3036         final TaskView finalNextFocusedTaskView = nextFocusedTaskView;
3037         final boolean finalCloseGapBetweenClearAll = closeGapBetweenClearAll;
3038         final boolean finalSnapToLastTask = snapToLastTask;
3039         final boolean finalIsFocusedTaskDismissed = isFocusedTaskDismissed;
3040         mPendingAnimation.addEndListener(new Consumer<Boolean>() {
3041             @Override
3042             public void accept(Boolean success) {
3043                 if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
3044                         && dismissedTaskView.isRunningTask() && success) {
3045                     finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
3046                             () -> onEnd(success));
3047                 } else {
3048                     onEnd(success);
3049                 }
3050             }
3051 
3052             @SuppressWarnings("WrongCall")
3053             private void onEnd(boolean success) {
3054                 // Reset task translations as they may have updated via animations in
3055                 // createTaskDismissAnimation
3056                 resetTaskVisuals();
3057 
3058                 if (success) {
3059                     if (shouldRemoveTask) {
3060                         if (dismissedTaskView.getTask() != null) {
3061                             if (ENABLE_QUICKSTEP_LIVE_TILE.get()
3062                                     && dismissedTaskView.isRunningTask()) {
3063                                 finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
3064                                         () -> removeTaskInternal(dismissedTaskViewId));
3065                             } else {
3066                                 removeTaskInternal(dismissedTaskViewId);
3067                             }
3068                             mActivity.getStatsLogManager().logger()
3069                                     .withItemInfo(dismissedTaskView.getItemInfo())
3070                                     .log(LAUNCHER_TASK_DISMISS_SWIPE_UP);
3071                         }
3072                     }
3073 
3074                     int pageToSnapTo = mCurrentPage;
3075                     mCurrentPageScrollDiff = 0;
3076                     int taskViewIdToSnapTo = -1;
3077                     if (showAsGrid) {
3078                         if (finalCloseGapBetweenClearAll) {
3079                             if (finalSnapToLastTask) {
3080                                 // Last task will be determined after removing dismissed task.
3081                                 pageToSnapTo = -1;
3082                             } else if (taskCount > 2) {
3083                                 pageToSnapTo = indexOfChild(mClearAllButton);
3084                             } else if (isClearAllHidden) {
3085                                 // Snap to focused task if clear all is hidden.
3086                                 pageToSnapTo = 0;
3087                             }
3088                         } else {
3089                             // Get the id of the task view we will snap to based on the current
3090                             // page's relative position as the order of indices change over time due
3091                             // to dismissals.
3092                             TaskView snappedTaskView = getTaskViewAt(mCurrentPage);
3093                             boolean calculateScrollDiff = true;
3094                             if (snappedTaskView != null && !finalSnapToLastTask) {
3095                                 if (snappedTaskView.getTaskViewId() == mFocusedTaskViewId) {
3096                                     if (finalNextFocusedTaskView != null) {
3097                                         taskViewIdToSnapTo =
3098                                                 finalNextFocusedTaskView.getTaskViewId();
3099                                     } else if (dismissedTaskViewId != mFocusedTaskViewId) {
3100                                         taskViewIdToSnapTo = mFocusedTaskViewId;
3101                                     } else {
3102                                         // Won't focus next task in split select, so snap to the
3103                                         // first task.
3104                                         pageToSnapTo = 0;
3105                                         calculateScrollDiff = false;
3106                                     }
3107                                 } else {
3108                                     int snappedTaskViewId = snappedTaskView.getTaskViewId();
3109                                     boolean isSnappedTaskInTopRow = mTopRowIdSet.contains(
3110                                             snappedTaskViewId);
3111                                     IntArray taskViewIdArray =
3112                                             isSnappedTaskInTopRow ? getTopRowIdArray()
3113                                                     : getBottomRowIdArray();
3114                                     int snappedIndex = taskViewIdArray.indexOf(snappedTaskViewId);
3115                                     taskViewIdArray.removeValue(dismissedTaskViewId);
3116                                     if (finalNextFocusedTaskView != null) {
3117                                         taskViewIdArray.removeValue(
3118                                                 finalNextFocusedTaskView.getTaskViewId());
3119                                     }
3120                                     if (snappedIndex < taskViewIdArray.size()) {
3121                                         taskViewIdToSnapTo = taskViewIdArray.get(snappedIndex);
3122                                     } else if (snappedIndex == taskViewIdArray.size()) {
3123                                         // If the snapped task is the last item from the
3124                                         // dismissed row,
3125                                         // snap to the same column in the other grid row
3126                                         IntArray inverseRowTaskViewIdArray =
3127                                                 isSnappedTaskInTopRow ? getBottomRowIdArray()
3128                                                         : getTopRowIdArray();
3129                                         if (snappedIndex < inverseRowTaskViewIdArray.size()) {
3130                                             taskViewIdToSnapTo = inverseRowTaskViewIdArray.get(
3131                                                     snappedIndex);
3132                                         }
3133                                     }
3134                                 }
3135                             }
3136 
3137                             if (calculateScrollDiff) {
3138                                 int primaryScroll = mOrientationHandler.getPrimaryScroll(
3139                                         RecentsView.this);
3140                                 int currentPageScroll = getScrollForPage(mCurrentPage);
3141                                 mCurrentPageScrollDiff = primaryScroll - currentPageScroll;
3142                                 // Compensate for coordinate shift by split placeholder.
3143                                 if (isSplitPlaceholderFirstInGrid && !finalSnapToLastTask) {
3144                                     mCurrentPageScrollDiff +=
3145                                             mIsRtl ? -mSplitPlaceholderSize : mSplitPlaceholderSize;
3146                                 } else if (isSplitPlaceholderLastInGrid && finalSnapToLastTask) {
3147                                     mCurrentPageScrollDiff +=
3148                                             mIsRtl ? mSplitPlaceholderSize : -mSplitPlaceholderSize;
3149                                 }
3150                             }
3151                         }
3152                     } else if (dismissedIndex < pageToSnapTo || pageToSnapTo == taskCount - 1) {
3153                         pageToSnapTo--;
3154                     }
3155                     boolean isHomeTaskDismissed = dismissedTaskView == getHomeTaskView();
3156                     removeViewInLayout(dismissedTaskView);
3157                     mTopRowIdSet.remove(dismissedTaskViewId);
3158 
3159                     if (taskCount == 1) {
3160                         removeViewInLayout(mClearAllButton);
3161                         if (isHomeTaskDismissed) {
3162                             updateEmptyMessage();
3163                         } else {
3164                             startHome();
3165                         }
3166                     } else {
3167                         // Update focus task and its size.
3168                         if (finalIsFocusedTaskDismissed && finalNextFocusedTaskView != null) {
3169                             mFocusedTaskViewId = finalNextFocusedTaskView.getTaskViewId();
3170                             mTopRowIdSet.remove(mFocusedTaskViewId);
3171                             finalNextFocusedTaskView.animateIconScaleAndDimIntoView();
3172                         }
3173                         updateTaskSize(/*isTaskDismissal=*/ true);
3174                         updateChildTaskOrientations();
3175                         // Update scroll and snap to page.
3176                         updateScrollSynchronously();
3177 
3178                         if (showAsGrid) {
3179                             // Rebalance tasks in the grid
3180                             int highestVisibleTaskIndex = getHighestVisibleTaskIndex();
3181                             if (highestVisibleTaskIndex < Integer.MAX_VALUE) {
3182                                 TaskView taskView = requireTaskViewAt(highestVisibleTaskIndex);
3183 
3184                                 boolean shouldRebalance;
3185                                 int screenStart = mOrientationHandler.getPrimaryScroll(
3186                                         RecentsView.this);
3187                                 int taskStart = mOrientationHandler.getChildStart(taskView)
3188                                         + (int) taskView.getOffsetAdjustment(/*fullscreenEnabled=*/
3189                                         false, /*gridEnabled=*/ true);
3190 
3191                                 // Rebalance only if there is a maximum gap between the task and the
3192                                 // screen's edge; this ensures that rebalanced tasks are outside the
3193                                 // visible screen.
3194                                 if (mIsRtl) {
3195                                     shouldRebalance = taskStart <= screenStart + mPageSpacing;
3196                                 } else {
3197                                     int screenEnd =
3198                                             screenStart + mOrientationHandler.getMeasuredSize(
3199                                                     RecentsView.this);
3200                                     int taskSize = (int) (mOrientationHandler.getMeasuredSize(
3201                                             taskView) * taskView
3202                                             .getSizeAdjustment(/*fullscreenEnabled=*/false));
3203                                     int taskEnd = taskStart + taskSize;
3204 
3205                                     shouldRebalance = taskEnd >= screenEnd - mPageSpacing;
3206                                 }
3207 
3208                                 if (shouldRebalance) {
3209                                     updateGridProperties(/*isTaskDismissal=*/ true,
3210                                             highestVisibleTaskIndex);
3211                                     updateScrollSynchronously();
3212                                 }
3213                             }
3214 
3215                             IntArray topRowIdArray = getTopRowIdArray();
3216                             IntArray bottomRowIdArray = getBottomRowIdArray();
3217                             if (finalSnapToLastTask) {
3218                                 // If snapping to last task, find the last task after dismissal.
3219                                 pageToSnapTo = indexOfChild(
3220                                         getLastGridTaskView(topRowIdArray, bottomRowIdArray));
3221                             } else if (taskViewIdToSnapTo != -1) {
3222                                 // If snapping to another page due to indices rearranging, find
3223                                 // the new index after dismissal & rearrange using the task view id.
3224                                 pageToSnapTo = indexOfChild(
3225                                         getTaskViewFromTaskViewId(taskViewIdToSnapTo));
3226                                 if (!currentPageSnapsToEndOfGrid) {
3227                                     // If it wasn't snapped to one of the last pages, but is now
3228                                     // snapped to last pages, we'll need to compensate for the
3229                                     // offset from the page's scroll to its visual position.
3230                                     mCurrentPageScrollDiff += getOffsetFromScrollPosition(
3231                                             pageToSnapTo, topRowIdArray, bottomRowIdArray);
3232                                 }
3233                             }
3234                         }
3235                         pageBeginTransition();
3236                         setCurrentPage(pageToSnapTo);
3237                         // Update various scroll-dependent UI.
3238                         dispatchScrollChanged();
3239                         updateActionsViewFocusedScroll();
3240                         if (isClearAllHidden()) {
3241                             mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING,
3242                                     false);
3243                         }
3244                     }
3245                 }
3246                 updateCurrentTaskActionsVisibility();
3247                 onDismissAnimationEnds();
3248                 mPendingAnimation = null;
3249             }
3250         });
3251         return anim;
3252     }
3253 
3254     /**
3255      * Hides all overview actions if current page is for split apps, shows otherwise
3256      * If actions are showing, we only show split option if
3257      * * Device is large screen
3258      * * There are at least 2 tasks to invoke split
3259      */
updateCurrentTaskActionsVisibility()3260     private void updateCurrentTaskActionsVisibility() {
3261         boolean isCurrentSplit = getCurrentPageTaskView() instanceof GroupedTaskView;
3262         mActionsView.updateHiddenFlags(HIDDEN_SPLIT_SCREEN, isCurrentSplit);
3263         if (isCurrentSplit) {
3264             return;
3265         }
3266         mActionsView.setSplitButtonVisible(
3267                 mActivity.getDeviceProfile().overviewShowAsGrid && getTaskViewCount() > 1);
3268     }
3269 
3270     /**
3271      * Returns all the tasks in the top row, without the focused task
3272      */
getTopRowIdArray()3273     private IntArray getTopRowIdArray() {
3274         if (mTopRowIdSet.isEmpty()) {
3275             return new IntArray(0);
3276         }
3277         IntArray topArray = new IntArray(mTopRowIdSet.size());
3278         int taskViewCount = getTaskViewCount();
3279         for (int i = 0; i < taskViewCount; i++) {
3280             int taskViewId = requireTaskViewAt(i).getTaskViewId();
3281             if (mTopRowIdSet.contains(taskViewId)) {
3282                 topArray.add(taskViewId);
3283             }
3284         }
3285         return topArray;
3286     }
3287 
3288     /**
3289      * Returns all the tasks in the bottom row, without the focused task
3290      */
getBottomRowIdArray()3291     private IntArray getBottomRowIdArray() {
3292         int bottomRowIdArraySize = getBottomRowTaskCountForTablet();
3293         if (bottomRowIdArraySize <= 0) {
3294             return new IntArray(0);
3295         }
3296         IntArray bottomArray = new IntArray(bottomRowIdArraySize);
3297         int taskViewCount = getTaskViewCount();
3298         for (int i = 0; i < taskViewCount; i++) {
3299             int taskViewId = requireTaskViewAt(i).getTaskViewId();
3300             if (!mTopRowIdSet.contains(taskViewId) && taskViewId != mFocusedTaskViewId) {
3301                 bottomArray.add(taskViewId);
3302             }
3303         }
3304         return bottomArray;
3305     }
3306 
3307     /**
3308      * Iterate the grid by columns instead of by TaskView index, starting after the focused task and
3309      * up to the last balanced column.
3310      *
3311      * @return the highest visible TaskView index between both rows
3312      */
getHighestVisibleTaskIndex()3313     private int getHighestVisibleTaskIndex() {
3314         if (mTopRowIdSet.isEmpty()) return Integer.MAX_VALUE; // return earlier
3315 
3316         int lastVisibleIndex = Integer.MAX_VALUE;
3317         IntArray topRowIdArray = getTopRowIdArray();
3318         IntArray bottomRowIdArray = getBottomRowIdArray();
3319         int balancedColumns = Math.min(bottomRowIdArray.size(), topRowIdArray.size());
3320 
3321         for (int i = 0; i < balancedColumns; i++) {
3322             TaskView topTask = getTaskViewFromTaskViewId(topRowIdArray.get(i));
3323 
3324             if (isTaskViewVisible(topTask)) {
3325                 TaskView bottomTask = getTaskViewFromTaskViewId(bottomRowIdArray.get(i));
3326                 lastVisibleIndex = Math.max(indexOfChild(topTask), indexOfChild(bottomTask));
3327             } else if (lastVisibleIndex < Integer.MAX_VALUE) {
3328                 break;
3329             }
3330         }
3331 
3332         return lastVisibleIndex;
3333     }
3334 
removeTaskInternal(int dismissedTaskViewId)3335     private void removeTaskInternal(int dismissedTaskViewId) {
3336         int[] taskIds = getTaskIdsForTaskViewId(dismissedTaskViewId);
3337         int primaryTaskId = taskIds[0];
3338         int secondaryTaskId = taskIds[1];
3339         UI_HELPER_EXECUTOR.getHandler().postDelayed(
3340                 () -> {
3341                     ActivityManagerWrapper.getInstance().removeTask(primaryTaskId);
3342                     if (secondaryTaskId != -1) {
3343                         ActivityManagerWrapper.getInstance().removeTask(secondaryTaskId);
3344                     }
3345                 },
3346                 REMOVE_TASK_WAIT_FOR_APP_STOP_MS);
3347     }
3348 
3349     /**
3350      * Returns {@code true} if one of the task thumbnails would intersect/overlap with the
3351      * {@link #mFirstFloatingTaskView}.
3352      */
shouldShiftThumbnailsForSplitSelect()3353     public boolean shouldShiftThumbnailsForSplitSelect() {
3354         return !mActivity.getDeviceProfile().isTablet || !mActivity.getDeviceProfile().isLandscape;
3355     }
3356 
onDismissAnimationEnds()3357     protected void onDismissAnimationEnds() {
3358         AccessibilityManagerCompat.sendDismissAnimationEndsEventToTest(getContext());
3359     }
3360 
createAllTasksDismissAnimation(long duration)3361     public PendingAnimation createAllTasksDismissAnimation(long duration) {
3362         if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) {
3363             throw new IllegalStateException("Another pending animation is still running");
3364         }
3365         PendingAnimation anim = new PendingAnimation(duration);
3366 
3367         int count = getTaskViewCount();
3368         for (int i = 0; i < count; i++) {
3369             addDismissedTaskAnimations(requireTaskViewAt(i), duration, anim);
3370         }
3371 
3372         mPendingAnimation = anim;
3373         mPendingAnimation.addEndListener(isSuccess -> {
3374             if (isSuccess) {
3375                 // Remove all the task views now
3376                 finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, () -> {
3377                     UI_HELPER_EXECUTOR.getHandler().postDelayed(
3378                             ActivityManagerWrapper.getInstance()::removeAllRecentTasks,
3379                             REMOVE_TASK_WAIT_FOR_APP_STOP_MS);
3380                     removeTasksViewsAndClearAllButton();
3381                     startHome();
3382                 });
3383             }
3384             mPendingAnimation = null;
3385         });
3386         return anim;
3387     }
3388 
snapToPageRelative(int pageCount, int delta, boolean cycle)3389     private boolean snapToPageRelative(int pageCount, int delta, boolean cycle) {
3390         if (pageCount == 0) {
3391             return false;
3392         }
3393         final int newPageUnbound = getNextPage() + delta;
3394         if (!cycle && (newPageUnbound < 0 || newPageUnbound >= pageCount)) {
3395             return false;
3396         }
3397         snapToPage((newPageUnbound + pageCount) % pageCount);
3398         getChildAt(getNextPage()).requestFocus();
3399         return true;
3400     }
3401 
runDismissAnimation(PendingAnimation pendingAnim)3402     private void runDismissAnimation(PendingAnimation pendingAnim) {
3403         AnimatorPlaybackController controller = pendingAnim.createPlaybackController();
3404         controller.dispatchOnStart();
3405         controller.getAnimationPlayer().setInterpolator(FAST_OUT_SLOW_IN);
3406         controller.start();
3407     }
3408 
3409     @UiThread
dismissTask(int taskId)3410     private void dismissTask(int taskId) {
3411         TaskView taskView = getTaskViewByTaskId(taskId);
3412         if (taskView == null) {
3413             return;
3414         }
3415         dismissTask(taskView, true /* animate */, false /* removeTask */);
3416     }
3417 
dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask)3418     public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) {
3419         runDismissAnimation(createTaskDismissAnimation(taskView, animateTaskView, removeTask,
3420                 DISMISS_TASK_DURATION, false /* dismissingForSplitSelection*/));
3421     }
3422 
3423     @SuppressWarnings("unused")
dismissAllTasks(View view)3424     private void dismissAllTasks(View view) {
3425         runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION));
3426         mActivity.getStatsLogManager().logger().log(LAUNCHER_TASK_CLEAR_ALL);
3427     }
3428 
dismissCurrentTask()3429     private void dismissCurrentTask() {
3430         TaskView taskView = getNextPageTaskView();
3431         if (taskView != null) {
3432             dismissTask(taskView, true /*animateTaskView*/, true /*removeTask*/);
3433         }
3434     }
3435 
3436     @Override
dispatchKeyEvent(KeyEvent event)3437     public boolean dispatchKeyEvent(KeyEvent event) {
3438         if (event.getAction() == KeyEvent.ACTION_DOWN) {
3439             switch (event.getKeyCode()) {
3440                 case KeyEvent.KEYCODE_TAB:
3441                     return snapToPageRelative(getTaskViewCount(), event.isShiftPressed() ? -1 : 1,
3442                             event.isAltPressed() /* cycle */);
3443                 case KeyEvent.KEYCODE_DPAD_RIGHT:
3444                     return snapToPageRelative(getPageCount(), mIsRtl ? -1 : 1, false /* cycle */);
3445                 case KeyEvent.KEYCODE_DPAD_LEFT:
3446                     return snapToPageRelative(getPageCount(), mIsRtl ? 1 : -1, false /* cycle */);
3447                 case KeyEvent.KEYCODE_DEL:
3448                 case KeyEvent.KEYCODE_FORWARD_DEL:
3449                     dismissCurrentTask();
3450                     return true;
3451                 case KeyEvent.KEYCODE_NUMPAD_DOT:
3452                     if (event.isAltPressed()) {
3453                         // Numpad DEL pressed while holding Alt.
3454                         dismissCurrentTask();
3455                         return true;
3456                     }
3457             }
3458         }
3459         return super.dispatchKeyEvent(event);
3460     }
3461 
3462     @Override
onFocusChanged(boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect)3463     protected void onFocusChanged(boolean gainFocus, int direction,
3464             @Nullable Rect previouslyFocusedRect) {
3465         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
3466         if (gainFocus && getChildCount() > 0) {
3467             switch (direction) {
3468                 case FOCUS_FORWARD:
3469                     setCurrentPage(0);
3470                     break;
3471                 case FOCUS_BACKWARD:
3472                 case FOCUS_RIGHT:
3473                 case FOCUS_LEFT:
3474                     setCurrentPage(getChildCount() - 1);
3475                     break;
3476             }
3477         }
3478     }
3479 
getContentAlpha()3480     public float getContentAlpha() {
3481         return mContentAlpha;
3482     }
3483 
setContentAlpha(float alpha)3484     public void setContentAlpha(float alpha) {
3485         if (alpha == mContentAlpha) {
3486             return;
3487         }
3488         alpha = Utilities.boundToRange(alpha, 0, 1);
3489         mContentAlpha = alpha;
3490         int runningTaskId = getTaskIdsForRunningTaskView()[0];
3491         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
3492             TaskView child = requireTaskViewAt(i);
3493             int[] childTaskIds = child.getTaskIds();
3494             if (!mRunningTaskTileHidden ||
3495                     (childTaskIds[0] != runningTaskId && childTaskIds[1] != runningTaskId)) {
3496                 child.setStableAlpha(alpha);
3497             }
3498         }
3499         mClearAllButton.setContentAlpha(mContentAlpha);
3500         int alphaInt = Math.round(alpha * 255);
3501         mEmptyMessagePaint.setAlpha(alphaInt);
3502         mEmptyIcon.setAlpha(alphaInt);
3503         mActionsView.getContentAlpha().setValue(mContentAlpha);
3504 
3505         if (alpha > 0) {
3506             setVisibility(VISIBLE);
3507         } else if (!mFreezeViewVisibility) {
3508             setVisibility(INVISIBLE);
3509         }
3510     }
3511 
3512     /**
3513      * Freezes the view visibility change. When frozen, the view will not change its visibility
3514      * to gone due to alpha changes.
3515      */
setFreezeViewVisibility(boolean freezeViewVisibility)3516     public void setFreezeViewVisibility(boolean freezeViewVisibility) {
3517         if (mFreezeViewVisibility != freezeViewVisibility) {
3518             mFreezeViewVisibility = freezeViewVisibility;
3519             if (!mFreezeViewVisibility) {
3520                 setVisibility(mContentAlpha > 0 ? VISIBLE : INVISIBLE);
3521             }
3522         }
3523     }
3524 
3525     @Override
setVisibility(int visibility)3526     public void setVisibility(int visibility) {
3527         super.setVisibility(visibility);
3528         if (mActionsView != null) {
3529             mActionsView.updateHiddenFlags(HIDDEN_NO_RECENTS, visibility != VISIBLE);
3530             if (visibility != VISIBLE) {
3531                 mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false);
3532             }
3533         }
3534     }
3535 
3536     @Override
onConfigurationChanged(Configuration newConfig)3537     protected void onConfigurationChanged(Configuration newConfig) {
3538         super.onConfigurationChanged(newConfig);
3539         updateRecentsRotation();
3540         onOrientationChanged();
3541     }
3542 
3543     /**
3544      * Updates {@link RecentsOrientedState}'s cached RecentsView rotation.
3545      */
updateRecentsRotation()3546     public void updateRecentsRotation() {
3547         final int rotation = mActivity.getDisplay().getRotation();
3548         mOrientationState.setRecentsRotation(rotation);
3549     }
3550 
setLayoutRotation(int touchRotation, int displayRotation)3551     public void setLayoutRotation(int touchRotation, int displayRotation) {
3552         if (mOrientationState.update(touchRotation, displayRotation)) {
3553             updateOrientationHandler();
3554         }
3555     }
3556 
getPagedViewOrientedState()3557     public RecentsOrientedState getPagedViewOrientedState() {
3558         return mOrientationState;
3559     }
3560 
getPagedOrientationHandler()3561     public PagedOrientationHandler getPagedOrientationHandler() {
3562         return mOrientationHandler;
3563     }
3564 
3565     @Nullable
getNextTaskView()3566     public TaskView getNextTaskView() {
3567         return getTaskViewAt(getRunningTaskIndex() + 1);
3568     }
3569 
3570     @Nullable
getCurrentPageTaskView()3571     public TaskView getCurrentPageTaskView() {
3572         return getTaskViewAt(getCurrentPage());
3573     }
3574 
3575     @Nullable
getNextPageTaskView()3576     public TaskView getNextPageTaskView() {
3577         return getTaskViewAt(getNextPage());
3578     }
3579 
3580     @Nullable
getTaskViewNearestToCenterOfScreen()3581     public TaskView getTaskViewNearestToCenterOfScreen() {
3582         return getTaskViewAt(getPageNearestToCenterOfScreen());
3583     }
3584 
3585     /**
3586      * Returns null instead of indexOutOfBoundsError when index is not in range
3587      */
3588     @Nullable
getTaskViewAt(int index)3589     public TaskView getTaskViewAt(int index) {
3590         View child = getChildAt(index);
3591         return child instanceof TaskView ? (TaskView) child : null;
3592     }
3593 
3594     /**
3595      * A version of {@link #getTaskViewAt} when the caller is sure about the input index.
3596      */
3597     @NonNull
requireTaskViewAt(int index)3598     private TaskView requireTaskViewAt(int index) {
3599         return Objects.requireNonNull(getTaskViewAt(index));
3600     }
3601 
setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener listener)3602     public void setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener listener) {
3603         mOnEmptyMessageUpdatedListener = listener;
3604     }
3605 
updateEmptyMessage()3606     public void updateEmptyMessage() {
3607         boolean isEmpty = getTaskViewCount() == 0;
3608         boolean hasSizeChanged = mLastMeasureSize.x != getWidth()
3609                 || mLastMeasureSize.y != getHeight();
3610         if (isEmpty == mShowEmptyMessage && !hasSizeChanged) {
3611             return;
3612         }
3613         setContentDescription(isEmpty ? mEmptyMessage : "");
3614         mShowEmptyMessage = isEmpty;
3615         updateEmptyStateUi(hasSizeChanged);
3616         invalidate();
3617 
3618         if (mOnEmptyMessageUpdatedListener != null) {
3619             mOnEmptyMessageUpdatedListener.onEmptyMessageUpdated(mShowEmptyMessage);
3620         }
3621     }
3622 
3623     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)3624     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
3625         // If we're going to a state without overview panel, avoid unnecessary onLayout that
3626         // cause TaskViews to re-arrange during animation to that state.
3627         if (!mOverviewStateEnabled && !mFirstLayout) {
3628             return;
3629         }
3630 
3631         mShowAsGridLastOnLayout = showAsGrid();
3632 
3633         super.onLayout(changed, left, top, right, bottom);
3634 
3635         updateEmptyStateUi(changed);
3636 
3637         // Update the pivots such that when the task is scaled, it fills the full page
3638         getTaskSize(mTempRect);
3639         getPagedViewOrientedState().getFullScreenScaleAndPivot(mTempRect,
3640                 mActivity.getDeviceProfile(), mTempPointF);
3641         setPivotX(mTempPointF.x);
3642         setPivotY(mTempPointF.y);
3643         setTaskModalness(mTaskModalness);
3644         mLastComputedTaskStartPushOutDistance = null;
3645         mLastComputedTaskEndPushOutDistance = null;
3646         updatePageOffsets();
3647         runActionOnRemoteHandles(
3648                 remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
3649                         .setScroll(getScrollOffset()));
3650         setImportantForAccessibility(isModal() ? IMPORTANT_FOR_ACCESSIBILITY_NO
3651                 : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
3652     }
3653 
updatePageOffsets()3654     private void updatePageOffsets() {
3655         float offset = mAdjacentPageHorizontalOffset;
3656         float modalOffset = ACCEL_0_75.getInterpolation(mTaskModalness);
3657         int count = getChildCount();
3658 
3659         TaskView runningTask = mRunningTaskViewId == -1 || !mRunningTaskTileHidden
3660                 ? null : getRunningTaskView();
3661         int midpoint = runningTask == null ? -1 : indexOfChild(runningTask);
3662         int modalMidpoint = getCurrentPage();
3663 
3664         float midpointOffsetSize = 0;
3665         float leftOffsetSize = midpoint - 1 >= 0
3666                 ? getHorizontalOffsetSize(midpoint - 1, midpoint, offset)
3667                 : 0;
3668         float rightOffsetSize = midpoint + 1 < count
3669                 ? getHorizontalOffsetSize(midpoint + 1, midpoint, offset)
3670                 : 0;
3671 
3672         boolean showAsGrid = showAsGrid();
3673         float modalMidpointOffsetSize = 0;
3674         float modalLeftOffsetSize = 0;
3675         float modalRightOffsetSize = 0;
3676         float gridOffsetSize = 0;
3677 
3678         if (showAsGrid) {
3679             // In grid, we only focus the task on the side. The reference index used for offset
3680             // calculation is the task directly next to the focus task in the grid.
3681             int referenceIndex = modalMidpoint == 0 ? 1 : 0;
3682             gridOffsetSize = referenceIndex < count
3683                     ? getHorizontalOffsetSize(referenceIndex, modalMidpoint, modalOffset)
3684                     : 0;
3685         } else {
3686             modalLeftOffsetSize = modalMidpoint - 1 >= 0
3687                     ? getHorizontalOffsetSize(modalMidpoint - 1, modalMidpoint, modalOffset)
3688                     : 0;
3689             modalRightOffsetSize = modalMidpoint + 1 < count
3690                     ? getHorizontalOffsetSize(modalMidpoint + 1, modalMidpoint, modalOffset)
3691                     : 0;
3692         }
3693 
3694         for (int i = 0; i < count; i++) {
3695             float translation = i == midpoint
3696                     ? midpointOffsetSize
3697                     : i < midpoint
3698                             ? leftOffsetSize
3699                             : rightOffsetSize;
3700             float modalTranslation = i == modalMidpoint
3701                     ? modalMidpointOffsetSize
3702                     : showAsGrid
3703                             ? gridOffsetSize
3704                             : i < modalMidpoint ? modalLeftOffsetSize : modalRightOffsetSize;
3705             float totalTranslation = translation + modalTranslation;
3706             View child = getChildAt(i);
3707             FloatProperty translationProperty = child instanceof TaskView
3708                     ? ((TaskView) child).getPrimaryTaskOffsetTranslationProperty()
3709                     : mOrientationHandler.getPrimaryViewTranslate();
3710             translationProperty.set(child, totalTranslation);
3711             if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
3712                     && i == getRunningTaskIndex()) {
3713                 runActionOnRemoteHandles(
3714                         remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
3715                                 .taskPrimaryTranslation.value = totalTranslation);
3716                 redrawLiveTile();
3717             }
3718         }
3719         updateCurveProperties();
3720     }
3721 
3722     /**
3723      * Computes the child position with persistent translation considered (see
3724      * {@link TaskView#getPersistentTranslationX()}.
3725      */
3726     private void getPersistentChildPosition(int childIndex, int midPointScroll, RectF outRect) {
3727         View child = getChildAt(childIndex);
3728         outRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
3729         if (child instanceof TaskView) {
3730             TaskView taskView = (TaskView) child;
3731             outRect.offset(taskView.getPersistentTranslationX(),
3732                     taskView.getPersistentTranslationY());
3733             outRect.top += mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx;
3734 
3735             mTempMatrix.reset();
3736             float persistentScale = taskView.getPersistentScale();
3737             mTempMatrix.postScale(persistentScale, persistentScale,
3738                     mIsRtl ? outRect.right : outRect.left, outRect.top);
3739             mTempMatrix.mapRect(outRect);
3740         }
3741         outRect.offset(mOrientationHandler.getPrimaryValue(-midPointScroll, 0),
3742                 mOrientationHandler.getSecondaryValue(-midPointScroll, 0));
3743     }
3744 
3745     /**
3746      * Computes the distance to offset the given child such that it is completely offscreen when
3747      * translating away from the given midpoint.
3748      * @param offsetProgress From 0 to 1 where 0 means no offset and 1 means offset offscreen.
3749      */
3750     private float getHorizontalOffsetSize(int childIndex, int midpointIndex, float offsetProgress) {
3751         if (offsetProgress == 0) {
3752             // Don't bother calculating everything below if we won't offset anyway.
3753             return 0;
3754         }
3755 
3756         // First, get the position of the task relative to the midpoint. If there is no midpoint
3757         // then we just use the normal (centered) task position.
3758         RectF taskPosition = mTempRectF;
3759         // Whether the task should be shifted to start direction (i.e. left edge for portrait, top
3760         // edge for landscape/seascape).
3761         boolean isStartShift;
3762         if (midpointIndex > -1) {
3763             // When there is a midpoint reference task, adjacent tasks have less distance to travel
3764             // to reach offscreen. Offset the task position to the task's starting point, and offset
3765             // by current page's scroll diff.
3766             int midpointScroll = getScrollForPage(midpointIndex)
3767                     + mOrientationHandler.getPrimaryScroll(this) - getScrollForPage(mCurrentPage);
3768 
3769             getPersistentChildPosition(midpointIndex, midpointScroll, taskPosition);
3770             float midpointStart = mOrientationHandler.getStart(taskPosition);
3771 
3772             getPersistentChildPosition(childIndex, midpointScroll, taskPosition);
3773             // Assume child does not overlap with midPointChild.
3774             isStartShift = mOrientationHandler.getStart(taskPosition) < midpointStart;
3775         } else {
3776             // Position the task at scroll position.
3777             getPersistentChildPosition(childIndex, getScrollForPage(childIndex), taskPosition);
3778             isStartShift = mIsRtl;
3779         }
3780 
3781         // Next, calculate the distance to move the task off screen. We also need to account for
3782         // RecentsView scale, because it moves tasks based on its pivot. To do this, we move the
3783         // task position to where it would be offscreen at scale = 1 (computed above), then we
3784         // apply the scale via getMatrix() to determine how much that moves the task from its
3785         // desired position, and adjust the computed distance accordingly.
3786         float distanceToOffscreen;
3787         if (isStartShift) {
3788             float desiredStart = -mOrientationHandler.getPrimarySize(taskPosition);
3789             distanceToOffscreen = -mOrientationHandler.getEnd(taskPosition);
3790             if (mLastComputedTaskStartPushOutDistance == null) {
3791                 taskPosition.offsetTo(
3792                         mOrientationHandler.getPrimaryValue(desiredStart, 0f),
3793                         mOrientationHandler.getSecondaryValue(desiredStart, 0f));
3794                 getMatrix().mapRect(taskPosition);
3795                 mLastComputedTaskStartPushOutDistance = mOrientationHandler.getEnd(taskPosition)
3796                         / mOrientationHandler.getPrimaryScale(this);
3797             }
3798             distanceToOffscreen -= mLastComputedTaskStartPushOutDistance;
3799         } else {
3800             float desiredStart = mOrientationHandler.getPrimarySize(this);
3801             distanceToOffscreen = desiredStart - mOrientationHandler.getStart(taskPosition);
3802             if (mLastComputedTaskEndPushOutDistance == null) {
3803                 taskPosition.offsetTo(
3804                         mOrientationHandler.getPrimaryValue(desiredStart, 0f),
3805                         mOrientationHandler.getSecondaryValue(desiredStart, 0f));
3806                 getMatrix().mapRect(taskPosition);
3807                 mLastComputedTaskEndPushOutDistance = (mOrientationHandler.getStart(taskPosition)
3808                         - desiredStart) / mOrientationHandler.getPrimaryScale(this);
3809             }
3810             distanceToOffscreen -= mLastComputedTaskEndPushOutDistance;
3811         }
3812         return distanceToOffscreen * offsetProgress;
3813     }
3814 
3815     protected void setTaskViewsResistanceTranslation(float translation) {
3816         mTaskViewsSecondaryTranslation = translation;
3817         for (int i = 0; i < getTaskViewCount(); i++) {
3818             TaskView task = requireTaskViewAt(i);
3819             task.getTaskResistanceTranslationProperty().set(task, translation / getScaleY());
3820         }
3821         runActionOnRemoteHandles(
3822                 remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
3823                         .recentsViewSecondaryTranslation.value = translation);
3824     }
3825 
3826     private void updateTaskViewsSnapshotRadius() {
3827         for (int i = 0; i < getTaskViewCount(); i++) {
3828             requireTaskViewAt(i).updateSnapshotRadius();
3829         }
3830     }
3831 
3832     protected void setTaskViewsPrimarySplitTranslation(float translation) {
3833         mTaskViewsPrimarySplitTranslation = translation;
3834         for (int i = 0; i < getTaskViewCount(); i++) {
3835             TaskView task = requireTaskViewAt(i);
3836             task.getPrimarySplitTranslationProperty().set(task, translation);
3837         }
3838     }
3839 
3840     protected void setTaskViewsSecondarySplitTranslation(float translation) {
3841         mTaskViewsSecondarySplitTranslation = translation;
3842         for (int i = 0; i < getTaskViewCount(); i++) {
3843             TaskView taskView = requireTaskViewAt(i);
3844             if (taskView == mSplitHiddenTaskView) {
3845                 continue;
3846             }
3847             taskView.getSecondarySplitTranslationProperty().set(taskView, translation);
3848         }
3849     }
3850 
3851     /**
3852      * Apply scroll offset to children of RecentsView when entering split select.
3853      */
3854     public void applySplitPrimaryScrollOffset() {
3855         float taskSplitScrollOffsetPrimary = 0f;
3856         float clearAllSplitScrollOffsetPrimar = 0f;
3857         if (isSplitPlaceholderFirstInGrid()) {
3858             taskSplitScrollOffsetPrimary = mSplitPlaceholderSize;
3859         } else if (isSplitPlaceholderLastInGrid()) {
3860             clearAllSplitScrollOffsetPrimar = -mSplitPlaceholderSize;
3861         }
3862 
3863         for (int i = 0; i < getTaskViewCount(); i++) {
3864             requireTaskViewAt(i).setSplitScrollOffsetPrimary(taskSplitScrollOffsetPrimary);
3865         }
3866         mClearAllButton.setSplitSelectScrollOffsetPrimary(clearAllSplitScrollOffsetPrimar);
3867     }
3868 
3869     /**
3870      * Returns if split placeholder is at the beginning of RecentsView. Always returns {@code false}
3871      * if RecentsView is in portrait or RecentsView isn't shown as grid.
3872      */
3873     private boolean isSplitPlaceholderFirstInGrid() {
3874         if (!mActivity.getDeviceProfile().isLandscape || !showAsGrid()
3875                 || !isSplitSelectionActive()) {
3876             return false;
3877         }
3878         @StagePosition int position = mSplitSelectStateController.getActiveSplitStagePosition();
3879         return mIsRtl
3880                 ? position == STAGE_POSITION_BOTTOM_OR_RIGHT
3881                 : position == STAGE_POSITION_TOP_OR_LEFT;
3882     }
3883 
3884     /**
3885      * Returns if split placeholder is at the end of RecentsView. Always returns {@code false} if
3886      * RecentsView is in portrait or RecentsView isn't shown as grid.
3887      */
3888     private boolean isSplitPlaceholderLastInGrid() {
3889         if (!mActivity.getDeviceProfile().isLandscape || !showAsGrid()
3890                 || !isSplitSelectionActive()) {
3891             return false;
3892         }
3893         @StagePosition int position = mSplitSelectStateController.getActiveSplitStagePosition();
3894         return mIsRtl
3895                 ? position == STAGE_POSITION_TOP_OR_LEFT
3896                 : position == STAGE_POSITION_BOTTOM_OR_RIGHT;
3897     }
3898 
3899     /**
3900      * Reset scroll offset on children of RecentsView when exiting split select.
3901      */
3902     public void resetSplitPrimaryScrollOffset() {
3903         for (int i = 0; i < getTaskViewCount(); i++) {
3904             requireTaskViewAt(i).setSplitScrollOffsetPrimary(0);
3905         }
3906         mClearAllButton.setSplitSelectScrollOffsetPrimary(0);
3907     }
3908 
3909     /**
3910      * Resets the visuals when exit modal state.
3911      */
3912     public void resetModalVisuals() {
3913         TaskView taskView = getCurrentPageTaskView();
3914         if (taskView != null) {
3915             taskView.getThumbnail().getTaskOverlay().resetModalVisuals();
3916         }
3917     }
3918 
3919     public void initiateSplitSelect(TaskView taskView) {
3920         int defaultSplitPosition = mOrientationHandler
3921                 .getDefaultSplitPosition(mActivity.getDeviceProfile());
3922         initiateSplitSelect(taskView, defaultSplitPosition);
3923     }
3924 
3925     public void initiateSplitSelect(TaskView taskView, @StagePosition int stagePosition) {
3926         mSplitHiddenTaskView = taskView;
3927         Rect initialBounds = new Rect(taskView.getLeft(), taskView.getTop(), taskView.getRight(),
3928                 taskView.getBottom());
3929         mSplitSelectStateController.setInitialTaskSelect(taskView.getTask(),
3930                 stagePosition, initialBounds);
3931         mSplitHiddenTaskViewIndex = indexOfChild(taskView);
3932         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
3933             finishRecentsAnimation(true, null);
3934         }
3935     }
3936 
3937     public PendingAnimation createSplitSelectInitAnimation() {
3938         int duration = mActivity.getStateManager().getState().getTransitionDuration(getContext());
3939         return createTaskDismissAnimation(mSplitHiddenTaskView, true, false, duration,
3940                 true /* dismissingForSplitSelection*/);
3941     }
3942 
3943     public void confirmSplitSelect(TaskView taskView) {
3944         mSplitToast.cancel();
3945         if (!taskView.getTask().isDockable) {
3946             // Task not split screen supported
3947             mSplitUnsupportedToast.show();
3948             return;
3949         }
3950         RectF secondTaskStartingBounds = new RectF();
3951         Rect secondTaskEndingBounds = new Rect();
3952         // TODO(194414938) starting bounds seem slightly off, investigate
3953         Rect firstTaskStartingBounds = new Rect();
3954         Rect firstTaskEndingBounds = mTempRect;
3955         int duration = mActivity.getStateManager().getState().getTransitionDuration(getContext());
3956         PendingAnimation pendingAnimation = new PendingAnimation(duration);
3957 
3958         int halfDividerSize = getResources()
3959                 .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2;
3960         mOrientationHandler.getFinalSplitPlaceholderBounds(halfDividerSize,
3961                 mActivity.getDeviceProfile(),
3962                 mSplitSelectStateController.getActiveSplitStagePosition(), firstTaskEndingBounds,
3963                 secondTaskEndingBounds);
3964 
3965         mFirstFloatingTaskView.getBoundsOnScreen(firstTaskStartingBounds);
3966         mFirstFloatingTaskView.addAnimation(pendingAnimation,
3967                 new RectF(firstTaskStartingBounds), firstTaskEndingBounds, mFirstFloatingTaskView,
3968                 false /*fadeWithThumbnail*/);
3969 
3970         mSecondFloatingTaskView = FloatingTaskView.getFloatingTaskView(mActivity,
3971                 taskView, secondTaskStartingBounds);
3972         mSecondFloatingTaskView.setAlpha(1);
3973         mSecondFloatingTaskView.addAnimation(pendingAnimation, secondTaskStartingBounds,
3974                 secondTaskEndingBounds, taskView.getThumbnail(),
3975                 true /*fadeWithThumbnail*/);
3976         pendingAnimation.addEndListener(aBoolean ->
3977                 mSplitSelectStateController.setSecondTaskId(taskView.getTask(),
3978                 aBoolean1 -> RecentsView.this.resetFromSplitSelectionState()));
3979         mSecondSplitHiddenTaskView = taskView;
3980         taskView.setVisibility(INVISIBLE);
3981         pendingAnimation.buildAnim().start();
3982     }
3983 
3984     /** TODO(b/181707736) More gracefully handle exiting split selection state */
3985     protected void resetFromSplitSelectionState() {
3986         if (mSplitHiddenTaskViewIndex == -1) {
3987             return;
3988         }
3989         if (!mActivity.getDeviceProfile().overviewShowAsGrid) {
3990             int pageToSnapTo = mCurrentPage;
3991             if (mSplitHiddenTaskViewIndex <= pageToSnapTo) {
3992                 pageToSnapTo += 1;
3993             } else {
3994                 pageToSnapTo = mSplitHiddenTaskViewIndex;
3995             }
3996             snapToPageImmediately(pageToSnapTo);
3997         }
3998         onLayout(false /*  changed */, getLeft(), getTop(), getRight(), getBottom());
3999         resetTaskVisuals();
4000         mSplitHiddenTaskViewIndex = -1;
4001         if (mSplitHiddenTaskView != null) {
4002             mSplitHiddenTaskView.setVisibility(VISIBLE);
4003             mSplitHiddenTaskView = null;
4004         }
4005         if (mFirstFloatingTaskView != null) {
4006             mActivity.getRootView().removeView(mFirstFloatingTaskView);
4007             mFirstFloatingTaskView = null;
4008         }
4009         if (mSecondFloatingTaskView != null) {
4010             mActivity.getRootView().removeView(mSecondFloatingTaskView);
4011             mSecondFloatingTaskView = null;
4012             mSecondSplitHiddenTaskView.setVisibility(VISIBLE);
4013             mSecondSplitHiddenTaskView = null;
4014         }
4015     }
4016 
4017     /**
4018      * Returns how much additional translation there should be for each of the child TaskViews.
4019      * Note that the translation can be its primary or secondary dimension.
4020      */
4021     public float getSplitSelectTranslation() {
4022         int splitPosition = getSplitPlaceholder().getActiveSplitStagePosition();
4023         if (!shouldShiftThumbnailsForSplitSelect()) {
4024             return 0f;
4025         }
4026         PagedOrientationHandler orientationHandler = getPagedOrientationHandler();
4027         int direction = orientationHandler.getSplitTranslationDirectionFactor(
4028                 splitPosition, mActivity.getDeviceProfile());
4029         return mActivity.getResources().getDimension(R.dimen.split_placeholder_size) * direction;
4030     }
4031 
4032     protected void onRotateInSplitSelectionState() {
4033         mOrientationHandler.getInitialSplitPlaceholderBounds(mSplitPlaceholderSize,
4034                 mActivity.getDeviceProfile(),
4035                 mSplitSelectStateController.getActiveSplitStagePosition(), mTempRect);
4036         mTempRectF.set(mTempRect);
4037         // TODO(194414938) set correct corner radius
4038         mFirstFloatingTaskView.updateOrientationHandler(mOrientationHandler);
4039         mFirstFloatingTaskView.update(mTempRectF, /*progress=*/1f, /*windowRadius=*/0f);
4040 
4041         PagedOrientationHandler orientationHandler = getPagedOrientationHandler();
4042         Pair<FloatProperty, FloatProperty> taskViewsFloat =
4043                 orientationHandler.getSplitSelectTaskOffset(
4044                         TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
4045                         mActivity.getDeviceProfile());
4046         taskViewsFloat.first.set(this, getSplitSelectTranslation());
4047         taskViewsFloat.second.set(this, 0f);
4048 
4049         applySplitPrimaryScrollOffset();
4050     }
4051 
4052     private void updateDeadZoneRects() {
4053         // Get the deadzone rect surrounding the clear all button to not dismiss overview to home
4054         mClearAllButtonDeadZoneRect.setEmpty();
4055         if (mClearAllButton.getWidth() > 0) {
4056             int verticalMargin = getResources()
4057                     .getDimensionPixelSize(R.dimen.recents_clear_all_deadzone_vertical_margin);
4058             mClearAllButton.getHitRect(mClearAllButtonDeadZoneRect);
4059             mClearAllButtonDeadZoneRect.inset(-getPaddingRight() / 2, -verticalMargin);
4060         }
4061 
4062         // Get the deadzone rect between the task views
4063         mTaskViewDeadZoneRect.setEmpty();
4064         int count = getTaskViewCount();
4065         if (count > 0) {
4066             final View taskView = requireTaskViewAt(0);
4067             requireTaskViewAt(count - 1).getHitRect(mTaskViewDeadZoneRect);
4068             mTaskViewDeadZoneRect.union(taskView.getLeft(), taskView.getTop(), taskView.getRight(),
4069                     taskView.getBottom());
4070         }
4071     }
4072 
4073     private void updateEmptyStateUi(boolean sizeChanged) {
4074         boolean hasValidSize = getWidth() > 0 && getHeight() > 0;
4075         if (sizeChanged && hasValidSize) {
4076             mEmptyTextLayout = null;
4077             mLastMeasureSize.set(getWidth(), getHeight());
4078         }
4079 
4080         if (mShowEmptyMessage && hasValidSize && mEmptyTextLayout == null) {
4081             int availableWidth = mLastMeasureSize.x - mEmptyMessagePadding - mEmptyMessagePadding;
4082             mEmptyTextLayout = StaticLayout.Builder.obtain(mEmptyMessage, 0, mEmptyMessage.length(),
4083                     mEmptyMessagePaint, availableWidth)
4084                     .setAlignment(Layout.Alignment.ALIGN_CENTER)
4085                     .build();
4086             int totalHeight = mEmptyTextLayout.getHeight()
4087                     + mEmptyMessagePadding + mEmptyIcon.getIntrinsicHeight();
4088 
4089             int top = (mLastMeasureSize.y - totalHeight) / 2;
4090             int left = (mLastMeasureSize.x - mEmptyIcon.getIntrinsicWidth()) / 2;
4091             mEmptyIcon.setBounds(left, top, left + mEmptyIcon.getIntrinsicWidth(),
4092                     top + mEmptyIcon.getIntrinsicHeight());
4093         }
4094     }
4095 
4096     @Override
4097     protected boolean verifyDrawable(Drawable who) {
4098         return super.verifyDrawable(who) || (mShowEmptyMessage && who == mEmptyIcon);
4099     }
4100 
4101     protected void maybeDrawEmptyMessage(Canvas canvas) {
4102         if (mShowEmptyMessage && mEmptyTextLayout != null) {
4103             // Offset to center in the visible (non-padded) part of RecentsView
4104             mTempRect.set(mInsets.left + getPaddingLeft(), mInsets.top + getPaddingTop(),
4105                     mInsets.right + getPaddingRight(), mInsets.bottom + getPaddingBottom());
4106             canvas.save();
4107             canvas.translate(getScrollX() + (mTempRect.left - mTempRect.right) / 2,
4108                     (mTempRect.top - mTempRect.bottom) / 2);
4109             mEmptyIcon.draw(canvas);
4110             canvas.translate(mEmptyMessagePadding,
4111                     mEmptyIcon.getBounds().bottom + mEmptyMessagePadding);
4112             mEmptyTextLayout.draw(canvas);
4113             canvas.restore();
4114         }
4115     }
4116 
4117     /**
4118      * Animate adjacent tasks off screen while scaling up.
4119      *
4120      * If launching one of the adjacent tasks, parallax the center task and other adjacent task
4121      * to the right.
4122      */
4123     public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv) {
4124         AnimatorSet anim = new AnimatorSet();
4125 
4126         int taskIndex = indexOfChild(tv);
4127         int centerTaskIndex = getCurrentPage();
4128         boolean launchingCenterTask = taskIndex == centerTaskIndex;
4129 
4130         float toScale = getMaxScaleForFullScreen();
4131         RecentsView recentsView = tv.getRecentsView();
4132         if (launchingCenterTask) {
4133             anim.play(ObjectAnimator.ofFloat(recentsView, RECENTS_SCALE_PROPERTY, toScale));
4134             anim.play(ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS, 1));
4135         } else {
4136             // We are launching an adjacent task, so parallax the center and other adjacent task.
4137             float displacementX = tv.getWidth() * (toScale - 1f);
4138             float primaryTranslation = mIsRtl ? -displacementX : displacementX;
4139             anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex),
4140                     mOrientationHandler.getPrimaryViewTranslate(), primaryTranslation));
4141             int runningTaskIndex = recentsView.getRunningTaskIndex();
4142             if (ENABLE_QUICKSTEP_LIVE_TILE.get()
4143                     && runningTaskIndex != -1
4144                     && runningTaskIndex != taskIndex
4145                     && recentsView.getRemoteTargetHandles() != null) {
4146                 for (RemoteTargetHandle remoteHandle : recentsView.getRemoteTargetHandles()) {
4147                     anim.play(ObjectAnimator.ofFloat(
4148                             remoteHandle.getTaskViewSimulator().taskPrimaryTranslation,
4149                             AnimatedFloat.VALUE,
4150                             primaryTranslation));
4151                 }
4152             }
4153 
4154             int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - taskIndex);
4155             if (otherAdjacentTaskIndex >= 0 && otherAdjacentTaskIndex < getPageCount()) {
4156                 PropertyValuesHolder[] properties = new PropertyValuesHolder[3];
4157                 properties[0] = PropertyValuesHolder.ofFloat(
4158                         mOrientationHandler.getPrimaryViewTranslate(), primaryTranslation);
4159                 properties[1] = PropertyValuesHolder.ofFloat(View.SCALE_X, 1);
4160                 properties[2] = PropertyValuesHolder.ofFloat(View.SCALE_Y, 1);
4161 
4162                 anim.play(ObjectAnimator.ofPropertyValuesHolder(getPageAt(otherAdjacentTaskIndex),
4163                         properties));
4164             }
4165         }
4166         return anim;
4167     }
4168 
4169     /**
4170      * Returns the scale up required on the view, so that it coves the screen completely
4171      */
4172     public float getMaxScaleForFullScreen() {
4173         getTaskSize(mTempRect);
4174         return getPagedViewOrientedState().getFullScreenScaleAndPivot(
4175                 mTempRect, mActivity.getDeviceProfile(), mTempPointF);
4176     }
4177 
4178     public PendingAnimation createTaskLaunchAnimation(
4179             TaskView tv, long duration, Interpolator interpolator) {
4180         if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) {
4181             throw new IllegalStateException("Another pending animation is still running");
4182         }
4183 
4184         int count = getTaskViewCount();
4185         if (count == 0) {
4186             return new PendingAnimation(duration);
4187         }
4188 
4189         // When swiping down from overview to tasks, ensures the snapped page's scroll maintain
4190         // invariant between quick switch and overview, to ensure a smooth animation transition.
4191         updateGridProperties();
4192         updateScrollSynchronously();
4193 
4194         int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags();
4195         final boolean[] passedOverviewThreshold = new boolean[] {false};
4196         ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1);
4197         progressAnim.addUpdateListener(animator -> {
4198             // Once we pass a certain threshold, update the sysui flags to match the target
4199             // tasks' flags
4200             if (animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD) {
4201                 mActivity.getSystemUiController().updateUiState(
4202                         UI_STATE_FULLSCREEN_TASK, targetSysUiFlags);
4203             } else {
4204                 mActivity.getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, 0);
4205             }
4206 
4207             // Passing the threshold from taskview to fullscreen app will vibrate
4208             final boolean passed = animator.getAnimatedFraction() >=
4209                     SUCCESS_TRANSITION_PROGRESS;
4210             if (passed != passedOverviewThreshold[0]) {
4211                 passedOverviewThreshold[0] = passed;
4212                 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
4213                         HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
4214             }
4215         });
4216 
4217         AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv);
4218 
4219         DepthController depthController = getDepthController();
4220         if (depthController != null) {
4221             ObjectAnimator depthAnimator = ObjectAnimator.ofFloat(depthController, DEPTH,
4222                     BACKGROUND_APP.getDepth(mActivity));
4223             anim.play(depthAnimator);
4224         }
4225         anim.play(progressAnim);
4226         anim.setInterpolator(interpolator);
4227 
4228         mPendingAnimation = new PendingAnimation(duration);
4229         mPendingAnimation.add(anim);
4230         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
4231             runActionOnRemoteHandles(
4232                     remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
4233                             .addOverviewToAppAnim(mPendingAnimation, interpolator));
4234             mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
4235         }
4236         mPendingAnimation.addEndListener(isSuccess -> {
4237             if (isSuccess) {
4238                 if (tv.getTaskIds()[1] != -1) {
4239                     // TODO(b/194414938): make this part of the animations instead.
4240                     TaskViewUtils.setSplitAuxiliarySurfacesShown(mRemoteTargetHandles[0]
4241                             .getTransformParams().getTargetSet().nonApps,
4242                             true /*shown*/, false /*animate*/);
4243                 }
4244                 if (ENABLE_QUICKSTEP_LIVE_TILE.get() && tv.isRunningTask()) {
4245                     finishRecentsAnimation(false /* toRecents */, null);
4246                     onTaskLaunchAnimationEnd(true /* success */);
4247                 } else {
4248                     tv.launchTask(this::onTaskLaunchAnimationEnd);
4249                 }
4250                 Task task = tv.getTask();
4251                 if (task != null) {
4252                     mActivity.getStatsLogManager().logger().withItemInfo(tv.getItemInfo())
4253                             .log(LAUNCHER_TASK_LAUNCH_SWIPE_DOWN);
4254                 }
4255             } else {
4256                 onTaskLaunchAnimationEnd(false);
4257             }
4258             mPendingAnimation = null;
4259         });
4260         return mPendingAnimation;
4261     }
4262 
onTaskLaunchAnimationEnd(boolean success)4263     protected void onTaskLaunchAnimationEnd(boolean success) {
4264         if (success) {
4265             resetTaskVisuals();
4266         }
4267     }
4268 
4269     @Override
notifyPageSwitchListener(int prevPage)4270     protected void notifyPageSwitchListener(int prevPage) {
4271         super.notifyPageSwitchListener(prevPage);
4272         updateCurrentTaskActionsVisibility();
4273         loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
4274         updateEnabledOverlays();
4275     }
4276 
4277     @Override
getCurrentPageDescription()4278     protected String getCurrentPageDescription() {
4279         return "";
4280     }
4281 
4282     @Override
addChildrenForAccessibility(ArrayList<View> outChildren)4283     public void addChildrenForAccessibility(ArrayList<View> outChildren) {
4284         // Add children in reverse order
4285         for (int i = getChildCount() - 1; i >= 0; --i) {
4286             outChildren.add(getChildAt(i));
4287         }
4288     }
4289 
4290     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)4291     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
4292         super.onInitializeAccessibilityNodeInfo(info);
4293         final AccessibilityNodeInfo.CollectionInfo
4294                 collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain(
4295                 1, getTaskViewCount(), false,
4296                 AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE);
4297         info.setCollectionInfo(collectionInfo);
4298     }
4299 
4300     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)4301     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
4302         super.onInitializeAccessibilityEvent(event);
4303 
4304         final int taskViewCount = getTaskViewCount();
4305         event.setScrollable(taskViewCount > 0);
4306 
4307         if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
4308             final int[] visibleTasks = getVisibleChildrenRange();
4309             event.setFromIndex(taskViewCount - visibleTasks[1]);
4310             event.setToIndex(taskViewCount - visibleTasks[0]);
4311             event.setItemCount(taskViewCount);
4312         }
4313     }
4314 
4315     @Override
getAccessibilityClassName()4316     public CharSequence getAccessibilityClassName() {
4317         // To hear position-in-list related feedback from Talkback.
4318         return ListView.class.getName();
4319     }
4320 
4321     @Override
isPageOrderFlipped()4322     protected boolean isPageOrderFlipped() {
4323         return true;
4324     }
4325 
setEnableDrawingLiveTile(boolean enableDrawingLiveTile)4326     public void setEnableDrawingLiveTile(boolean enableDrawingLiveTile) {
4327         mEnableDrawingLiveTile = enableDrawingLiveTile;
4328     }
4329 
redrawLiveTile()4330     public void redrawLiveTile() {
4331         runActionOnRemoteHandles(remoteTargetHandle -> {
4332             TransformParams params = remoteTargetHandle.getTransformParams();
4333             if (params.getTargetSet() != null) {
4334                 remoteTargetHandle.getTaskViewSimulator().apply(params);
4335             }
4336         });
4337     }
4338 
getRemoteTargetHandles()4339     public RemoteTargetHandle[] getRemoteTargetHandles() {
4340         return mRemoteTargetHandles;
4341     }
4342 
4343     // TODO: To be removed in a follow up CL
setRecentsAnimationTargets(RecentsAnimationController recentsAnimationController, RecentsAnimationTargets recentsAnimationTargets)4344     public void setRecentsAnimationTargets(RecentsAnimationController recentsAnimationController,
4345             RecentsAnimationTargets recentsAnimationTargets) {
4346         mRecentsAnimationController = recentsAnimationController;
4347         mSplitSelectStateController.setRecentsAnimationRunning(true);
4348         if (recentsAnimationTargets == null || recentsAnimationTargets.apps.length == 0) {
4349             return;
4350         }
4351 
4352         RemoteTargetGluer gluer = new RemoteTargetGluer(getContext(), getSizeStrategy());
4353         mRemoteTargetHandles = gluer.assignTargetsForSplitScreen(recentsAnimationTargets);
4354         mSplitBoundsConfig = gluer.getStagedSplitBounds();
4355         // Add release check to the targets from the RemoteTargetGluer and not the targets
4356         // passed in because in the event we're in split screen, we use the passed in targets
4357         // to create new RemoteAnimationTargets in assignTargetsForSplitScreen(), and the
4358         // mSyncTransactionApplier doesn't get transferred over
4359         runActionOnRemoteHandles(remoteTargetHandle -> {
4360             final TransformParams params = remoteTargetHandle.getTransformParams();
4361             if (mSyncTransactionApplier != null) {
4362                 params.setSyncTransactionApplier(mSyncTransactionApplier);
4363                 params.getTargetSet().addReleaseCheck(mSyncTransactionApplier);
4364             }
4365 
4366             TaskViewSimulator tvs = remoteTargetHandle.getTaskViewSimulator();
4367             tvs.setOrientationState(mOrientationState);
4368             tvs.setDp(mActivity.getDeviceProfile());
4369             tvs.recentsViewScale.value = 1;
4370         });
4371 
4372         TaskView runningTaskView = getRunningTaskView();
4373         if (runningTaskView instanceof GroupedTaskView) {
4374             // We initially create a GroupedTaskView in showCurrentTask() before launcher even
4375             // receives the leashes for the remote apps, so the mSplitBoundsConfig that gets passed
4376             // in there is either null or outdated, so we need to update here as soon as we're
4377             // notified.
4378             ((GroupedTaskView) runningTaskView).updateSplitBoundsConfig(mSplitBoundsConfig);
4379         }
4380     }
4381 
4382     /** Helper to avoid writing some for-loops to iterate over {@link #mRemoteTargetHandles} */
runActionOnRemoteHandles(Consumer<RemoteTargetHandle> consumer)4383     public void runActionOnRemoteHandles(Consumer<RemoteTargetHandle> consumer) {
4384         if (mRemoteTargetHandles == null) {
4385             return;
4386         }
4387 
4388         for (RemoteTargetHandle handle : mRemoteTargetHandles) {
4389             consumer.accept(handle);
4390         }
4391     }
4392 
4393     /**
4394      * Finish recents animation.
4395      */
finishRecentsAnimation(boolean toRecents, @Nullable Runnable onFinishComplete)4396     public void finishRecentsAnimation(boolean toRecents, @Nullable Runnable onFinishComplete) {
4397         finishRecentsAnimation(toRecents, true /* shouldPip */, onFinishComplete);
4398     }
4399 
finishRecentsAnimation(boolean toRecents, boolean shouldPip, @Nullable Runnable onFinishComplete)4400     public void finishRecentsAnimation(boolean toRecents, boolean shouldPip,
4401             @Nullable Runnable onFinishComplete) {
4402         // TODO(b/197232424#comment#10) Move this back into onRecentsAnimationComplete(). Maybe?
4403         cleanupRemoteTargets();
4404         if (!toRecents && ENABLE_QUICKSTEP_LIVE_TILE.get()) {
4405             // Reset the minimized state since we force-toggled the minimized state when entering
4406             // overview, but never actually finished the recents animation.  This is a catch all for
4407             // cases where we haven't already reset it.
4408             SystemUiProxy p = SystemUiProxy.INSTANCE.getNoCreate();
4409             if (p != null) {
4410                 p.setSplitScreenMinimized(false);
4411             }
4412         }
4413 
4414         if (mRecentsAnimationController == null) {
4415             if (onFinishComplete != null) {
4416                 onFinishComplete.run();
4417             }
4418             return;
4419         }
4420 
4421         final boolean sendUserLeaveHint = toRecents && shouldPip;
4422         if (sendUserLeaveHint) {
4423             // Notify the SysUI to use fade-in animation when entering PiP from live tile.
4424             final SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(getContext());
4425             systemUiProxy.notifySwipeToHomeFinished();
4426             systemUiProxy.setShelfHeight(true, mActivity.getDeviceProfile().hotseatBarSizePx);
4427         }
4428         mRecentsAnimationController.finish(toRecents, () -> {
4429             if (onFinishComplete != null) {
4430                 onFinishComplete.run();
4431             }
4432             onRecentsAnimationComplete();
4433         }, sendUserLeaveHint);
4434     }
4435 
4436     /**
4437      * Called when a running recents animation has finished or canceled.
4438      */
onRecentsAnimationComplete()4439     public void onRecentsAnimationComplete() {
4440         // At this point, the recents animation is not running and if the animation was canceled
4441         // by a display rotation then reset this state to show the screenshot
4442         setRunningTaskViewShowScreenshot(true);
4443         // After we finish the recents animation, the current task id should be correctly
4444         // reset so that when the task is launched from Overview later, it goes through the
4445         // flow of starting a new task instead of finishing recents animation to app. A
4446         // typical example of this is (1) user swipes up from app to Overview (2) user
4447         // taps on QSB (3) user goes back to Overview and launch the most recent task.
4448         setCurrentTask(-1);
4449         mRecentsAnimationController = null;
4450         mSplitSelectStateController.setRecentsAnimationRunning(false);
4451         executeSideTaskLaunchCallback();
4452     }
4453 
setDisallowScrollToClearAll(boolean disallowScrollToClearAll)4454     public void setDisallowScrollToClearAll(boolean disallowScrollToClearAll) {
4455         if (mDisallowScrollToClearAll != disallowScrollToClearAll) {
4456             mDisallowScrollToClearAll = disallowScrollToClearAll;
4457             updateMinAndMaxScrollX();
4458         }
4459     }
4460 
4461     /**
4462      * Updates page scroll synchronously after measure and layout child views.
4463      */
updateScrollSynchronously()4464     public void updateScrollSynchronously() {
4465         // onMeasure is needed to update child's measured width which is used in scroll calculation,
4466         // in case TaskView sizes has changed when being focused/unfocused.
4467         onMeasure(makeMeasureSpec(getMeasuredWidth(), EXACTLY),
4468                 makeMeasureSpec(getMeasuredHeight(), EXACTLY));
4469         onLayout(false /*  changed */, getLeft(), getTop(), getRight(), getBottom());
4470         updateMinAndMaxScrollX();
4471     }
4472 
4473     @Override
updateMinAndMaxScrollX()4474     protected void updateMinAndMaxScrollX() {
4475         super.updateMinAndMaxScrollX();
4476         if (DEBUG) {
4477             Log.d(TAG, "updateMinAndMaxScrollX - mMinScroll: " + mMinScroll);
4478             Log.d(TAG, "updateMinAndMaxScrollX - mMaxScroll: " + mMaxScroll);
4479         }
4480     }
4481 
4482     @Override
computeMinScroll()4483     protected int computeMinScroll() {
4484         if (getTaskViewCount() <= 0) {
4485             return super.computeMinScroll();
4486         }
4487 
4488         return getScrollForPage(mIsRtl ? getLastViewIndex() : getFirstViewIndex());
4489     }
4490 
4491     @Override
computeMaxScroll()4492     protected int computeMaxScroll() {
4493         if (getTaskViewCount() <= 0) {
4494             return super.computeMaxScroll();
4495         }
4496 
4497         return getScrollForPage(mIsRtl ? getFirstViewIndex() : getLastViewIndex());
4498     }
4499 
getFirstViewIndex()4500     private int getFirstViewIndex() {
4501         TaskView focusedTaskView = mShowAsGridLastOnLayout ? getFocusedTaskView() : null;
4502         return focusedTaskView != null ? indexOfChild(focusedTaskView) : 0;
4503     }
4504 
getLastViewIndex()4505     private int getLastViewIndex() {
4506         return mDisallowScrollToClearAll
4507                 ? mShowAsGridLastOnLayout
4508                     ? indexOfChild(getLastGridTaskView())
4509                     : getTaskViewCount() - 1
4510                 : indexOfChild(mClearAllButton);
4511     }
4512 
4513     /**
4514      * Returns page scroll of ClearAllButton.
4515      */
getClearAllScroll()4516     public int getClearAllScroll() {
4517         return getScrollForPage(indexOfChild(mClearAllButton));
4518     }
4519 
4520     @Override
getPageScrolls(int[] outPageScrolls, boolean layoutChildren, ComputePageScrollsLogic scrollLogic)4521     protected boolean getPageScrolls(int[] outPageScrolls, boolean layoutChildren,
4522             ComputePageScrollsLogic scrollLogic) {
4523         int[] newPageScrolls = new int[outPageScrolls.length];
4524         super.getPageScrolls(newPageScrolls, layoutChildren, scrollLogic);
4525         boolean showAsFullscreen = showAsFullscreen();
4526         boolean showAsGrid = showAsGrid();
4527 
4528         // Align ClearAllButton to the left (RTL) or right (non-RTL), which is different from other
4529         // TaskViews. This must be called after laying out ClearAllButton.
4530         if (layoutChildren) {
4531             int clearAllWidthDiff = mOrientationHandler.getPrimaryValue(mTaskWidth, mTaskHeight)
4532                     - mOrientationHandler.getPrimarySize(mClearAllButton);
4533             mClearAllButton.setScrollOffsetPrimary(mIsRtl ? clearAllWidthDiff : -clearAllWidthDiff);
4534         }
4535 
4536         boolean pageScrollChanged = false;
4537 
4538         int clearAllIndex = indexOfChild(mClearAllButton);
4539         int clearAllScroll = 0;
4540         int clearAllWidth = mOrientationHandler.getPrimarySize(mClearAllButton);
4541         if (clearAllIndex != -1 && clearAllIndex < outPageScrolls.length) {
4542             float scrollDiff = mClearAllButton.getScrollAdjustment(showAsFullscreen, showAsGrid);
4543             clearAllScroll = newPageScrolls[clearAllIndex] + (int) scrollDiff;
4544             if (outPageScrolls[clearAllIndex] != clearAllScroll) {
4545                 pageScrollChanged = true;
4546                 outPageScrolls[clearAllIndex] = clearAllScroll;
4547             }
4548         }
4549 
4550         final int taskCount = getTaskViewCount();
4551         for (int i = 0; i < taskCount; i++) {
4552             TaskView taskView = requireTaskViewAt(i);
4553             float scrollDiff = taskView.getScrollAdjustment(showAsFullscreen, showAsGrid);
4554             int pageScroll = newPageScrolls[i] + (int) scrollDiff;
4555             if ((mIsRtl && pageScroll < clearAllScroll + clearAllWidth)
4556                     || (!mIsRtl && pageScroll > clearAllScroll - clearAllWidth)) {
4557                 pageScroll = clearAllScroll + (mIsRtl ? clearAllWidth : -clearAllWidth);
4558             }
4559             if (outPageScrolls[i] != pageScroll) {
4560                 pageScrollChanged = true;
4561                 outPageScrolls[i] = pageScroll;
4562             }
4563             if (DEBUG) {
4564                 Log.d(TAG, "getPageScrolls - outPageScrolls[" + i + "]: " + outPageScrolls[i]);
4565             }
4566         }
4567         if (DEBUG) {
4568             Log.d(TAG, "getPageScrolls - clearAllScroll: " + clearAllScroll);
4569         }
4570         return pageScrollChanged;
4571     }
4572 
4573     @Override
getChildOffset(int index)4574     protected int getChildOffset(int index) {
4575         int childOffset = super.getChildOffset(index);
4576         View child = getChildAt(index);
4577         if (child instanceof TaskView) {
4578             childOffset += ((TaskView) child).getOffsetAdjustment(showAsFullscreen(),
4579                     showAsGrid());
4580         } else if (child instanceof ClearAllButton) {
4581             childOffset += ((ClearAllButton) child).getOffsetAdjustment(mOverviewFullscreenEnabled,
4582                     showAsGrid());
4583         }
4584         return childOffset;
4585     }
4586 
4587     @Override
getChildVisibleSize(int index)4588     protected int getChildVisibleSize(int index) {
4589         final TaskView taskView = getTaskViewAt(index);
4590         if (taskView == null) {
4591             return super.getChildVisibleSize(index);
4592         }
4593         return (int) (super.getChildVisibleSize(index) * taskView.getSizeAdjustment(
4594                 showAsFullscreen()));
4595     }
4596 
getClearAllButton()4597     public ClearAllButton getClearAllButton() {
4598         return mClearAllButton;
4599     }
4600 
4601     /**
4602      * @return How many pixels the running task is offset on the currently laid out dominant axis.
4603      */
getScrollOffset()4604     public int getScrollOffset() {
4605         return getScrollOffset(getRunningTaskIndex());
4606     }
4607 
4608     /**
4609      * Returns how many pixels the page is offset on the currently laid out dominant axis.
4610      */
getScrollOffset(int pageIndex)4611     public int getScrollOffset(int pageIndex) {
4612         if (pageIndex == -1) {
4613             return 0;
4614         }
4615 
4616         int overScrollShift = getOverScrollShift();
4617         if (mAdjacentPageHorizontalOffset > 0) {
4618             // Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so
4619             // that the page can move freely given there's no visual indication why it shouldn't.
4620             overScrollShift = (int) Utilities.mapRange(mAdjacentPageHorizontalOffset,
4621                     overScrollShift, getUndampedOverScrollShift());
4622         }
4623         return getScrollForPage(pageIndex) - mOrientationHandler.getPrimaryScroll(this)
4624                 + overScrollShift + getOffsetFromScrollPosition(pageIndex);
4625     }
4626 
4627     /**
4628      * Returns how many pixels the page is offset from its scroll position.
4629      */
getOffsetFromScrollPosition(int pageIndex)4630     private int getOffsetFromScrollPosition(int pageIndex) {
4631         return getOffsetFromScrollPosition(pageIndex, getTopRowIdArray(), getBottomRowIdArray());
4632     }
4633 
getOffsetFromScrollPosition( int pageIndex, IntArray topRowIdArray, IntArray bottomRowIdArray)4634     private int getOffsetFromScrollPosition(
4635             int pageIndex, IntArray topRowIdArray, IntArray bottomRowIdArray) {
4636         if (!showAsGrid()) {
4637             return 0;
4638         }
4639 
4640         TaskView taskView = getTaskViewAt(pageIndex);
4641         if (taskView == null) {
4642             return 0;
4643         }
4644 
4645         TaskView lastGridTaskView = getLastGridTaskView(topRowIdArray, bottomRowIdArray);
4646         if (lastGridTaskView == null) {
4647             return 0;
4648         }
4649 
4650         if (getScrollForPage(pageIndex) != getScrollForPage(indexOfChild(lastGridTaskView))) {
4651             return 0;
4652         }
4653 
4654         // Check distance from lastGridTaskView to taskView.
4655         int lastGridTaskViewPosition =
4656                 getPositionInRow(lastGridTaskView, topRowIdArray, bottomRowIdArray);
4657         int taskViewPosition = getPositionInRow(taskView, topRowIdArray, bottomRowIdArray);
4658         int gridTaskSizeAndSpacing = mLastComputedGridTaskSize.width() + mPageSpacing;
4659         int positionDiff = gridTaskSizeAndSpacing * (lastGridTaskViewPosition - taskViewPosition);
4660 
4661         int lastTaskEnd = (mIsRtl
4662                 ? mLastComputedGridSize.left
4663                 : mLastComputedGridSize.right)
4664                 + (mIsRtl ? mPageSpacing : -mPageSpacing);
4665         int taskEnd = lastTaskEnd + (mIsRtl ? positionDiff : -positionDiff);
4666         int normalTaskEnd = mIsRtl
4667                 ? mLastComputedGridTaskSize.left
4668                 : mLastComputedGridTaskSize.right;
4669         return taskEnd - normalTaskEnd;
4670     }
4671 
getPositionInRow( TaskView taskView, IntArray topRowIdArray, IntArray bottomRowIdArray)4672     private int getPositionInRow(
4673             TaskView taskView, IntArray topRowIdArray, IntArray bottomRowIdArray) {
4674         int position = topRowIdArray.indexOf(taskView.getTaskViewId());
4675         return position != -1 ? position : bottomRowIdArray.indexOf(taskView.getTaskViewId());
4676     }
4677 
4678     /**
4679      * @return true if the task in on the top of the grid
4680      */
isOnGridBottomRow(TaskView taskView)4681     public boolean isOnGridBottomRow(TaskView taskView) {
4682         return showAsGrid()
4683                 && !mTopRowIdSet.contains(taskView.getTaskViewId())
4684                 && taskView.getTaskViewId() != mFocusedTaskViewId;
4685     }
4686 
getEventDispatcher(float navbarRotation)4687     public Consumer<MotionEvent> getEventDispatcher(float navbarRotation) {
4688         float degreesRotated;
4689         if (navbarRotation == 0) {
4690             degreesRotated = mOrientationHandler.getDegreesRotated();
4691         } else {
4692             degreesRotated = -navbarRotation;
4693         }
4694         if (degreesRotated == 0) {
4695             return super::onTouchEvent;
4696         }
4697 
4698         // At this point the event coordinates have already been transformed, so we need to
4699         // undo that transformation since PagedView also accommodates for the transformation via
4700         // PagedOrientationHandler
4701         return e -> {
4702             if (navbarRotation != 0
4703                     && mOrientationState.isMultipleOrientationSupportedByDevice()
4704                     && !mOrientationState.getOrientationHandler().isLayoutNaturalToLauncher()) {
4705                 mOrientationState.flipVertical(e);
4706                 super.onTouchEvent(e);
4707                 mOrientationState.flipVertical(e);
4708                 return;
4709             }
4710             mOrientationState.transformEvent(-degreesRotated, e, true);
4711             super.onTouchEvent(e);
4712             mOrientationState.transformEvent(-degreesRotated, e, false);
4713         };
4714     }
4715 
4716     private void updateEnabledOverlays() {
4717         int overlayEnabledPage = mOverlayEnabled ? getNextPage() : -1;
4718         int taskCount = getTaskViewCount();
4719         for (int i = 0; i < taskCount; i++) {
4720             requireTaskViewAt(i).setOverlayEnabled(i == overlayEnabledPage);
4721         }
4722     }
4723 
4724     public void setOverlayEnabled(boolean overlayEnabled) {
4725         if (mOverlayEnabled != overlayEnabled) {
4726             mOverlayEnabled = overlayEnabled;
4727             updateEnabledOverlays();
4728         }
4729     }
4730 
4731     public void setOverviewGridEnabled(boolean overviewGridEnabled) {
4732         if (mOverviewGridEnabled != overviewGridEnabled) {
4733             mOverviewGridEnabled = overviewGridEnabled;
4734             updateActionsViewFocusedScroll();
4735             // Request layout to ensure scroll position is recalculated with updated mGridProgress.
4736             requestLayout();
4737         }
4738     }
4739 
4740     public void setOverviewFullscreenEnabled(boolean overviewFullscreenEnabled) {
4741         if (mOverviewFullscreenEnabled != overviewFullscreenEnabled) {
4742             mOverviewFullscreenEnabled = overviewFullscreenEnabled;
4743             // Request layout to ensure scroll position is recalculated with updated
4744             // mFullscreenProgress.
4745             requestLayout();
4746         }
4747     }
4748 
4749     /**
4750      * Switch the current running task view to static snapshot mode,
4751      * capturing the snapshot at the same time.
4752      */
4753     public void switchToScreenshot(Runnable onFinishRunnable) {
4754         if (mRecentsAnimationController == null) {
4755             if (onFinishRunnable != null) {
4756                 onFinishRunnable.run();
4757             }
4758             return;
4759         }
4760 
4761         switchToScreenshotInternal(onFinishRunnable);
4762     }
4763 
4764     private void switchToScreenshotInternal(Runnable onFinishRunnable) {
4765         TaskView taskView = getRunningTaskView();
4766         if (taskView == null) {
4767             onFinishRunnable.run();
4768             return;
4769         }
4770 
4771         taskView.setShowScreenshot(true);
4772         for (TaskView.TaskIdAttributeContainer container :
4773                 taskView.getTaskIdAttributeContainers()) {
4774             if (container == null) {
4775                 continue;
4776             }
4777 
4778             ThumbnailData td =
4779                     mRecentsAnimationController.screenshotTask(container.getTask().key.id);
4780             TaskThumbnailView thumbnailView = container.getThumbnailView();
4781             if (td != null) {
4782                 thumbnailView.setThumbnail(container.getTask(), td);
4783             } else {
4784                 thumbnailView.refresh();
4785             }
4786         }
4787         ViewUtils.postFrameDrawn(taskView, onFinishRunnable);
4788     }
4789 
4790     /**
4791      * Switch the current running task view to static snapshot mode, using the
4792      * provided thumbnail data as the snapshot.
4793      * TODO(b/195609063) Consolidate this method w/ the one above, except this thumbnail data comes
4794      *  from gesture state, which is a larger change of it having to keep track of multiple tasks.
4795      *  OR. Maybe it doesn't need to pass in a thumbnail and we can use the exact same flow as above
4796      */
4797     public void switchToScreenshot(@Nullable HashMap<Integer, ThumbnailData> thumbnailDatas,
4798             Runnable onFinishRunnable) {
4799         final TaskView taskView = getRunningTaskView();
4800         if (taskView != null) {
4801             taskView.setShowScreenshot(true);
4802             taskView.refreshThumbnails(thumbnailDatas);
4803             ViewUtils.postFrameDrawn(taskView, onFinishRunnable);
4804         } else {
4805             onFinishRunnable.run();
4806         }
4807     }
4808 
4809     /**
4810      * The current task is fully modal (modalness = 1) when it is shown on its own in a modal
4811      * way. Modalness 0 means the task is shown in context with all the other tasks.
4812      */
4813     private void setTaskModalness(float modalness) {
4814         mTaskModalness = modalness;
4815         updatePageOffsets();
4816         if (getCurrentPageTaskView() != null) {
4817             getCurrentPageTaskView().setModalness(modalness);
4818         }
4819         // Only show actions view when it's modal for in-place landscape mode.
4820         boolean inPlaceLandscape = !mOrientationState.isRecentsActivityRotationAllowed()
4821                 && mOrientationState.getTouchRotation() != ROTATION_0;
4822         mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION, modalness < 1 && inPlaceLandscape);
4823     }
4824 
4825     @Nullable
4826     protected DepthController getDepthController() {
4827         return null;
4828     }
4829 
4830     @Override
4831     public void onSecondaryWindowBoundsChanged() {
4832         // Invalidate the task view size
4833         setInsets(mInsets);
4834     }
4835 
4836     /**
4837      * Enables or disables modal state for RecentsView
4838      * @param isModalState
4839      */
4840     public void setModalStateEnabled(boolean isModalState) { }
4841 
4842     public TaskOverlayFactory getTaskOverlayFactory() {
4843         return mTaskOverlayFactory;
4844     }
4845 
4846     public BaseActivityInterface getSizeStrategy() {
4847         return mSizeStrategy;
4848     }
4849 
4850     /**
4851      * Set all the task views to color tint scrim mode, dimming or tinting them all. Allows the
4852      * tasks to be dimmed while other elements in the recents view are left alone.
4853      */
4854     public void showForegroundScrim(boolean show) {
4855         if (!show && mColorTint == 0) {
4856             if (mTintingAnimator != null) {
4857                 mTintingAnimator.cancel();
4858                 mTintingAnimator = null;
4859             }
4860             return;
4861         }
4862 
4863         mTintingAnimator = ObjectAnimator.ofFloat(this, COLOR_TINT, show ? 0.5f : 0f);
4864         mTintingAnimator.setAutoCancel(true);
4865         mTintingAnimator.start();
4866     }
4867 
4868     /** Tint the RecentsView and TaskViews in to simulate a scrim. */
4869     // TODO(b/187528071): Replace this tinting with a scrim on top of RecentsView
4870     private void setColorTint(float tintAmount) {
4871         mColorTint = tintAmount;
4872 
4873         for (int i = 0; i < getTaskViewCount(); i++) {
4874             requireTaskViewAt(i).setColorTint(mColorTint, mTintingColor);
4875         }
4876 
4877         Drawable scrimBg = mActivity.getScrimView().getBackground();
4878         if (scrimBg != null) {
4879             if (tintAmount == 0f) {
4880                 scrimBg.setTintList(null);
4881             } else {
4882                 scrimBg.setTintBlendMode(BlendMode.SRC_OVER);
4883                 scrimBg.setTint(
4884                         ColorUtils.setAlphaComponent(mTintingColor, (int) (255 * tintAmount)));
4885             }
4886         }
4887     }
4888 
4889     private float getColorTint() {
4890         return mColorTint;
4891     }
4892 
4893     /** Returns {@code true} if the overview tasks are displayed as a grid. */
4894     public boolean showAsGrid() {
4895         return mOverviewGridEnabled || (mCurrentGestureEndTarget != null
4896                 && mSizeStrategy.stateFromGestureEndTarget(
4897                 mCurrentGestureEndTarget).displayOverviewTasksAsGrid(mActivity.getDeviceProfile()));
4898     }
4899 
4900     private boolean showAsFullscreen() {
4901         return mOverviewFullscreenEnabled
4902                 && mCurrentGestureEndTarget != GestureState.GestureEndTarget.RECENTS;
4903     }
4904 
4905     public void cleanupRemoteTargets() {
4906         mRemoteTargetHandles = null;
4907     }
4908 
4909     /**
4910      * Used to register callbacks for when our empty message state changes.
4911      *
4912      * @see #setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener)
4913      * @see #updateEmptyMessage()
4914      */
4915     public interface OnEmptyMessageUpdatedListener {
4916         /** @param isEmpty Whether RecentsView is empty (i.e. has no children) */
4917         void onEmptyMessageUpdated(boolean isEmpty);
4918     }
4919 
4920     /**
4921      * Adds a listener for scroll changes
4922      */
4923     public void addOnScrollChangedListener(OnScrollChangedListener listener) {
4924         mScrollListeners.add(listener);
4925     }
4926 
4927     /**
4928      * Removes a previously added scroll change listener
4929      */
4930     public void removeOnScrollChangedListener(OnScrollChangedListener listener) {
4931         mScrollListeners.remove(listener);
4932     }
4933 
4934     /**
4935      * @return Corner radius in pixel value for PiP window, which is updated via
4936      *         {@link #mIPipAnimationListener}
4937      */
4938     public int getPipCornerRadius() {
4939         return mPipCornerRadius;
4940     }
4941 
4942     @Override
4943     public boolean scrollLeft() {
4944         if (!showAsGrid()) {
4945             return super.scrollLeft();
4946         }
4947 
4948         int targetPage = getNextPage();
4949         if (targetPage >= 0) {
4950             // Find the next page that is not fully visible.
4951             TaskView taskView = getTaskViewAt(targetPage);
4952             while ((taskView == null || isTaskViewFullyVisible(taskView)) && targetPage - 1 >= 0) {
4953                 taskView = getTaskViewAt(--targetPage);
4954             }
4955             // Target a scroll where targetPage is on left of screen but still fully visible.
4956             int lastTaskEnd = (mIsRtl
4957                     ? mLastComputedGridSize.left
4958                     : mLastComputedGridSize.right)
4959                     + (mIsRtl ? mPageSpacing : -mPageSpacing);
4960             int normalTaskEnd = mIsRtl
4961                     ? mLastComputedGridTaskSize.left
4962                     : mLastComputedGridTaskSize.right;
4963             int targetScroll = getScrollForPage(targetPage) + normalTaskEnd - lastTaskEnd;
4964             // Find a page that is close to targetScroll while not over it.
4965             while (targetPage - 1 >= 0
4966                     && (mIsRtl
4967                     ? getScrollForPage(targetPage - 1) < targetScroll
4968                     : getScrollForPage(targetPage - 1) > targetScroll)) {
4969                 targetPage--;
4970             }
4971             snapToPage(targetPage);
4972             return true;
4973         }
4974 
4975         return mAllowOverScroll;
4976     }
4977 
4978     @Override
4979     public boolean scrollRight() {
4980         if (!showAsGrid()) {
4981             return super.scrollRight();
4982         }
4983 
4984         int targetPage = getNextPage();
4985         if (targetPage < getChildCount()) {
4986             // Find the next page that is not fully visible.
4987             TaskView taskView = getTaskViewAt(targetPage);
4988             while ((taskView != null && isTaskViewFullyVisible(taskView))
4989                     && targetPage + 1 < getChildCount()) {
4990                 taskView = getTaskViewAt(++targetPage);
4991             }
4992             snapToPage(targetPage);
4993             return true;
4994         }
4995         return mAllowOverScroll;
4996     }
4997 
4998     @Override
4999     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
5000         super.onScrollChanged(l, t, oldl, oldt);
5001         dispatchScrollChanged();
5002     }
5003 
5004     private void dispatchScrollChanged() {
5005         runActionOnRemoteHandles(remoteTargetHandle ->
5006                 remoteTargetHandle.getTaskViewSimulator().setScroll(getScrollOffset()));
5007         for (int i = mScrollListeners.size() - 1; i >= 0; i--) {
5008             mScrollListeners.get(i).onScrollChanged();
5009         }
5010     }
5011 
5012     private static class PinnedStackAnimationListener<T extends BaseActivity> extends
5013             IPipAnimationListener.Stub {
5014         @Nullable
5015         private T mActivity;
5016         @Nullable
5017         private RecentsView mRecentsView;
5018 
5019         public void setActivityAndRecentsView(@Nullable T activity,
5020                 @Nullable RecentsView recentsView) {
5021             mActivity = activity;
5022             mRecentsView = recentsView;
5023         }
5024 
5025         @Override
5026         public void onPipAnimationStarted() {
5027             MAIN_EXECUTOR.execute(() -> {
5028                 // Needed for activities that auto-enter PiP, which will not trigger a remote
5029                 // animation to be created
5030                 if (mActivity != null) {
5031                     mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
5032                 }
5033             });
5034         }
5035 
5036         @Override
5037         public void onPipCornerRadiusChanged(int cornerRadius) {
5038             if (mRecentsView != null) {
5039                 mRecentsView.mPipCornerRadius = cornerRadius;
5040             }
5041         }
5042     }
5043 
5044     /** Get the color used for foreground scrimming the RecentsView for sharing. */
5045     public static int getForegroundScrimDimColor(Context context) {
5046         int baseColor = Themes.getAttrColor(context, R.attr.overviewScrimColor);
5047         // The Black blending is temporary until we have the proper color token.
5048         return ColorUtils.blendARGB(Color.BLACK, baseColor, 0.25f);
5049     }
5050 
5051     /** Get the RecentsAnimationController */
5052     @Nullable
5053     public RecentsAnimationController getRecentsAnimationController() {
5054         return mRecentsAnimationController;
5055     }
5056 
5057     /** Update the current activity locus id to show the enabled state of Overview */
5058     public void updateLocusId() {
5059         String locusId = "Overview";
5060 
5061         if (mOverviewStateEnabled && mActivity.isStarted()) {
5062             locusId += "|ENABLED";
5063         } else {
5064             locusId += "|DISABLED";
5065         }
5066 
5067         final LocusId id = new LocusId(locusId);
5068         // Set locus context is a binder call, don't want it to happen during a transition
5069         UI_HELPER_EXECUTOR.post(() -> mActivity.setLocusContext(id, Bundle.EMPTY));
5070     }
5071 
5072     public interface TaskLaunchListener {
5073         void onTaskLaunched();
5074     }
5075 }
5076