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.app.ActivityTaskManager.INVALID_TASK_ID;
20 import static android.view.Surface.ROTATION_0;
21 import static android.view.View.MeasureSpec.EXACTLY;
22 import static android.view.View.MeasureSpec.makeMeasureSpec;
23 
24 import static com.android.app.animation.Interpolators.ACCELERATE;
25 import static com.android.app.animation.Interpolators.ACCELERATE_0_75;
26 import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
27 import static com.android.app.animation.Interpolators.DECELERATE_2;
28 import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
29 import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN;
30 import static com.android.app.animation.Interpolators.FINAL_FRAME;
31 import static com.android.app.animation.Interpolators.LINEAR;
32 import static com.android.app.animation.Interpolators.OVERSHOOT_0_75;
33 import static com.android.app.animation.Interpolators.clampToProgress;
34 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
35 import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU;
36 import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
37 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
38 import static com.android.launcher3.Flags.enableAdditionalHomeAnimations;
39 import static com.android.launcher3.Flags.enableGridOnlyOverview;
40 import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
41 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
42 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
43 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
44 import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION;
45 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
46 import static com.android.launcher3.Utilities.mapToRange;
47 import static com.android.launcher3.Utilities.squaredHypot;
48 import static com.android.launcher3.Utilities.squaredTouchSlop;
49 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_ACTIONS_SPLIT;
50 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_ORIENTATION_CHANGED;
51 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_CLEAR_ALL;
52 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP;
53 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN;
54 import static com.android.launcher3.testing.shared.TestProtocol.DISMISS_ANIMATION_ENDS_MESSAGE;
55 import static com.android.launcher3.touch.PagedOrientationHandler.CANVAS_TRANSLATE;
56 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
57 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
58 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
59 import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
60 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
61 import static com.android.quickstep.util.LogUtils.splitFailureMessage;
62 import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_DOWN;
63 import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_LEFT;
64 import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_RIGHT;
65 import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_TAB;
66 import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_UP;
67 import static com.android.quickstep.views.ClearAllButton.DISMISS_ALPHA;
68 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_ACTIONS_IN_MENU;
69 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_DESKTOP;
70 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NON_ZERO_ROTATION;
71 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_RECENTS;
72 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_TASKS;
73 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_SPLIT_SCREEN;
74 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_SPLIT_SELECT_ACTIVE;
75 
76 import android.animation.Animator;
77 import android.animation.AnimatorListenerAdapter;
78 import android.animation.AnimatorSet;
79 import android.animation.LayoutTransition;
80 import android.animation.LayoutTransition.TransitionListener;
81 import android.animation.ObjectAnimator;
82 import android.animation.PropertyValuesHolder;
83 import android.animation.ValueAnimator;
84 import android.annotation.SuppressLint;
85 import android.app.WindowConfiguration;
86 import android.content.Context;
87 import android.content.Intent;
88 import android.content.LocusId;
89 import android.content.res.Configuration;
90 import android.graphics.Bitmap;
91 import android.graphics.BlendMode;
92 import android.graphics.Canvas;
93 import android.graphics.Matrix;
94 import android.graphics.Point;
95 import android.graphics.PointF;
96 import android.graphics.Rect;
97 import android.graphics.RectF;
98 import android.graphics.Typeface;
99 import android.graphics.drawable.Drawable;
100 import android.os.Bundle;
101 import android.os.SystemClock;
102 import android.os.UserHandle;
103 import android.os.VibrationEffect;
104 import android.text.Layout;
105 import android.text.StaticLayout;
106 import android.text.TextPaint;
107 import android.util.AttributeSet;
108 import android.util.FloatProperty;
109 import android.util.Log;
110 import android.util.Pair;
111 import android.util.SparseBooleanArray;
112 import android.view.HapticFeedbackConstants;
113 import android.view.KeyEvent;
114 import android.view.LayoutInflater;
115 import android.view.MotionEvent;
116 import android.view.RemoteAnimationTarget;
117 import android.view.View;
118 import android.view.ViewDebug;
119 import android.view.ViewGroup;
120 import android.view.ViewTreeObserver.OnScrollChangedListener;
121 import android.view.accessibility.AccessibilityEvent;
122 import android.view.accessibility.AccessibilityNodeInfo;
123 import android.view.animation.Interpolator;
124 import android.widget.ListView;
125 import android.widget.OverScroller;
126 import android.widget.Toast;
127 import android.window.PictureInPictureSurfaceTransaction;
128 
129 import androidx.annotation.NonNull;
130 import androidx.annotation.Nullable;
131 import androidx.annotation.UiThread;
132 import androidx.core.graphics.ColorUtils;
133 
134 import com.android.internal.jank.Cuj;
135 import com.android.launcher3.AbstractFloatingView;
136 import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener;
137 import com.android.launcher3.DeviceProfile;
138 import com.android.launcher3.Flags;
139 import com.android.launcher3.Insettable;
140 import com.android.launcher3.InvariantDeviceProfile;
141 import com.android.launcher3.PagedView;
142 import com.android.launcher3.R;
143 import com.android.launcher3.Utilities;
144 import com.android.launcher3.anim.AnimatedFloat;
145 import com.android.launcher3.anim.AnimatorListeners;
146 import com.android.launcher3.anim.AnimatorPlaybackController;
147 import com.android.launcher3.anim.PendingAnimation;
148 import com.android.launcher3.anim.SpringProperty;
149 import com.android.launcher3.compat.AccessibilityManagerCompat;
150 import com.android.launcher3.config.FeatureFlags;
151 import com.android.launcher3.desktop.DesktopRecentsTransitionController;
152 import com.android.launcher3.logger.LauncherAtom;
153 import com.android.launcher3.logging.StatsLogManager;
154 import com.android.launcher3.model.data.ItemInfo;
155 import com.android.launcher3.statehandlers.DepthController;
156 import com.android.launcher3.statemanager.BaseState;
157 import com.android.launcher3.statemanager.StateManager;
158 import com.android.launcher3.statemanager.StatefulContainer;
159 import com.android.launcher3.testing.TestLogging;
160 import com.android.launcher3.testing.shared.TestProtocol;
161 import com.android.launcher3.touch.OverScroll;
162 import com.android.launcher3.util.CancellableTask;
163 import com.android.launcher3.util.DynamicResource;
164 import com.android.launcher3.util.IntArray;
165 import com.android.launcher3.util.IntSet;
166 import com.android.launcher3.util.ResourceBasedOverride.Overrides;
167 import com.android.launcher3.util.RunnableList;
168 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
169 import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource;
170 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
171 import com.android.launcher3.util.Themes;
172 import com.android.launcher3.util.TraceHelper;
173 import com.android.launcher3.util.TranslateEdgeEffect;
174 import com.android.launcher3.util.VibratorWrapper;
175 import com.android.launcher3.util.ViewPool;
176 import com.android.quickstep.BaseContainerInterface;
177 import com.android.quickstep.GestureState;
178 import com.android.quickstep.OverviewCommandHelper;
179 import com.android.quickstep.RecentsAnimationController;
180 import com.android.quickstep.RecentsAnimationTargets;
181 import com.android.quickstep.RecentsFilterState;
182 import com.android.quickstep.RecentsModel;
183 import com.android.quickstep.RemoteAnimationTargets;
184 import com.android.quickstep.RemoteTargetGluer;
185 import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle;
186 import com.android.quickstep.RotationTouchHelper;
187 import com.android.quickstep.SplitSelectionListener;
188 import com.android.quickstep.SystemUiProxy;
189 import com.android.quickstep.TaskOverlayFactory;
190 import com.android.quickstep.TaskThumbnailCache;
191 import com.android.quickstep.TaskViewUtils;
192 import com.android.quickstep.TopTaskTracker;
193 import com.android.quickstep.ViewUtils;
194 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
195 import com.android.quickstep.recents.data.TasksRepository;
196 import com.android.quickstep.recents.viewmodel.RecentsViewData;
197 import com.android.quickstep.util.ActiveGestureErrorDetector;
198 import com.android.quickstep.util.ActiveGestureLog;
199 import com.android.quickstep.util.AnimUtils;
200 import com.android.quickstep.util.DesktopTask;
201 import com.android.quickstep.util.GroupTask;
202 import com.android.quickstep.util.LayoutUtils;
203 import com.android.quickstep.util.RecentsAtomicAnimationFactory;
204 import com.android.quickstep.util.RecentsOrientedState;
205 import com.android.quickstep.util.SplitAnimationController.Companion.SplitAnimInitProps;
206 import com.android.quickstep.util.SplitAnimationTimings;
207 import com.android.quickstep.util.SplitSelectStateController;
208 import com.android.quickstep.util.SurfaceTransaction;
209 import com.android.quickstep.util.SurfaceTransactionApplier;
210 import com.android.quickstep.util.TaskGridNavHelper;
211 import com.android.quickstep.util.TaskViewSimulator;
212 import com.android.quickstep.util.TaskVisualsChangeListener;
213 import com.android.quickstep.util.TransformParams;
214 import com.android.quickstep.util.VibrationConstants;
215 import com.android.quickstep.views.TaskView.TaskContainer;
216 import com.android.systemui.plugins.ResourceProvider;
217 import com.android.systemui.shared.recents.model.Task;
218 import com.android.systemui.shared.recents.model.ThumbnailData;
219 import com.android.systemui.shared.system.ActivityManagerWrapper;
220 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
221 import com.android.systemui.shared.system.PackageManagerWrapper;
222 import com.android.systemui.shared.system.TaskStackChangeListener;
223 import com.android.systemui.shared.system.TaskStackChangeListeners;
224 import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
225 import com.android.wm.shell.common.pip.IPipAnimationListener;
226 import com.android.wm.shell.shared.DesktopModeStatus;
227 
228 import kotlin.Unit;
229 
230 import java.util.ArrayList;
231 import java.util.Arrays;
232 import java.util.HashMap;
233 import java.util.List;
234 import java.util.Map;
235 import java.util.Objects;
236 import java.util.Optional;
237 import java.util.function.Consumer;
238 import java.util.stream.Collectors;
239 
240 /**
241  * A list of recent tasks.
242  * @param <CONTAINER_TYPE> : the container that should host recents view
243  * @param <STATE_TYPE> : the type of base state that will be used
244  */
245 
246 public abstract class RecentsView<CONTAINER_TYPE extends Context & RecentsViewContainer,
247         STATE_TYPE extends BaseState<STATE_TYPE>> extends PagedView implements Insettable,
248         TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
249         TaskVisualsChangeListener {
250 
251     private static final String TAG = "RecentsView";
252     private static final boolean DEBUG = false;
253 
254     public static final FloatProperty<RecentsView> CONTENT_ALPHA =
255             new FloatProperty<RecentsView>("contentAlpha") {
256                 @Override
257                 public void setValue(RecentsView view, float v) {
258                     view.setContentAlpha(v);
259                 }
260 
261                 @Override
262                 public Float get(RecentsView view) {
263                     return view.getContentAlpha();
264                 }
265             };
266 
267     public static final FloatProperty<RecentsView> FULLSCREEN_PROGRESS =
268             new FloatProperty<RecentsView>("fullscreenProgress") {
269                 @Override
270                 public void setValue(RecentsView recentsView, float v) {
271                     recentsView.setFullscreenProgress(v);
272                 }
273 
274                 @Override
275                 public Float get(RecentsView recentsView) {
276                     return recentsView.mFullscreenProgress;
277                 }
278             };
279 
280     public static final FloatProperty<RecentsView> TASK_MODALNESS =
281             new FloatProperty<RecentsView>("taskModalness") {
282                 @Override
283                 public void setValue(RecentsView recentsView, float v) {
284                     recentsView.setTaskModalness(v);
285                 }
286 
287                 @Override
288                 public Float get(RecentsView recentsView) {
289                     return recentsView.mTaskModalness;
290                 }
291             };
292 
293     public static final FloatProperty<RecentsView> ADJACENT_PAGE_HORIZONTAL_OFFSET =
294             new FloatProperty<RecentsView>("adjacentPageHorizontalOffset") {
295                 @Override
296                 public void setValue(RecentsView recentsView, float v) {
297                     if (recentsView.mAdjacentPageHorizontalOffset != v) {
298                         recentsView.mAdjacentPageHorizontalOffset = v;
299                         recentsView.updatePageOffsets();
300                     }
301                 }
302 
303                 @Override
304                 public Float get(RecentsView recentsView) {
305                     return recentsView.mAdjacentPageHorizontalOffset;
306                 }
307             };
308 
309     public static final int SCROLL_VIBRATION_PRIMITIVE =
310             Utilities.ATLEAST_S ? VibrationEffect.Composition.PRIMITIVE_LOW_TICK : -1;
311     public static final float SCROLL_VIBRATION_PRIMITIVE_SCALE = 0.6f;
312     public static final VibrationEffect SCROLL_VIBRATION_FALLBACK =
313             VibrationConstants.EFFECT_TEXTURE_TICK;
314     public static final int UNBOUND_TASK_VIEW_ID = -1;
315 
316     /**
317      * Can be used to tint the color of the RecentsView to simulate a scrim that can views
318      * excluded from. Really should be a proper scrim.
319      * TODO(b/187528071): Remove this and replace with a real scrim.
320      */
321     private static final FloatProperty<RecentsView> COLOR_TINT =
322             new FloatProperty<RecentsView>("colorTint") {
323                 @Override
324                 public void setValue(RecentsView recentsView, float v) {
325                     recentsView.setColorTint(v);
326                 }
327 
328                 @Override
329                 public Float get(RecentsView recentsView) {
330                     return recentsView.getColorTint();
331                 }
332             };
333 
334     /**
335      * Even though {@link TaskView} has distinct offsetTranslationX/Y and resistance property, they
336      * are currently both used to apply secondary translation. Should their use cases change to be
337      * more specific, we'd want to create a similar FloatProperty just for a TaskView's
338      * offsetX/Y property
339      */
340     public static final FloatProperty<RecentsView> TASK_SECONDARY_TRANSLATION =
341             new FloatProperty<RecentsView>("taskSecondaryTranslation") {
342                 @Override
343                 public void setValue(RecentsView recentsView, float v) {
344                     recentsView.setTaskViewsResistanceTranslation(v);
345                 }
346 
347                 @Override
348                 public Float get(RecentsView recentsView) {
349                     return recentsView.mTaskViewsSecondaryTranslation;
350                 }
351             };
352 
353     /**
354      * Even though {@link TaskView} has distinct offsetTranslationX/Y and resistance property, they
355      * are currently both used to apply secondary translation. Should their use cases change to be
356      * more specific, we'd want to create a similar FloatProperty just for a TaskView's
357      * offsetX/Y property
358      */
359     public static final FloatProperty<RecentsView> TASK_PRIMARY_SPLIT_TRANSLATION =
360             new FloatProperty<RecentsView>("taskPrimarySplitTranslation") {
361                 @Override
362                 public void setValue(RecentsView recentsView, float v) {
363                     recentsView.setTaskViewsPrimarySplitTranslation(v);
364                 }
365 
366                 @Override
367                 public Float get(RecentsView recentsView) {
368                     return recentsView.mTaskViewsPrimarySplitTranslation;
369                 }
370             };
371 
372     public static final FloatProperty<RecentsView> TASK_SECONDARY_SPLIT_TRANSLATION =
373             new FloatProperty<RecentsView>("taskSecondarySplitTranslation") {
374                 @Override
375                 public void setValue(RecentsView recentsView, float v) {
376                     recentsView.setTaskViewsSecondarySplitTranslation(v);
377                 }
378 
379                 @Override
380                 public Float get(RecentsView recentsView) {
381                     return recentsView.mTaskViewsSecondarySplitTranslation;
382                 }
383             };
384 
385     /** Same as normal SCALE_PROPERTY, but also updates page offsets that depend on this scale. */
386     public static final FloatProperty<RecentsView> RECENTS_SCALE_PROPERTY =
387             new FloatProperty<RecentsView>("recentsScale") {
388                 @Override
389                 public void setValue(RecentsView view, float scale) {
390                     view.setScaleX(scale);
391                     view.setScaleY(scale);
392                     if (enableRefactorTaskThumbnail()) {
393                         view.mRecentsViewData.getScale().setValue(scale);
394                     }
395                     view.mLastComputedTaskStartPushOutDistance = null;
396                     view.mLastComputedTaskEndPushOutDistance = null;
397                     view.runActionOnRemoteHandles(new Consumer<RemoteTargetHandle>() {
398                         @Override
399                         public void accept(RemoteTargetHandle remoteTargetHandle) {
400                             remoteTargetHandle.getTaskViewSimulator().recentsViewScale.value =
401                                     scale;
402                         }
403                     });
404                     view.setTaskViewsResistanceTranslation(view.mTaskViewsSecondaryTranslation);
405                     view.updateTaskViewsSnapshotRadius();
406                     view.updatePageOffsets();
407                 }
408 
409                 @Override
410                 public Float get(RecentsView view) {
411                     return view.getScaleX();
412                 }
413             };
414 
415     /**
416      * Progress of Recents view from carousel layout to grid layout. If Recents is not shown as a
417      * grid, then the value remains 0.
418      */
419     public static final FloatProperty<RecentsView> RECENTS_GRID_PROGRESS =
420             new FloatProperty<RecentsView>("recentsGrid") {
421                 @Override
422                 public void setValue(RecentsView view, float gridProgress) {
423                     view.setGridProgress(gridProgress);
424                 }
425 
426                 @Override
427                 public Float get(RecentsView view) {
428                     return view.mGridProgress;
429                 }
430             };
431 
432     /**
433      * Alpha of the task thumbnail splash, where being in BackgroundAppState has a value of 1, and
434      * being in any other state has a value of 0.
435      */
436     public static final FloatProperty<RecentsView> TASK_THUMBNAIL_SPLASH_ALPHA =
437             new FloatProperty<RecentsView>("taskThumbnailSplashAlpha") {
438                 @Override
439                 public void setValue(RecentsView view, float taskThumbnailSplashAlpha) {
440                     view.setTaskThumbnailSplashAlpha(taskThumbnailSplashAlpha);
441                 }
442 
443                 @Override
444                 public Float get(RecentsView view) {
445                     return view.mTaskThumbnailSplashAlpha;
446                 }
447             };
448 
449     // OverScroll constants
450     private static final int OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION = 270;
451 
452     private static final int DEFAULT_ACTIONS_VIEW_ALPHA_ANIMATION_DURATION = 300;
453 
454     private static final int DISMISS_TASK_DURATION = 300;
455     private static final int ADDITION_TASK_DURATION = 200;
456     private static final float INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET = 0.55f;
457     private static final float ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET = 0.05f;
458     private static final float ANIMATION_DISMISS_PROGRESS_MIDPOINT = 0.5f;
459     private static final float END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET = 0.75f;
460 
461     private static final float SIGNIFICANT_MOVE_SCREEN_WIDTH_PERCENTAGE = 0.15f;
462 
463     private static final float FOREGROUND_SCRIM_TINT = 0.32f;
464 
465     public final RecentsViewData mRecentsViewData = new RecentsViewData();
466     public final TasksRepository mTasksRepository;
467 
468     protected final RecentsOrientedState mOrientationState;
469     protected final BaseContainerInterface<STATE_TYPE, CONTAINER_TYPE> mSizeStrategy;
470     @Nullable
471     protected RecentsAnimationController mRecentsAnimationController;
472     @Nullable
473     protected SurfaceTransactionApplier mSyncTransactionApplier;
474     protected int mTaskWidth;
475     protected int mTaskHeight;
476     // Used to position the top of a task in the top row of the grid
477     private float mTaskGridVerticalDiff;
478     // The vertical space one grid task takes + space between top and bottom row.
479     private float mTopBottomRowHeightDiff;
480     // mTaskGridVerticalDiff and mTopBottomRowHeightDiff summed together provides the top
481     // position for bottom row of grid tasks.
482 
483     @Nullable
484     protected RemoteTargetHandle[] mRemoteTargetHandles;
485     protected final Rect mLastComputedCarouselTaskSize = new Rect();
486     protected final Rect mLastComputedTaskSize = new Rect();
487     protected final Rect mLastComputedGridSize = new Rect();
488     protected final Rect mLastComputedGridTaskSize = new Rect();
489     private TaskView mSelectedTask = null;
490     // How much a task that is directly offscreen will be pushed out due to RecentsView scale/pivot.
491     @Nullable
492     protected Float mLastComputedTaskStartPushOutDistance = null;
493     @Nullable
494     protected Float mLastComputedTaskEndPushOutDistance = null;
495     protected boolean mEnableDrawingLiveTile = false;
496     protected final Rect mTempRect = new Rect();
497     protected final RectF mTempRectF = new RectF();
498     private final PointF mTempPointF = new PointF();
499     private final Matrix mTempMatrix = new Matrix();
500     private final float[] mTempFloat = new float[1];
501     private final List<OnScrollChangedListener> mScrollListeners = new ArrayList<>();
502 
503     // The threshold at which we update the SystemUI flags when animating from the task into the app
504     public static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.85f;
505 
506     protected final CONTAINER_TYPE mContainer;
507     private final float mFastFlingVelocity;
508     private final int mScrollHapticMinGapMillis;
509     private final RecentsModel mModel;
510     private final int mSplitPlaceholderSize;
511     private final int mSplitPlaceholderInset;
512     private final ClearAllButton mClearAllButton;
513     private final Rect mClearAllButtonDeadZoneRect = new Rect();
514     private final Rect mTaskViewDeadZoneRect = new Rect();
515     /**
516      * Reflects if Recents is currently in the middle of a gesture, and if so, which tasks are
517      * running. If a gesture is not in progress, this will be null.
518      */
519     private @Nullable Task[] mActiveGestureRunningTasks;
520 
521     // Keeps track of the previously known visible tasks for purposes of loading/unloading task data
522     private final SparseBooleanArray mHasVisibleTaskData = new SparseBooleanArray();
523 
524     private final InvariantDeviceProfile mIdp;
525 
526     /**
527      * Getting views should be done via {@link #getTaskViewFromPool(int)}
528      */
529     private final ViewPool<TaskView> mTaskViewPool;
530     private final ViewPool<GroupedTaskView> mGroupedTaskViewPool;
531     private final ViewPool<DesktopTaskView> mDesktopTaskViewPool;
532 
533     private final TaskOverlayFactory mTaskOverlayFactory;
534 
535     protected boolean mDisallowScrollToClearAll;
536     private boolean mOverlayEnabled;
537     protected boolean mFreezeViewVisibility;
538     private boolean mOverviewGridEnabled;
539     private boolean mOverviewFullscreenEnabled;
540     private boolean mOverviewSelectEnabled;
541 
542     private boolean mShouldClampScrollOffset;
543     private int mClampedScrollOffsetBound;
544 
545     private float mAdjacentPageHorizontalOffset = 0;
546     protected float mTaskViewsSecondaryTranslation = 0;
547     protected float mTaskViewsPrimarySplitTranslation = 0;
548     protected float mTaskViewsSecondarySplitTranslation = 0;
549     // Progress from 0 to 1 where 0 is a carousel and 1 is a 2 row grid.
550     private float mGridProgress = 0;
551     private float mTaskThumbnailSplashAlpha = 0;
552     private boolean mShowAsGridLastOnLayout = false;
553     private final IntSet mTopRowIdSet = new IntSet();
554     private int mClearAllShortTotalWidthTranslation = 0;
555 
556     // The GestureEndTarget that is still in progress.
557     @Nullable
558     protected GestureState.GestureEndTarget mCurrentGestureEndTarget;
559 
560     // TODO(b/187528071): Remove these and replace with a real scrim.
561     private float mColorTint;
562     private final int mTintingColor;
563     @Nullable
564     private ObjectAnimator mTintingAnimator;
565 
566     private int mOverScrollShift = 0;
567     private long mScrollLastHapticTimestamp;
568 
569     private int mKeyboardTaskFocusSnapAnimationDuration;
570     private int mKeyboardTaskFocusIndex = INVALID_PAGE;
571 
572     /**
573      * TODO: Call reloadIdNeeded in onTaskStackChanged.
574      */
575     private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
576         @Override
577         public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
578             if (!mHandleTaskStackChanges) {
579                 return;
580             }
581             // Check this is for the right user
582             if (!checkCurrentOrManagedUserId(userId, getContext())) {
583                 return;
584             }
585 
586             // Remove the task immediately from the task list
587             TaskView taskView = getTaskViewByTaskId(taskId);
588             if (taskView != null) {
589                 removeView(taskView);
590             }
591         }
592 
593         @Override
594         public void onActivityUnpinned() {
595             if (!mHandleTaskStackChanges) {
596                 return;
597             }
598 
599             reloadIfNeeded();
600             enableLayoutTransitions();
601         }
602 
603         @Override
604         public void onTaskRemoved(int taskId) {
605             if (!mHandleTaskStackChanges) {
606                 return;
607             }
608 
609             TaskView taskView = getTaskViewByTaskId(taskId);
610             if (taskView == null) {
611                 return;
612             }
613             Task.TaskKey taskKey = taskView.getFirstTask().key;
614             UI_HELPER_EXECUTOR.execute(new CancellableTask<>(
615                     () -> PackageManagerWrapper.getInstance()
616                             .getActivityInfo(taskKey.getComponent(), taskKey.userId) == null,
617                     MAIN_EXECUTOR,
618                     apkRemoved -> {
619                         if (apkRemoved) {
620                             dismissTask(taskId);
621                         } else {
622                             mModel.isTaskRemoved(taskKey.id, taskRemoved -> {
623                                 if (taskRemoved) {
624                                     dismissTask(taskId);
625                                 }
626                             }, RecentsFilterState.getFilter(mFilterState.getPackageNameToFilter()));
627                         }
628                     }));
629         }
630     };
631 
632     private final PinnedStackAnimationListener mIPipAnimationListener =
633             new PinnedStackAnimationListener();
634     private int mPipCornerRadius;
635     private int mPipShadowRadius;
636 
637     // Used to keep track of the last requested task list id, so that we do not request to load the
638     // tasks again if we have already requested it and the task list has not changed
639     private int mTaskListChangeId = -1;
640 
641     // Only valid until the launcher state changes to NORMAL
642     /**
643      * ID for the current running TaskView view, unique amongst TaskView instances. ID's are set
644      * through {@link #getTaskViewFromPool(boolean)} and incremented by {@link #mTaskViewIdCount}
645      */
646     protected int mRunningTaskViewId = -1;
647     private int mTaskViewIdCount;
648     protected boolean mRunningTaskTileHidden;
649     @Nullable
650     private Task[] mTmpRunningTasks;
651     protected int mFocusedTaskViewId = -1;
652 
653     private boolean mTaskIconScaledDown = false;
654     private boolean mRunningTaskShowScreenshot = false;
655 
656     private boolean mOverviewStateEnabled;
657     private boolean mHandleTaskStackChanges;
658     private boolean mSwipeDownShouldLaunchApp;
659     private boolean mTouchDownToStartHome;
660     private final float mSquaredTouchSlop;
661     private int mDownX;
662     private int mDownY;
663 
664     @Nullable
665     private PendingAnimation mPendingAnimation;
666     @Nullable
667     private LayoutTransition mLayoutTransition;
668 
669     @ViewDebug.ExportedProperty(category = "launcher")
670     protected float mContentAlpha = 1;
671     @ViewDebug.ExportedProperty(category = "launcher")
672     protected float mFullscreenProgress = 0;
673     /**
674      * How modal is the current task to be displayed, 1 means the task is fully modal and no other
675      * tasks are show. 0 means the task is displays in context in the list with other tasks.
676      */
677     @ViewDebug.ExportedProperty(category = "launcher")
678     protected float mTaskModalness = 0;
679 
680     // Keeps track of task id whose visual state should not be reset
681     private int mIgnoreResetTaskId = -1;
682     protected boolean mLoadPlanEverApplied;
683 
684     // Variables for empty state
685     private final Drawable mEmptyIcon;
686     private final CharSequence mEmptyMessage;
687     private final TextPaint mEmptyMessagePaint;
688     private final Point mLastMeasureSize = new Point();
689     private final int mEmptyMessagePadding;
690     private boolean mShowEmptyMessage;
691     @Nullable
692     private OnEmptyMessageUpdatedListener mOnEmptyMessageUpdatedListener;
693     @Nullable
694     private Layout mEmptyTextLayout;
695 
696     /**
697      * Placeholder view indicating where the first split screen selected app will be placed
698      */
699     protected SplitSelectStateController mSplitSelectStateController;
700 
701     /**
702      * The first task that split screen selection was initiated with. When split select state is
703      * initialized, we create a
704      * {@link #createTaskDismissAnimation(TaskView, boolean, boolean, long, boolean)} for this
705      * TaskView but don't actually remove the task since the user might back out. As such, we also
706      * ensure this View doesn't go back into the {@link #mTaskViewPool},
707      * see {@link #onViewRemoved(View)}
708      */
709     @Nullable
710     private TaskView mSplitHiddenTaskView;
711     @Nullable
712     private TaskView mSecondSplitHiddenView;
713     @Nullable
714     private SplitBounds mSplitBoundsConfig;
715     private final Toast mSplitUnsupportedToast = Toast.makeText(getContext(),
716             R.string.toast_split_app_unsupported, Toast.LENGTH_SHORT);
717 
718     @Nullable
719     private SplitSelectSource mSplitSelectSource;
720 
721     private final SplitSelectionListener mSplitSelectionListener = new SplitSelectionListener() {
722         @Override
723         public void onSplitSelectionConfirmed() { }
724 
725         @Override
726         public void onSplitSelectionActive() { }
727 
728         @Override
729         public void onSplitSelectionExit(boolean launchedSplit) {
730             resetFromSplitSelectionState();
731         }
732     };
733 
734     /**
735      * Keeps track of the index of the TaskView that split screen was initialized with so we know
736      * where to insert it back into list of taskViews in case user backs out of entering split
737      * screen.
738      * NOTE: This index is the index while {@link #mSplitHiddenTaskView} was a child of recentsView,
739      * this doesn't get adjusted to reflect the new child count after the taskView is dismissed/
740      * removed from recentsView
741      */
742     private int mSplitHiddenTaskViewIndex = -1;
743     @Nullable
744     private FloatingTaskView mSecondFloatingTaskView;
745     /**
746      * A fullscreen scrim that goes behind the splitscreen animation to hide color conflicts and
747      * possible flickers. Removed after tasks + divider finish animating in.
748      */
749     private View mSplitScrim;
750 
751     /**
752      * The task to be removed and immediately re-added. Should not be added to task pool.
753      */
754     @Nullable
755     private TaskView mMovingTaskView;
756 
757     private OverviewActionsView mActionsView;
758     private ObjectAnimator mActionsViewAlphaAnimator;
759     private float mActionsViewAlphaAnimatorFinalValue;
760 
761     @Nullable
762     private DesktopRecentsTransitionController mDesktopRecentsTransitionController;
763 
764     /**
765      * Keeps track of the desktop task. Optional and only present when the feature flag is enabled.
766      */
767     @Nullable
768     private DesktopTaskView mDesktopTaskView;
769 
770     private MultiWindowModeChangedListener mMultiWindowModeChangedListener =
771             new MultiWindowModeChangedListener() {
772                 @Override
773                 public void onMultiWindowModeChanged(boolean inMultiWindowMode) {
774                     mOrientationState.setMultiWindowMode(inMultiWindowMode);
775                     setLayoutRotation(mOrientationState.getTouchRotation(),
776                             mOrientationState.getDisplayRotation());
777                     updateChildTaskOrientations();
778                     if (!inMultiWindowMode && mOverviewStateEnabled) {
779                         // TODO: Re-enable layout transitions for addition of the unpinned task
780                         reloadIfNeeded();
781                     }
782                 }
783             };
784 
785     @Nullable
786     private RunnableList mSideTaskLaunchCallback;
787     @Nullable
788     private TaskLaunchListener mTaskLaunchListener;
789     @Nullable
790     private Runnable mOnTaskLaunchCancelledRunnable;
791 
792 
793     // keeps track of the state of the filter for tasks in recents view
794     private final RecentsFilterState mFilterState = new RecentsFilterState();
795 
796     private int mOffsetMidpointIndexOverride = INVALID_PAGE;
797 
RecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, BaseContainerInterface sizeStrategy)798     public RecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
799             BaseContainerInterface sizeStrategy) {
800         super(context, attrs, defStyleAttr);
801         setEnableFreeScroll(true);
802         mSizeStrategy = sizeStrategy;
803         mContainer = RecentsViewContainer.containerFromContext(context);
804         mOrientationState = new RecentsOrientedState(
805                 context, mSizeStrategy, this::animateRecentsRotationInPlace);
806         final int rotation = mContainer.getDisplay().getRotation();
807         mOrientationState.setRecentsRotation(rotation);
808 
809         mScrollHapticMinGapMillis = getResources()
810                 .getInteger(R.integer.recentsScrollHapticMinGapMillis);
811         mFastFlingVelocity = getResources()
812                 .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
813         mModel = RecentsModel.INSTANCE.get(context);
814         mIdp = InvariantDeviceProfile.INSTANCE.get(context);
815         if (enableRefactorTaskThumbnail()) {
816             mTasksRepository = new TasksRepository(
817                     mModel, mModel.getThumbnailCache(), mModel.getIconCache());
818         } else {
819             mTasksRepository = null;
820         }
821 
822         mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
823                 .inflate(R.layout.overview_clear_all_button, this, false);
824         mClearAllButton.setOnClickListener(this::dismissAllTasks);
825         mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */,
826                 10 /* initial size */);
827         mGroupedTaskViewPool = new ViewPool<>(context, this,
828                 R.layout.task_grouped, 20 /* max size */, 10 /* initial size */);
829         mDesktopTaskViewPool = new ViewPool<>(context, this, R.layout.task_desktop,
830                 5 /* max size */, 1 /* initial size */);
831 
832         setOrientationHandler(mOrientationState.getOrientationHandler());
833         mIsRtl = getPagedOrientationHandler().getRecentsRtlSetting(getResources());
834         setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
835         mSplitPlaceholderSize = getResources().getDimensionPixelSize(
836                 R.dimen.split_placeholder_size);
837         mSplitPlaceholderInset = getResources().getDimensionPixelSize(
838                 R.dimen.split_placeholder_inset);
839         mSquaredTouchSlop = squaredTouchSlop(context);
840         mClampedScrollOffsetBound = getResources().getDimensionPixelSize(
841                 R.dimen.transient_taskbar_clamped_offset_bound);
842 
843         mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents);
844         mEmptyIcon.setCallback(this);
845         mEmptyMessage = context.getText(R.string.recents_empty_message);
846         mEmptyMessagePaint = new TextPaint();
847         mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary));
848         mEmptyMessagePaint.setTextSize(getResources()
849                 .getDimension(R.dimen.recents_empty_message_text_size));
850         mEmptyMessagePaint.setTypeface(Typeface.create(Themes.getDefaultBodyFont(context),
851                 Typeface.NORMAL));
852         mEmptyMessagePaint.setAntiAlias(true);
853         mEmptyMessagePadding = getResources()
854                 .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
855         setWillNotDraw(false);
856         updateEmptyMessage();
857 
858         mTaskOverlayFactory = Overrides.getObject(
859                 TaskOverlayFactory.class,
860                 context.getApplicationContext(),
861                 R.string.task_overlay_factory_class);
862 
863         // Initialize quickstep specific cache params here, as this is constructed only once
864         mContainer.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5);
865 
866         mTintingColor = getForegroundScrimDimColor(context);
867 
868         // if multi-instance feature is enabled
869         if (FeatureFlags.ENABLE_MULTI_INSTANCE.get()) {
870             // invalidate the current list of tasks if filter changes with a fading in/out animation
871             mFilterState.setOnFilterUpdatedListener(() -> {
872                 Animator animatorFade = getStateManager().createStateElementAnimation(
873                         RecentsAtomicAnimationFactory.INDEX_RECENTS_FADE_ANIM, 1f, 0f);
874                 Animator animatorAppear = getStateManager().createStateElementAnimation(
875                         RecentsAtomicAnimationFactory.INDEX_RECENTS_FADE_ANIM, 0f, 1f);
876                 animatorFade.addListener(new AnimatorListenerAdapter() {
877                     @Override
878                     public void onAnimationEnd(@NonNull Animator animation) {
879                         RecentsView.this.invalidateTaskList();
880                         updateClearAllFunction();
881                         reloadIfNeeded();
882                         if (mPendingAnimation != null) {
883                             mPendingAnimation.addEndListener(success -> {
884                                 animatorAppear.start();
885                             });
886                         } else {
887                             animatorAppear.start();
888                         }
889                     }
890                 });
891                 animatorFade.start();
892             });
893         }
894         // make sure filter is turned off by default
895         mFilterState.setFilterBy(null);
896     }
897 
898     /** Get the state of the filter */
getFilterState()899     public RecentsFilterState getFilterState() {
900         return mFilterState;
901     }
902 
903     /**
904      * Toggles the filter and reloads the recents view if needed.
905      *
906      * @param packageName package name to filter by if the filter is being turned on;
907      *                    should be null if filter is being turned off
908      */
setAndApplyFilter(@ullable String packageName)909     public void setAndApplyFilter(@Nullable String packageName) {
910         mFilterState.setFilterBy(packageName);
911     }
912 
913     /**
914      * Updates the "Clear All" button and its function depending on the recents view state.
915      *
916      * TODO: add a different button for going back to overview. Present solution is for demo only.
917      */
updateClearAllFunction()918     public void updateClearAllFunction() {
919         if (mFilterState.isFiltered()) {
920             mClearAllButton.setText(R.string.recents_back);
921             mClearAllButton.setOnClickListener((view) -> {
922                 this.setAndApplyFilter(null);
923             });
924         } else {
925             mClearAllButton.setText(R.string.recents_clear_all);
926             mClearAllButton.setOnClickListener(this::dismissAllTasks);
927         }
928     }
929 
930     /**
931      * Invalidates the list of tasks so that an update occurs to the list of tasks if requested.
932      */
invalidateTaskList()933     private void invalidateTaskList() {
934         mTaskListChangeId = -1;
935     }
936 
getScroller()937     public OverScroller getScroller() {
938         return mScroller;
939     }
940 
isRtl()941     public boolean isRtl() {
942         return mIsRtl;
943     }
944 
945     @Override
initEdgeEffect()946     protected void initEdgeEffect() {
947         mEdgeGlowLeft = new TranslateEdgeEffect(getContext());
948         mEdgeGlowRight = new TranslateEdgeEffect(getContext());
949     }
950 
951     @Override
drawEdgeEffect(Canvas canvas)952     protected void drawEdgeEffect(Canvas canvas) {
953         // Do not draw edge effect
954     }
955 
956     @Override
dispatchDraw(Canvas canvas)957     protected void dispatchDraw(Canvas canvas) {
958         // Draw overscroll
959         if (mAllowOverScroll && (!mEdgeGlowRight.isFinished() || !mEdgeGlowLeft.isFinished())) {
960             final int restoreCount = canvas.save();
961 
962             int primarySize = getPagedOrientationHandler().getPrimaryValue(getWidth(), getHeight());
963             int scroll = OverScroll.dampedScroll(getUndampedOverScrollShift(), primarySize);
964             getPagedOrientationHandler().setPrimary(canvas, CANVAS_TRANSLATE, scroll);
965 
966             if (mOverScrollShift != scroll) {
967                 mOverScrollShift = scroll;
968                 dispatchScrollChanged();
969             }
970 
971             super.dispatchDraw(canvas);
972             canvas.restoreToCount(restoreCount);
973         } else {
974             if (mOverScrollShift != 0) {
975                 mOverScrollShift = 0;
976                 dispatchScrollChanged();
977             }
978             super.dispatchDraw(canvas);
979         }
980         if (mEnableDrawingLiveTile && mRemoteTargetHandles != null) {
981             redrawLiveTile();
982         }
983     }
984 
getUndampedOverScrollShift()985     private float getUndampedOverScrollShift() {
986         final int width = getWidth();
987         final int height = getHeight();
988         int primarySize = getPagedOrientationHandler().getPrimaryValue(width, height);
989         int secondarySize = getPagedOrientationHandler().getSecondaryValue(width, height);
990 
991         float effectiveShift = 0;
992         if (!mEdgeGlowLeft.isFinished()) {
993             mEdgeGlowLeft.setSize(secondarySize, primarySize);
994             if (((TranslateEdgeEffect) mEdgeGlowLeft).getTranslationShift(mTempFloat)) {
995                 effectiveShift = mTempFloat[0];
996                 postInvalidateOnAnimation();
997             }
998         }
999         if (!mEdgeGlowRight.isFinished()) {
1000             mEdgeGlowRight.setSize(secondarySize, primarySize);
1001             if (((TranslateEdgeEffect) mEdgeGlowRight).getTranslationShift(mTempFloat)) {
1002                 effectiveShift -= mTempFloat[0];
1003                 postInvalidateOnAnimation();
1004             }
1005         }
1006 
1007         return effectiveShift * primarySize;
1008     }
1009 
1010     /**
1011      * Returns the view shift due to overscroll
1012      */
getOverScrollShift()1013     public int getOverScrollShift() {
1014         return mOverScrollShift;
1015     }
1016 
1017     @Override
1018     @Nullable
onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData)1019     public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
1020         if (mHandleTaskStackChanges) {
1021             TaskView taskView = getTaskViewByTaskId(taskId);
1022             if (taskView != null) {
1023                 for (TaskContainer container : taskView.getTaskContainers()) {
1024                     if (container == null || taskId != container.getTask().key.id) {
1025                         continue;
1026                     }
1027                     container.getThumbnailViewDeprecated().setThumbnail(container.getTask(),
1028                             thumbnailData);
1029                 }
1030             }
1031         }
1032         return null;
1033     }
1034 
1035     @Override
onTaskIconChanged(String pkg, UserHandle user)1036     public void onTaskIconChanged(String pkg, UserHandle user) {
1037         for (int i = 0; i < getTaskViewCount(); i++) {
1038             TaskView tv = requireTaskViewAt(i);
1039             Task task = tv.getFirstTask();
1040             if (pkg.equals(task.key.getPackageName()) && task.key.userId == user.getIdentifier()) {
1041                 task.icon = null;
1042                 if (tv.getTaskContainers().stream().anyMatch(
1043                         container -> container.getIconView().getDrawable() != null)) {
1044                     tv.onTaskListVisibilityChanged(true /* visible */);
1045                 }
1046             }
1047         }
1048     }
1049 
1050     @Override
onTaskIconChanged(int taskId)1051     public void onTaskIconChanged(int taskId) {
1052         TaskView taskView = getTaskViewByTaskId(taskId);
1053         if (taskView != null) {
1054             taskView.refreshTaskThumbnailSplash();
1055         }
1056     }
1057 
1058     /**
1059      * Update the thumbnail(s) of the relevant TaskView.
1060      * @param refreshNow Refresh immediately if it's true.
1061      */
1062     @Nullable
updateThumbnail( HashMap<Integer, ThumbnailData> thumbnailData, boolean refreshNow)1063     public TaskView updateThumbnail(
1064             HashMap<Integer, ThumbnailData> thumbnailData, boolean refreshNow) {
1065         TaskView updatedTaskView = null;
1066         for (Map.Entry<Integer, ThumbnailData> entry : thumbnailData.entrySet()) {
1067             Integer id = entry.getKey();
1068             ThumbnailData thumbnail = entry.getValue();
1069             TaskView taskView = getTaskViewByTaskId(id);
1070             if (taskView == null) {
1071                 continue;
1072             }
1073             // taskView could be a GroupedTaskView, so select the relevant task by ID
1074             TaskContainer taskAttributes = taskView.getTaskContainerById(id);
1075             if (taskAttributes == null) {
1076                 continue;
1077             }
1078             Task task = taskAttributes.getTask();
1079             TaskThumbnailViewDeprecated taskThumbnailViewDeprecated =
1080                     taskAttributes.getThumbnailViewDeprecated();
1081             taskThumbnailViewDeprecated.setThumbnail(task, thumbnail, refreshNow);
1082             // thumbnailData can contain 1-2 ids, but they should correspond to the same
1083             // TaskView, so overwriting is ok
1084             updatedTaskView = taskView;
1085         }
1086 
1087         return updatedTaskView;
1088     }
1089 
1090     @Override
onWindowVisibilityChanged(int visibility)1091     protected void onWindowVisibilityChanged(int visibility) {
1092         super.onWindowVisibilityChanged(visibility);
1093         updateTaskStackListenerState();
1094     }
1095 
init(OverviewActionsView actionsView, SplitSelectStateController splitController, @Nullable DesktopRecentsTransitionController desktopRecentsTransitionController)1096     public void init(OverviewActionsView actionsView, SplitSelectStateController splitController,
1097             @Nullable DesktopRecentsTransitionController desktopRecentsTransitionController) {
1098         mActionsView = actionsView;
1099         mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
1100         // Update flags for 1p/3p launchers
1101         mActionsView.updateFor3pLauncher(!supportsAppPairs());
1102         mSplitSelectStateController = splitController;
1103         mDesktopRecentsTransitionController = desktopRecentsTransitionController;
1104     }
1105 
getSplitSelectController()1106     public SplitSelectStateController getSplitSelectController() {
1107         return mSplitSelectStateController;
1108     }
1109 
isSplitSelectionActive()1110     public boolean isSplitSelectionActive() {
1111         return mSplitSelectStateController.isSplitSelectActive();
1112     }
1113 
1114     /**
1115      * See overridden implementations
1116      * @return {@code true} if child TaskViews can be launched when user taps on them
1117      */
canLaunchFullscreenTask()1118     protected boolean canLaunchFullscreenTask() {
1119         return true;
1120     }
1121 
1122     @Override
onAttachedToWindow()1123     protected void onAttachedToWindow() {
1124         super.onAttachedToWindow();
1125         updateTaskStackListenerState();
1126         mModel.getThumbnailCache().getHighResLoadingState().addCallback(this);
1127         mContainer.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
1128         TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
1129         mSyncTransactionApplier = new SurfaceTransactionApplier(this);
1130         runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams()
1131                 .setSyncTransactionApplier(mSyncTransactionApplier));
1132         RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
1133         mIPipAnimationListener.setActivityAndRecentsView(mContainer, this);
1134         SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener(
1135                 mIPipAnimationListener);
1136         mOrientationState.initListeners();
1137         mTaskOverlayFactory.initListeners();
1138         if (FeatureFlags.enableSplitContextually()) {
1139             mSplitSelectStateController.registerSplitListener(mSplitSelectionListener);
1140         }
1141     }
1142 
1143     @Override
onDetachedFromWindow()1144     protected void onDetachedFromWindow() {
1145         super.onDetachedFromWindow();
1146         updateTaskStackListenerState();
1147         mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this);
1148         mContainer.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
1149         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
1150         mSyncTransactionApplier = null;
1151         runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams()
1152                 .setSyncTransactionApplier(null));
1153         executeSideTaskLaunchCallback();
1154         RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
1155         SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener(null);
1156         mIPipAnimationListener.setActivityAndRecentsView(null, null);
1157         mOrientationState.destroyListeners();
1158         mTaskOverlayFactory.removeListeners();
1159         if (FeatureFlags.enableSplitContextually()) {
1160             mSplitSelectStateController.unregisterSplitListener(mSplitSelectionListener);
1161         }
1162         reset();
1163     }
1164 
1165     @Override
onViewRemoved(View child)1166     public void onViewRemoved(View child) {
1167         super.onViewRemoved(child);
1168 
1169         // Clear the task data for the removed child if it was visible unless:
1170         // - It's the initial taskview for entering split screen, we only pretend to dismiss the
1171         // task
1172         // - It's the focused task to be moved to the front, we immediately re-add the task
1173         if (child instanceof TaskView && child != mSplitHiddenTaskView
1174                 && child != mMovingTaskView) {
1175             TaskView taskView = (TaskView) child;
1176             for (int i : taskView.getTaskIds()) {
1177                 mHasVisibleTaskData.delete(i);
1178             }
1179             if (child instanceof GroupedTaskView) {
1180                 mGroupedTaskViewPool.recycle((GroupedTaskView) taskView);
1181             } else if (child instanceof DesktopTaskView) {
1182                 mDesktopTaskViewPool.recycle((DesktopTaskView) taskView);
1183             } else {
1184                 mTaskViewPool.recycle(taskView);
1185             }
1186             mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
1187         }
1188     }
1189 
1190     @Override
onViewAdded(View child)1191     public void onViewAdded(View child) {
1192         super.onViewAdded(child);
1193         child.setAlpha(mContentAlpha);
1194         // RecentsView is set to RTL in the constructor when system is using LTR. Here we set the
1195         // child direction back to match system settings.
1196         child.setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_LTR : View.LAYOUT_DIRECTION_RTL);
1197         mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, false);
1198         updateEmptyMessage();
1199     }
1200 
1201     @Override
draw(Canvas canvas)1202     public void draw(Canvas canvas) {
1203         maybeDrawEmptyMessage(canvas);
1204         super.draw(canvas);
1205     }
1206 
1207     @Override
requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate)1208     public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
1209         if (isModal()) {
1210             // Do not scroll when clicking on a modal grid task, as it will already be centered
1211             // on screen.
1212             return false;
1213         }
1214         return super.requestChildRectangleOnScreen(child, rectangle, immediate);
1215     }
1216 
addSideTaskLaunchCallback(RunnableList callback)1217     public void addSideTaskLaunchCallback(RunnableList callback) {
1218         if (mSideTaskLaunchCallback == null) {
1219             mSideTaskLaunchCallback = new RunnableList();
1220         }
1221         mSideTaskLaunchCallback.add(callback::executeAllAndDestroy);
1222     }
1223 
1224     /**
1225      * This is a one-time callback when touching in live tile mode. It's reset to null right
1226      * after it's called.
1227      */
setTaskLaunchListener(TaskLaunchListener taskLaunchListener)1228     public void setTaskLaunchListener(TaskLaunchListener taskLaunchListener) {
1229         mTaskLaunchListener = taskLaunchListener;
1230     }
1231 
onTaskLaunchedInLiveTileMode()1232     public void onTaskLaunchedInLiveTileMode() {
1233         if (mTaskLaunchListener != null) {
1234             mTaskLaunchListener.onTaskLaunched();
1235             mTaskLaunchListener = null;
1236         }
1237     }
1238 
1239     /**
1240      * This is a one-time callback when touching in live tile mode. It's reset to null right
1241      * after it's called.
1242      */
setTaskLaunchCancelledRunnable(Runnable onTaskLaunchCancelledRunnable)1243     public void setTaskLaunchCancelledRunnable(Runnable onTaskLaunchCancelledRunnable) {
1244         mOnTaskLaunchCancelledRunnable = onTaskLaunchCancelledRunnable;
1245     }
1246 
onTaskLaunchedInLiveTileModeCancelled()1247     public void onTaskLaunchedInLiveTileModeCancelled() {
1248         if (mOnTaskLaunchCancelledRunnable != null) {
1249             mOnTaskLaunchCancelledRunnable.run();
1250             mOnTaskLaunchCancelledRunnable = null;
1251         }
1252     }
1253 
executeSideTaskLaunchCallback()1254     private void executeSideTaskLaunchCallback() {
1255         if (mSideTaskLaunchCallback != null) {
1256             mSideTaskLaunchCallback.executeAllAndDestroy();
1257             mSideTaskLaunchCallback = null;
1258         }
1259     }
1260 
1261     /**
1262      * TODO(b/195675206) Check both taskIDs from runningTaskViewId
1263      *  and launch if either of them is {@param taskId}
1264      */
launchSideTaskInLiveTileModeForRestartedApp(int taskId)1265     public void launchSideTaskInLiveTileModeForRestartedApp(int taskId) {
1266         int runningTaskViewId = getTaskViewIdFromTaskId(taskId);
1267         if (mRunningTaskViewId == -1 ||
1268                 mRunningTaskViewId != runningTaskViewId ||
1269                 mRemoteTargetHandles == null) {
1270             return;
1271         }
1272 
1273         TransformParams params = mRemoteTargetHandles[0].getTransformParams();
1274         RemoteAnimationTargets targets = params.getTargetSet();
1275         if (targets != null && targets.findTask(taskId) != null) {
1276             launchSideTaskInLiveTileMode(taskId, targets.apps, targets.wallpapers,
1277                     targets.nonApps);
1278         }
1279     }
1280 
launchSideTaskInLiveTileMode(int taskId, RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpaper, RemoteAnimationTarget[] nonApps)1281     public void launchSideTaskInLiveTileMode(int taskId, RemoteAnimationTarget[] apps,
1282             RemoteAnimationTarget[] wallpaper, RemoteAnimationTarget[] nonApps) {
1283         AnimatorSet anim = new AnimatorSet();
1284         TaskView taskView = getTaskViewByTaskId(taskId);
1285         if (taskView == null || !isTaskViewVisible(taskView)) {
1286             // TODO: Refine this animation.
1287             SurfaceTransactionApplier surfaceApplier =
1288                     new SurfaceTransactionApplier(mContainer.getDragLayer());
1289             ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
1290             appAnimator.setDuration(RECENTS_LAUNCH_DURATION);
1291             appAnimator.setInterpolator(ACCELERATE_DECELERATE);
1292             final Matrix matrix = new Matrix();
1293             appAnimator.addUpdateListener(valueAnimator -> {
1294                 float percent = valueAnimator.getAnimatedFraction();
1295                 SurfaceTransaction transaction = new SurfaceTransaction();
1296                 for (int i = apps.length - 1; i >= 0; --i) {
1297                     RemoteAnimationTarget app = apps[i];
1298 
1299                     float dx = mContainer.getDeviceProfile().widthPx * (1 - percent) / 2
1300                             + app.screenSpaceBounds.left * percent;
1301                     float dy = mContainer.getDeviceProfile().heightPx * (1 - percent) / 2
1302                             + app.screenSpaceBounds.top * percent;
1303                     matrix.setScale(percent, percent);
1304                     matrix.postTranslate(dx, dy);
1305                     transaction.forSurface(app.leash)
1306                             .setAlpha(percent)
1307                             .setMatrix(matrix);
1308                 }
1309                 surfaceApplier.scheduleApply(transaction);
1310             });
1311             appAnimator.addListener(new AnimatorListenerAdapter() {
1312                 @Override
1313                 public void onAnimationStart(Animator animation) {
1314                     super.onAnimationStart(animation);
1315                     final SurfaceTransaction showTransaction = new SurfaceTransaction();
1316                     for (int i = apps.length - 1; i >= 0; --i) {
1317                         showTransaction.getTransaction().show(apps[i].leash);
1318                         showTransaction.forSurface(apps[i].leash).setLayer(
1319                                 Integer.MAX_VALUE - 1000 + apps[i].prefixOrderIndex);
1320                     }
1321                     surfaceApplier.scheduleApply(showTransaction);
1322                 }
1323             });
1324             anim.play(appAnimator);
1325             anim.addListener(new AnimatorListenerAdapter() {
1326                 @Override
1327                 public void onAnimationEnd(Animator animation) {
1328                     finishRecentsAnimation(false /* toRecents */, null);
1329                 }
1330             });
1331         } else {
1332             TaskViewUtils.composeRecentsLaunchAnimator(anim, taskView, apps, wallpaper, nonApps,
1333                     true /* launcherClosing */, getStateManager(), this,
1334                     getDepthController());
1335         }
1336         anim.start();
1337     }
1338 
isTaskViewVisible(TaskView tv)1339     public boolean isTaskViewVisible(TaskView tv) {
1340         if (showAsGrid()) {
1341             int screenStart = getPagedOrientationHandler().getPrimaryScroll(this);
1342             int screenEnd = screenStart + getPagedOrientationHandler().getMeasuredSize(this);
1343             return isTaskViewWithinBounds(tv, screenStart, screenEnd);
1344         } else {
1345             // For now, just check if it's the active task or an adjacent task
1346             return Math.abs(indexOfChild(tv) - getNextPage()) <= 1;
1347         }
1348     }
1349 
isTaskViewFullyVisible(TaskView tv)1350     public boolean isTaskViewFullyVisible(TaskView tv) {
1351         if (showAsGrid()) {
1352             int screenStart = getPagedOrientationHandler().getPrimaryScroll(this);
1353             int screenEnd = screenStart + getPagedOrientationHandler().getMeasuredSize(this);
1354             return isTaskViewFullyWithinBounds(tv, screenStart, screenEnd);
1355         } else {
1356             // For now, just check if it's the active task
1357             return indexOfChild(tv) == getNextPage();
1358         }
1359     }
1360 
1361     @Nullable
getLastGridTaskView()1362     private TaskView getLastGridTaskView() {
1363         return getLastGridTaskView(getTopRowIdArray(), getBottomRowIdArray());
1364     }
1365 
1366     @Nullable
getLastGridTaskView(IntArray topRowIdArray, IntArray bottomRowIdArray)1367     private TaskView getLastGridTaskView(IntArray topRowIdArray, IntArray bottomRowIdArray) {
1368         if (topRowIdArray.isEmpty() && bottomRowIdArray.isEmpty()) {
1369             return null;
1370         }
1371         int lastTaskViewId = topRowIdArray.size() >= bottomRowIdArray.size() ? topRowIdArray.get(
1372                 topRowIdArray.size() - 1) : bottomRowIdArray.get(bottomRowIdArray.size() - 1);
1373         return getTaskViewFromTaskViewId(lastTaskViewId);
1374     }
1375 
getSnapToLastTaskScrollDiff()1376     private int getSnapToLastTaskScrollDiff() {
1377         // Snap to a position where ClearAll is just invisible.
1378         int screenStart = getPagedOrientationHandler().getPrimaryScroll(this);
1379         int clearAllScroll = getScrollForPage(indexOfChild(mClearAllButton));
1380         int clearAllWidth = getPagedOrientationHandler().getPrimarySize(mClearAllButton);
1381         int lastTaskScroll = getLastTaskScroll(clearAllScroll, clearAllWidth);
1382         return screenStart - lastTaskScroll;
1383     }
1384 
getLastTaskScroll(int clearAllScroll, int clearAllWidth)1385     private int getLastTaskScroll(int clearAllScroll, int clearAllWidth) {
1386         int distance = clearAllWidth + getClearAllExtraPageSpacing();
1387         return clearAllScroll + (mIsRtl ? distance : -distance);
1388     }
1389 
isTaskViewWithinBounds(TaskView tv, int start, int end)1390     private boolean isTaskViewWithinBounds(TaskView tv, int start, int end) {
1391         int taskStart = getPagedOrientationHandler().getChildStart(tv)
1392                 + (int) tv.getOffsetAdjustment(showAsGrid());
1393         int taskSize = (int) (getPagedOrientationHandler().getMeasuredSize(tv)
1394                 * tv.getSizeAdjustment(showAsFullscreen()));
1395         int taskEnd = taskStart + taskSize;
1396         return (taskStart >= start && taskStart <= end) || (taskEnd >= start
1397                 && taskEnd <= end);
1398     }
1399 
isTaskViewFullyWithinBounds(TaskView tv, int start, int end)1400     private boolean isTaskViewFullyWithinBounds(TaskView tv, int start, int end) {
1401         int taskStart = getPagedOrientationHandler().getChildStart(tv)
1402                 + (int) tv.getOffsetAdjustment(showAsGrid());
1403         int taskSize = (int) (getPagedOrientationHandler().getMeasuredSize(tv)
1404                 * tv.getSizeAdjustment(showAsFullscreen()));
1405         int taskEnd = taskStart + taskSize;
1406         return taskStart >= start && taskEnd <= end;
1407     }
1408 
1409     /**
1410      * Returns true if the task is in expected scroll position.
1411      *
1412      * @param taskIndex the index of the task
1413      */
isTaskInExpectedScrollPosition(int taskIndex)1414     public boolean isTaskInExpectedScrollPosition(int taskIndex) {
1415         return getScrollForPage(taskIndex) == getPagedOrientationHandler().getPrimaryScroll(this);
1416     }
1417 
isFocusedTaskInExpectedScrollPosition()1418     private boolean isFocusedTaskInExpectedScrollPosition() {
1419         TaskView focusedTask = getFocusedTaskView();
1420         return focusedTask != null && isTaskInExpectedScrollPosition(indexOfChild(focusedTask));
1421     }
1422 
1423     /**
1424      * Returns a {@link TaskView} that has taskId matching {@code taskId} or null if no match.
1425      */
1426     @Nullable
getTaskViewByTaskId(int taskId)1427     public TaskView getTaskViewByTaskId(int taskId) {
1428         if (taskId == INVALID_TASK_ID) {
1429             return null;
1430         }
1431 
1432         for (int i = 0; i < getTaskViewCount(); i++) {
1433             TaskView taskView = requireTaskViewAt(i);
1434             if (taskView.containsTaskId(taskId)) {
1435                 return taskView;
1436             }
1437         }
1438         return null;
1439     }
1440 
1441     /**
1442      * Returns a {@link TaskView} that has taskIds matching {@code taskIds} or null if no match.
1443      */
1444     @Nullable
getTaskViewByTaskIds(int[] taskIds)1445     public TaskView getTaskViewByTaskIds(int[] taskIds) {
1446         if (!hasAllValidTaskIds(taskIds)) {
1447             return null;
1448         }
1449 
1450         // We're looking for a taskView that matches these ids, regardless of order
1451         int[] taskIdsCopy = Arrays.copyOf(taskIds, taskIds.length);
1452         Arrays.sort(taskIdsCopy);
1453 
1454         for (int i = 0; i < getTaskViewCount(); i++) {
1455             TaskView taskView = requireTaskViewAt(i);
1456             int[] taskViewIdsCopy = taskView.getTaskIds();
1457             Arrays.sort(taskViewIdsCopy);
1458             if (Arrays.equals(taskIdsCopy, taskViewIdsCopy)) {
1459                 return taskView;
1460             }
1461         }
1462         return null;
1463     }
1464 
1465     /** Returns false if {@code taskIds} is null or contains any invalid values, true otherwise */
hasAllValidTaskIds(int[] taskIds)1466     private boolean hasAllValidTaskIds(int[] taskIds) {
1467         return taskIds != null
1468                 && taskIds.length > 0
1469                 && Arrays.stream(taskIds).noneMatch(taskId -> taskId == INVALID_TASK_ID);
1470     }
1471 
setOverviewStateEnabled(boolean enabled)1472     public void setOverviewStateEnabled(boolean enabled) {
1473         mOverviewStateEnabled = enabled;
1474         updateTaskStackListenerState();
1475         mOrientationState.setRotationWatcherEnabled(enabled);
1476         if (!enabled) {
1477             // Reset the running task when leaving overview since it can still have a reference to
1478             // its thumbnail
1479             mTmpRunningTasks = null;
1480             mSplitBoundsConfig = null;
1481             mTaskOverlayFactory.clearAllActiveState();
1482         }
1483         updateLocusId();
1484     }
1485 
1486     /**
1487      * Enable or disable showing border on hover and focus change on task views
1488      */
setTaskBorderEnabled(boolean enabled)1489     public void setTaskBorderEnabled(boolean enabled) {
1490         int taskCount = getTaskViewCount();
1491         for (int i = 0; i < taskCount; i++) {
1492             TaskView taskView = requireTaskViewAt(i);
1493             taskView.setBorderEnabled(enabled);
1494         }
1495         mClearAllButton.setBorderEnabled(enabled);
1496     }
1497 
1498     /**
1499      * Whether the Clear All button is hidden or fully visible. Used to determine if center
1500      * displayed page is a task or the Clear All button.
1501      *
1502      * @return True = Clear All button not fully visible, center page is a task. False = Clear All
1503      * button fully visible, center page is Clear All button.
1504      */
isClearAllHidden()1505     public boolean isClearAllHidden() {
1506         return mClearAllButton.getAlpha() != 1f;
1507     }
1508 
1509     @Override
onPageBeginTransition()1510     protected void onPageBeginTransition() {
1511         super.onPageBeginTransition();
1512         if (!mContainer.getDeviceProfile().isTablet) {
1513             mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, true);
1514         }
1515         if (mOverviewStateEnabled) { // only when in overview
1516             InteractionJankMonitorWrapper.begin(/* view= */ this, Cuj.CUJ_RECENTS_SCROLLING);
1517         }
1518     }
1519 
1520     @Override
onPageEndTransition()1521     protected void onPageEndTransition() {
1522         super.onPageEndTransition();
1523         ActiveGestureLog.INSTANCE.addLog(
1524                 "onPageEndTransition: current page index updated", getNextPage());
1525         if (isClearAllHidden() && !mContainer.getDeviceProfile().isTablet) {
1526             mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false);
1527         }
1528         if (getNextPage() > 0) {
1529             setSwipeDownShouldLaunchApp(true);
1530         }
1531         InteractionJankMonitorWrapper.end(Cuj.CUJ_RECENTS_SCROLLING);
1532     }
1533 
1534     @Override
isSignificantMove(float absoluteDelta, int pageOrientedSize)1535     protected boolean isSignificantMove(float absoluteDelta, int pageOrientedSize) {
1536         DeviceProfile deviceProfile = mContainer.getDeviceProfile();
1537         if (!deviceProfile.isTablet) {
1538             return super.isSignificantMove(absoluteDelta, pageOrientedSize);
1539         }
1540 
1541         return absoluteDelta
1542                 > deviceProfile.availableWidthPx * SIGNIFICANT_MOVE_SCREEN_WIDTH_PERCENTAGE;
1543     }
1544 
1545     @Override
onInterceptTouchEvent(MotionEvent ev)1546     public boolean onInterceptTouchEvent(MotionEvent ev) {
1547         boolean intercept = super.onInterceptTouchEvent(ev);
1548         if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
1549             Log.d("b/318590728", "onInterceptTouchEvent: " + ev);
1550         }
1551         return intercept;
1552     }
1553 
1554     @Override
onTouchEvent(MotionEvent ev)1555     public boolean onTouchEvent(MotionEvent ev) {
1556         super.onTouchEvent(ev);
1557 
1558         if (showAsGrid()) {
1559             int taskCount = getTaskViewCount();
1560             for (int i = 0; i < taskCount; i++) {
1561                 TaskView taskView = requireTaskViewAt(i);
1562                 if (isTaskViewVisible(taskView) && taskView.offerTouchToChildren(ev)) {
1563                     // Keep consuming events to pass to delegate
1564                     return true;
1565                 }
1566             }
1567         } else {
1568             TaskView taskView = getCurrentPageTaskView();
1569             if (taskView != null && taskView.offerTouchToChildren(ev)) {
1570                 // Keep consuming events to pass to delegate
1571                 return true;
1572             }
1573         }
1574 
1575         final int x = (int) ev.getX();
1576         final int y = (int) ev.getY();
1577         switch (ev.getAction()) {
1578             case MotionEvent.ACTION_UP:
1579                 if (mTouchDownToStartHome) {
1580                     startHome();
1581                 }
1582                 mTouchDownToStartHome = false;
1583                 break;
1584             case MotionEvent.ACTION_CANCEL:
1585                 mTouchDownToStartHome = false;
1586                 break;
1587             case MotionEvent.ACTION_MOVE:
1588                 // Passing the touch slop will not allow dismiss to home
1589                 if (mTouchDownToStartHome &&
1590                         (isHandlingTouch() ||
1591                                 squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop)) {
1592                     mTouchDownToStartHome = false;
1593                 }
1594                 break;
1595             case MotionEvent.ACTION_DOWN:
1596                 // Touch down anywhere but the deadzone around the visible clear all button and
1597                 // between the task views will start home on touch up
1598                 if (!isHandlingTouch() && !isModal()) {
1599                     if (mShowEmptyMessage) {
1600                         mTouchDownToStartHome = true;
1601                     } else {
1602                         updateDeadZoneRects();
1603                         final boolean clearAllButtonDeadZoneConsumed =
1604                                 mClearAllButton.getAlpha() == 1
1605                                         && mClearAllButtonDeadZoneRect.contains(x, y);
1606                         final boolean cameFromNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
1607                         if (!clearAllButtonDeadZoneConsumed && !cameFromNavBar
1608                                 && !mTaskViewDeadZoneRect.contains(x + getScrollX(), y)) {
1609                             mTouchDownToStartHome = true;
1610                         }
1611                     }
1612                 }
1613                 mDownX = x;
1614                 mDownY = y;
1615                 break;
1616         }
1617 
1618         return isHandlingTouch();
1619     }
1620 
1621     @Override
onNotSnappingToPageInFreeScroll()1622     protected void onNotSnappingToPageInFreeScroll() {
1623         int finalPos = mScroller.getFinalX();
1624         if (finalPos > mMinScroll && finalPos < mMaxScroll) {
1625             int firstPageScroll = getScrollForPage(!mIsRtl ? 0 : getPageCount() - 1);
1626             int lastPageScroll = getScrollForPage(!mIsRtl ? getPageCount() - 1 : 0);
1627 
1628             // If scrolling ends in the half of the added space that is closer to
1629             // the end, settle to the end. Otherwise snap to the nearest page.
1630             // If flinging past one of the ends, don't change the velocity as it
1631             // will get stopped at the end anyway.
1632             int pageSnapped = finalPos < (firstPageScroll + mMinScroll) / 2
1633                     ? mMinScroll
1634                     : finalPos > (lastPageScroll + mMaxScroll) / 2
1635                             ? mMaxScroll
1636                             : getScrollForPage(mNextPage);
1637 
1638             if (showAsGrid()) {
1639                 if (isSplitSelectionActive()) {
1640                     return;
1641                 }
1642                 TaskView taskView = getTaskViewAt(mNextPage);
1643                 // Snap to fully visible focused task and clear all button.
1644                 boolean shouldSnapToFocusedTask = taskView != null && taskView.isFocusedTask()
1645                         && isTaskViewFullyVisible(taskView);
1646                 boolean shouldSnapToClearAll = mNextPage == indexOfChild(mClearAllButton);
1647                 if (!shouldSnapToFocusedTask && !shouldSnapToClearAll) {
1648                     return;
1649                 }
1650             }
1651 
1652             mScroller.setFinalX(pageSnapped);
1653             // Ensure the scroll/snap doesn't happen too fast;
1654             int extraScrollDuration = OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION
1655                     - mScroller.getDuration();
1656             if (extraScrollDuration > 0) {
1657                 mScroller.extendDuration(extraScrollDuration);
1658             }
1659         }
1660     }
1661 
1662     @Override
onEdgeAbsorbingScroll()1663     protected void onEdgeAbsorbingScroll() {
1664         vibrateForScroll();
1665     }
1666 
1667     @Override
onScrollOverPageChanged()1668     protected void onScrollOverPageChanged() {
1669         vibrateForScroll();
1670     }
1671 
vibrateForScroll()1672     private void vibrateForScroll() {
1673         long now = SystemClock.uptimeMillis();
1674         if (now - mScrollLastHapticTimestamp > mScrollHapticMinGapMillis) {
1675             mScrollLastHapticTimestamp = now;
1676             VibratorWrapper.INSTANCE.get(mContext).vibrate(SCROLL_VIBRATION_PRIMITIVE,
1677                     SCROLL_VIBRATION_PRIMITIVE_SCALE, SCROLL_VIBRATION_FALLBACK);
1678         }
1679     }
1680 
1681     @Override
determineScrollingStart(MotionEvent ev, float touchSlopScale)1682     protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) {
1683         // Enables swiping to the left or right only if the task overlay is not modal.
1684         if (!isModal()) {
1685             super.determineScrollingStart(ev, touchSlopScale);
1686         }
1687     }
1688 
1689     /**
1690      * Moves the running task to the front of the carousel in tablets, to minimize animation
1691      * required to move the running task in grid.
1692      */
moveRunningTaskToFront()1693     public void moveRunningTaskToFront() {
1694         if (!mContainer.getDeviceProfile().isTablet) {
1695             return;
1696         }
1697 
1698         TaskView runningTaskView = getRunningTaskView();
1699         if (runningTaskView == null) {
1700             return;
1701         }
1702 
1703         if (indexOfChild(runningTaskView) != mCurrentPage) {
1704             return;
1705         }
1706 
1707         if (mCurrentPage == 0) {
1708             return;
1709         }
1710 
1711         int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this);
1712         int currentPageScroll = getScrollForPage(mCurrentPage);
1713         mCurrentPageScrollDiff = primaryScroll - currentPageScroll;
1714 
1715         mMovingTaskView = runningTaskView;
1716         removeView(runningTaskView);
1717         mMovingTaskView = null;
1718         runningTaskView.resetPersistentViewTransforms();
1719         addView(runningTaskView, 0);
1720         setCurrentPage(0);
1721 
1722         updateTaskSize();
1723     }
1724 
1725     @Override
onScrollerAnimationAborted()1726     protected void onScrollerAnimationAborted() {
1727         ActiveGestureLog.INSTANCE.addLog("scroller animation aborted",
1728                 ActiveGestureErrorDetector.GestureEvent.SCROLLER_ANIMATION_ABORTED);
1729     }
1730 
1731     @Override
isPageScrollsInitialized()1732     protected boolean isPageScrollsInitialized() {
1733         return super.isPageScrollsInitialized() && mLoadPlanEverApplied;
1734     }
1735 
applyLoadPlan(List<GroupTask> taskGroups)1736     protected void applyLoadPlan(List<GroupTask> taskGroups) {
1737         if (mPendingAnimation != null) {
1738             mPendingAnimation.addEndListener(success -> applyLoadPlan(taskGroups));
1739             return;
1740         }
1741 
1742         if (taskGroups == null) {
1743             Log.d(TAG, "applyLoadPlan - taskGroups is null");
1744         } else {
1745             Log.d(TAG, "applyLoadPlan - taskGroups: " + taskGroups.stream().map(
1746                     GroupTask::toString).toList());
1747         }
1748         mLoadPlanEverApplied = true;
1749         if (taskGroups == null || taskGroups.isEmpty()) {
1750             removeTasksViewsAndClearAllButton();
1751             onTaskStackUpdated();
1752             // With all tasks removed, touch handling in PagedView is disabled and we need to reset
1753             // touch state or otherwise values will be obsolete.
1754             resetTouchState();
1755             if (isPageScrollsInitialized()) {
1756                 onPageScrollsInitialized();
1757             }
1758             return;
1759         }
1760 
1761         int[] currentTaskIds;
1762         TaskView currentTaskView = getTaskViewAt(mCurrentPage);
1763         if (currentTaskView != null) {
1764             currentTaskIds = currentTaskView.getTaskIds();
1765         } else {
1766             currentTaskIds = new int[0];
1767         }
1768 
1769         // Unload existing visible task data
1770         unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
1771 
1772         TaskView ignoreResetTaskView =
1773                 mIgnoreResetTaskId == INVALID_TASK_ID
1774                         ? null : getTaskViewByTaskId(mIgnoreResetTaskId);
1775 
1776         // Save running task ID if it exists before rebinding all taskViews, otherwise the task from
1777         // the runningTaskView currently bound could get assigned to another TaskView
1778         int[] runningTaskIds = getTaskIdsForTaskViewId(mRunningTaskViewId);
1779         int[] focusedTaskIds = getTaskIdsForTaskViewId(mFocusedTaskViewId);
1780 
1781         // Reset the focused task to avoiding initializing TaskViews layout as focused task during
1782         // binding. The focused task view will be updated after all the TaskViews are bound.
1783         mFocusedTaskViewId = INVALID_TASK_ID;
1784 
1785         // Removing views sets the currentPage to 0, so we save this and restore it after
1786         // the new set of views are added
1787         int previousCurrentPage = mCurrentPage;
1788         int previousFocusedPage = indexOfChild(getFocusedChild());
1789         removeAllViews();
1790 
1791         // If we are entering Overview as a result of initiating a split from somewhere else
1792         // (e.g. split from Home), we need to make sure the staged app is not drawn as a thumbnail.
1793         int stagedTaskIdToBeRemoved;
1794         if (isSplitSelectionActive()) {
1795             stagedTaskIdToBeRemoved = mSplitSelectStateController.getInitialTaskId();
1796             updateCurrentTaskActionsVisibility();
1797         } else {
1798             stagedTaskIdToBeRemoved = INVALID_TASK_ID;
1799         }
1800         // update the map of instance counts
1801         mFilterState.updateInstanceCountMap(taskGroups);
1802 
1803         // Clear out desktop view if it is set
1804         mDesktopTaskView = null;
1805 
1806         // Add views as children based on whether it's grouped or single task. Looping through
1807         // taskGroups backwards populates the thumbnail grid from least recent to most recent.
1808         for (int i = taskGroups.size() - 1; i >= 0; i--) {
1809             GroupTask groupTask = taskGroups.get(i);
1810             boolean isRemovalNeeded = stagedTaskIdToBeRemoved != INVALID_TASK_ID
1811                     && groupTask.containsTask(stagedTaskIdToBeRemoved);
1812 
1813             if (isRemovalNeeded && !groupTask.hasMultipleTasks()) {
1814                 // If the task we need to remove is not part of a pair, avoiding creating the
1815                 // TaskView.
1816                 continue;
1817             }
1818 
1819             // If we need to remove half of a pair of tasks, force a TaskView with Type.SINGLE
1820             // to be a temporary container for the remaining task.
1821             TaskView taskView = getTaskViewFromPool(
1822                     isRemovalNeeded ? TaskView.Type.SINGLE : groupTask.taskViewType);
1823             if (taskView instanceof GroupedTaskView) {
1824                 boolean firstTaskIsLeftTopTask =
1825                         groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id;
1826                 Task leftTopTask = firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2;
1827                 Task rightBottomTask = firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1;
1828                 ((GroupedTaskView) taskView).bind(leftTopTask, rightBottomTask, mOrientationState,
1829                         mTaskOverlayFactory, groupTask.mSplitBounds);
1830             } else if (taskView instanceof DesktopTaskView) {
1831                 ((DesktopTaskView) taskView).bind(((DesktopTask) groupTask).tasks,
1832                         mOrientationState, mTaskOverlayFactory);
1833                 mDesktopTaskView = (DesktopTaskView) taskView;
1834             } else {
1835                 Task task = groupTask.task1.key.id == stagedTaskIdToBeRemoved ? groupTask.task2
1836                         : groupTask.task1;
1837                 taskView.bind(task, mOrientationState, mTaskOverlayFactory);
1838             }
1839             addView(taskView);
1840 
1841             // enables instance filtering if the feature flag for it is on
1842             if (FeatureFlags.ENABLE_MULTI_INSTANCE.get()) {
1843                 taskView.setUpShowAllInstancesListener();
1844             }
1845         }
1846 
1847         if (!taskGroups.isEmpty()) {
1848             addView(mClearAllButton);
1849         }
1850 
1851         // Keep same previous focused task
1852         TaskView newFocusedTaskView = getTaskViewByTaskIds(focusedTaskIds);
1853         // If the list changed, maybe the focused task doesn't exist anymore
1854         if (newFocusedTaskView == null && getTaskViewCount() > 0) {
1855             newFocusedTaskView = getTaskViewAt(0);
1856         }
1857         mFocusedTaskViewId = newFocusedTaskView != null && !enableGridOnlyOverview()
1858                 ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID;
1859         updateTaskSize();
1860         updateChildTaskOrientations();
1861 
1862         TaskView newRunningTaskView = null;
1863         if (hasAllValidTaskIds(runningTaskIds)) {
1864             // Update mRunningTaskViewId to be the new TaskView that was assigned by binding
1865             // the full list of tasks to taskViews
1866             newRunningTaskView = getTaskViewByTaskIds(runningTaskIds);
1867             if (newRunningTaskView != null) {
1868                 setRunningTaskViewId(newRunningTaskView.getTaskViewId());
1869             } else {
1870                 if (mActiveGestureRunningTasks != null) {
1871                     // This will update mRunningTaskViewId and create a stub view if necessary.
1872                     // We try to avoid this because it can cause a scroll jump, but it is needed
1873                     // for cases where the running task isn't included in this load plan (e.g. if
1874                     // the current running task is excludedFromRecents.)
1875                     showCurrentTask(mActiveGestureRunningTasks);
1876                 } else {
1877                     setRunningTaskViewId(INVALID_TASK_ID);
1878                 }
1879             }
1880         }
1881 
1882         int targetPage = -1;
1883         if (mNextPage != INVALID_PAGE) {
1884             // Restore mCurrentPage but don't call setCurrentPage() as that clobbers the scroll.
1885             mCurrentPage = previousCurrentPage;
1886             if (hasAllValidTaskIds(currentTaskIds)) {
1887                 currentTaskView = getTaskViewByTaskIds(currentTaskIds);
1888                 if (currentTaskView != null) {
1889                     targetPage = indexOfChild(currentTaskView);
1890                 }
1891             }
1892         } else if (previousFocusedPage != INVALID_PAGE) {
1893             targetPage = previousFocusedPage;
1894         } else {
1895             // Set the current page to the running task, but not if settling on new task.
1896             if (hasAllValidTaskIds(runningTaskIds)) {
1897                 targetPage = indexOfChild(newRunningTaskView);
1898             } else if (getTaskViewCount() > 0) {
1899                 targetPage = indexOfChild(requireTaskViewAt(0));
1900             }
1901         }
1902         if (targetPage != -1 && mCurrentPage != targetPage) {
1903             int finalTargetPage = targetPage;
1904             runOnPageScrollsInitialized(() -> {
1905                 // TODO(b/246283207): Remove logging once root cause of flake detected.
1906                 if (Utilities.isRunningInTestHarness()) {
1907                     Log.d("b/246283207", "RecentsView#applyLoadPlan() -> "
1908                             + "previousCurrentPage: " + previousCurrentPage
1909                             + ", targetPage: " + finalTargetPage
1910                             + ", getScrollForPage(targetPage): "
1911                             + getScrollForPage(finalTargetPage));
1912                 }
1913                 setCurrentPage(finalTargetPage);
1914             });
1915         }
1916 
1917         if (mIgnoreResetTaskId != INVALID_TASK_ID &&
1918                 getTaskViewByTaskId(mIgnoreResetTaskId) != ignoreResetTaskView) {
1919             // If the taskView mapping is changing, do not preserve the visuals. Since we are
1920             // mostly preserving the first task, and new taskViews are added to the end, it should
1921             // generally map to the same task.
1922             mIgnoreResetTaskId = INVALID_TASK_ID;
1923         }
1924         resetTaskVisuals();
1925         onTaskStackUpdated();
1926         updateEnabledOverlays();
1927         if (isPageScrollsInitialized()) {
1928             onPageScrollsInitialized();
1929         }
1930     }
1931 
isModal()1932     private boolean isModal() {
1933         return mTaskModalness > 0;
1934     }
1935 
isLoadingTasks()1936     public boolean isLoadingTasks() {
1937         return mModel.isLoadingTasksInBackground();
1938     }
1939 
removeTasksViewsAndClearAllButton()1940     private void removeTasksViewsAndClearAllButton() {
1941         // This handles an edge case where applyLoadPlan happens during a gesture when the
1942         // only Task is one with excludeFromRecents, in which case we should not remove it.
1943         final int stubRunningTaskIndex = isGestureActive() ? getRunningTaskIndex() : -1;
1944 
1945         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
1946             if (i == stubRunningTaskIndex) {
1947                 continue;
1948             }
1949             removeView(requireTaskViewAt(i));
1950         }
1951         if (getTaskViewCount() == 0 && indexOfChild(mClearAllButton) != -1) {
1952             removeView(mClearAllButton);
1953         }
1954     }
1955 
getTaskViewCount()1956     public int getTaskViewCount() {
1957         int taskViewCount = getChildCount();
1958         if (indexOfChild(mClearAllButton) != -1) {
1959             taskViewCount--;
1960         }
1961         return taskViewCount;
1962     }
1963 
getGroupedTaskViewCount()1964     public int getGroupedTaskViewCount() {
1965         int groupViewCount = 0;
1966         for (int i = 0; i < getChildCount(); i++) {
1967             if (getChildAt(i) instanceof GroupedTaskView) {
1968                 groupViewCount++;
1969             }
1970         }
1971         return groupViewCount;
1972     }
1973 
1974     /**
1975      * Returns the number of tasks in the top row of the overview grid.
1976      */
getTopRowTaskCountForTablet()1977     public int getTopRowTaskCountForTablet() {
1978         return mTopRowIdSet.size();
1979     }
1980 
1981     /**
1982      * Returns the number of tasks in the bottom row of the overview grid.
1983      */
getBottomRowTaskCountForTablet()1984     public int getBottomRowTaskCountForTablet() {
1985         return getTaskViewCount() - mTopRowIdSet.size() - (enableGridOnlyOverview() ? 0 : 1);
1986     }
1987 
onTaskStackUpdated()1988     protected void onTaskStackUpdated() {
1989         // Lazily update the empty message only when the task stack is reapplied
1990         updateEmptyMessage();
1991     }
1992 
resetTaskVisuals()1993     public void resetTaskVisuals() {
1994         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
1995             TaskView taskView = requireTaskViewAt(i);
1996             if (Arrays.stream(taskView.getTaskIds()).noneMatch(
1997                     taskId -> taskId == mIgnoreResetTaskId)) {
1998                 taskView.resetViewTransforms();
1999                 taskView.setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
2000                 taskView.setStableAlpha(mContentAlpha);
2001                 taskView.setFullscreenProgress(mFullscreenProgress);
2002                 taskView.setModalness(mTaskModalness);
2003                 taskView.setTaskThumbnailSplashAlpha(mTaskThumbnailSplashAlpha);
2004             }
2005         }
2006         // resetTaskVisuals is called at the end of dismiss animation which could update
2007         // primary and secondary translation of the live tile cut out. We will need to do so
2008         // here accordingly.
2009         runActionOnRemoteHandles(remoteTargetHandle -> {
2010             TaskViewSimulator simulator = remoteTargetHandle.getTaskViewSimulator();
2011             simulator.taskPrimaryTranslation.value = 0;
2012             simulator.taskSecondaryTranslation.value = 0;
2013             simulator.fullScreenProgress.value = 0;
2014             simulator.recentsViewScale.value = 1;
2015         });
2016         // Similar to setRunningTaskHidden below, reapply the state before runningTaskView is
2017         // null.
2018         if (!mRunningTaskShowScreenshot) {
2019             setRunningTaskViewShowScreenshot(mRunningTaskShowScreenshot);
2020         }
2021         if (mRunningTaskTileHidden) {
2022             setRunningTaskHidden(mRunningTaskTileHidden);
2023         }
2024 
2025         updateCurveProperties();
2026         // Update the set of visible task's data
2027         loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
2028         setTaskModalness(0);
2029         setColorTint(0);
2030     }
2031 
setFullscreenProgress(float fullscreenProgress)2032     public void setFullscreenProgress(float fullscreenProgress) {
2033         mFullscreenProgress = fullscreenProgress;
2034         if (enableRefactorTaskThumbnail()) {
2035             mRecentsViewData.getFullscreenProgress().setValue(mFullscreenProgress);
2036         }
2037         int taskCount = getTaskViewCount();
2038         for (int i = 0; i < taskCount; i++) {
2039             requireTaskViewAt(i).setFullscreenProgress(mFullscreenProgress);
2040         }
2041         mClearAllButton.setFullscreenProgress(fullscreenProgress);
2042 
2043         // Fade out the actions view quickly (0.1 range)
2044         mActionsView.getFullscreenAlpha().updateValue(
2045                 mapToRange(fullscreenProgress, 0, 0.1f, 1f, 0f, LINEAR));
2046     }
2047 
updateTaskStackListenerState()2048     private void updateTaskStackListenerState() {
2049         boolean handleTaskStackChanges = mOverviewStateEnabled && isAttachedToWindow()
2050                 && getWindowVisibility() == VISIBLE;
2051         if (handleTaskStackChanges != mHandleTaskStackChanges) {
2052             mHandleTaskStackChanges = handleTaskStackChanges;
2053             if (handleTaskStackChanges) {
2054                 reloadIfNeeded();
2055             }
2056         }
2057     }
2058 
2059     @Override
setInsets(Rect insets)2060     public void setInsets(Rect insets) {
2061         mInsets.set(insets);
2062 
2063         // Update DeviceProfile dependant state.
2064         DeviceProfile dp = mContainer.getDeviceProfile();
2065         setOverviewGridEnabled(
2066                 getStateManager().getState().displayOverviewTasksAsGrid(dp));
2067         if (enableGridOnlyOverview()) {
2068             mActionsView.updateHiddenFlags(HIDDEN_ACTIONS_IN_MENU, dp.isTablet);
2069         }
2070         setPageSpacing(dp.overviewPageSpacing);
2071 
2072         // Propagate DeviceProfile change event.
2073         runActionOnRemoteHandles(
2074                 remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator().setDp(dp));
2075         mOrientationState.setDeviceProfile(dp);
2076 
2077         // Update RecentsView and TaskView's DeviceProfile dependent layout.
2078         updateOrientationHandler();
2079         mActionsView.updateDimension(dp, mLastComputedTaskSize);
2080     }
2081 
updateOrientationHandler()2082     private void updateOrientationHandler() {
2083         updateOrientationHandler(true);
2084     }
2085 
updateOrientationHandler(boolean forceRecreateDragLayerControllers)2086     private void updateOrientationHandler(boolean forceRecreateDragLayerControllers) {
2087         // Handle orientation changes.
2088         RecentsPagedOrientationHandler oldOrientationHandler = getPagedOrientationHandler();
2089         setOrientationHandler(mOrientationState.getOrientationHandler());
2090 
2091         mIsRtl = getPagedOrientationHandler().getRecentsRtlSetting(getResources());
2092         setLayoutDirection(mIsRtl
2093                 ? View.LAYOUT_DIRECTION_RTL
2094                 : View.LAYOUT_DIRECTION_LTR);
2095         mClearAllButton.setLayoutDirection(mIsRtl
2096                 ? View.LAYOUT_DIRECTION_LTR
2097                 : View.LAYOUT_DIRECTION_RTL);
2098         mClearAllButton.setRotation(getPagedOrientationHandler().getDegreesRotated());
2099 
2100         boolean isOrientationHandlerChanged =
2101                 !getPagedOrientationHandler().equals(oldOrientationHandler);
2102         if (forceRecreateDragLayerControllers || isOrientationHandlerChanged) {
2103             // Changed orientations, update controllers so they intercept accordingly.
2104             mContainer.getDragLayer().recreateControllers();
2105             onOrientationChanged();
2106             resetTaskVisuals();
2107             // Log fake orientation changed.
2108             if (isOrientationHandlerChanged) {
2109                 logOrientationChanged();
2110             }
2111         }
2112 
2113         boolean isInLandscape = mOrientationState.getTouchRotation() != ROTATION_0
2114                 || mOrientationState.getRecentsActivityRotation() != ROTATION_0;
2115         mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION,
2116                 !mOrientationState.isRecentsActivityRotationAllowed() && isInLandscape);
2117 
2118         // Recalculate DeviceProfile dependent layout.
2119         updateSizeAndPadding();
2120 
2121         // Update TaskView's DeviceProfile dependent layout.
2122         updateChildTaskOrientations();
2123 
2124         requestLayout();
2125         // Reapply the current page to update page scrolls.
2126         setCurrentPage(mCurrentPage);
2127     }
2128 
onOrientationChanged()2129     private void onOrientationChanged() {
2130         // If overview is in modal state when rotate, reset it to overview state without running
2131         // animation.
2132         setModalStateEnabled(/* taskId= */ INVALID_TASK_ID, /* animate= */ false);
2133         if (isSplitSelectionActive()) {
2134             onRotateInSplitSelectionState();
2135         }
2136     }
2137 
2138     // Update task size and padding that are dependent on DeviceProfile and insets.
updateSizeAndPadding()2139     private void updateSizeAndPadding() {
2140         DeviceProfile dp = mContainer.getDeviceProfile();
2141         getTaskSize(mLastComputedTaskSize);
2142         mTaskWidth = mLastComputedTaskSize.width();
2143         mTaskHeight = mLastComputedTaskSize.height();
2144         setPadding(mLastComputedTaskSize.left - mInsets.left,
2145                 mLastComputedTaskSize.top - dp.overviewTaskThumbnailTopMarginPx - mInsets.top,
2146                 dp.widthPx - mInsets.right - mLastComputedTaskSize.right,
2147                 dp.heightPx - mInsets.bottom - mLastComputedTaskSize.bottom);
2148 
2149         mSizeStrategy.calculateGridSize(dp, mContainer, mLastComputedGridSize);
2150         mSizeStrategy.calculateGridTaskSize(mContainer, dp, mLastComputedGridTaskSize,
2151                 getPagedOrientationHandler());
2152 
2153         if (enableGridOnlyOverview()) {
2154             mSizeStrategy.calculateCarouselTaskSize(mContainer, dp, mLastComputedCarouselTaskSize,
2155                     getPagedOrientationHandler());
2156         }
2157 
2158         mTaskGridVerticalDiff = mLastComputedGridTaskSize.top - mLastComputedTaskSize.top;
2159         mTopBottomRowHeightDiff =
2160                 mLastComputedGridTaskSize.height() + dp.overviewTaskThumbnailTopMarginPx
2161                         + dp.overviewRowSpacing;
2162 
2163         // Force TaskView to update size from thumbnail
2164         updateTaskSize();
2165         updatePivots();
2166     }
2167 
2168     /**
2169      * Updates TaskView scaling and translation required to support variable width.
2170      */
updateTaskSize()2171     private void updateTaskSize() {
2172         updateTaskSize(false);
2173     }
2174 
2175     /**
2176      * Updates TaskView scaling and translation required to support variable width.
2177      *
2178      * @param isTaskDismissal indicates if update was called due to task dismissal
2179      */
updateTaskSize(boolean isTaskDismissal)2180     private void updateTaskSize(boolean isTaskDismissal) {
2181         final int taskCount = getTaskViewCount();
2182         if (taskCount == 0) {
2183             return;
2184         }
2185 
2186         float accumulatedTranslationX = 0;
2187         float translateXToMiddle = 0;
2188         if (enableGridOnlyOverview() && mContainer.getDeviceProfile().isTablet) {
2189             translateXToMiddle = mIsRtl
2190                     ? mLastComputedCarouselTaskSize.right - mLastComputedTaskSize.right
2191                     : mLastComputedCarouselTaskSize.left - mLastComputedTaskSize.left;
2192         }
2193         for (int i = 0; i < taskCount; i++) {
2194             TaskView taskView = requireTaskViewAt(i);
2195             taskView.updateTaskSize(mLastComputedTaskSize, mLastComputedGridTaskSize,
2196                     mLastComputedCarouselTaskSize);
2197             taskView.setNonGridTranslationX(accumulatedTranslationX);
2198             taskView.setNonGridPivotTranslationX(translateXToMiddle);
2199             // Compensate space caused by TaskView scaling.
2200             float widthDiff =
2201                     taskView.getLayoutParams().width * (1 - taskView.getNonGridScale());
2202             accumulatedTranslationX += mIsRtl ? widthDiff : -widthDiff;
2203         }
2204 
2205         mClearAllButton.setFullscreenTranslationPrimary(accumulatedTranslationX);
2206 
2207         updateGridProperties(isTaskDismissal);
2208     }
2209 
getTaskSize(Rect outRect)2210     public void getTaskSize(Rect outRect) {
2211         mSizeStrategy.calculateTaskSize(mContainer, mContainer.getDeviceProfile(), outRect,
2212                 getPagedOrientationHandler());
2213     }
2214 
2215     /**
2216      * Sets the last TaskView selected.
2217      */
setSelectedTask(int lastSelectedTaskId)2218     public void setSelectedTask(int lastSelectedTaskId) {
2219         mSelectedTask = getTaskViewByTaskId(lastSelectedTaskId);
2220     }
2221 
2222     /**
2223      * Returns the bounds of the task selected to enter modal state.
2224      */
getSelectedTaskBounds()2225     public Rect getSelectedTaskBounds() {
2226         if (mSelectedTask == null) {
2227             return mLastComputedTaskSize;
2228         }
2229         return getTaskBounds(mSelectedTask);
2230     }
2231 
getTaskBounds(TaskView taskView)2232     private Rect getTaskBounds(TaskView taskView) {
2233         int selectedPage = indexOfChild(taskView);
2234         int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this);
2235         int selectedPageScroll = getScrollForPage(selectedPage);
2236         boolean isTopRow = taskView != null && mTopRowIdSet.contains(taskView.getTaskViewId());
2237         Rect outRect = new Rect(mLastComputedTaskSize);
2238         outRect.offset(
2239                 -(primaryScroll - (selectedPageScroll + getOffsetFromScrollPosition(selectedPage))),
2240                 (int) (showAsGrid() && enableGridOnlyOverview() && !isTopRow
2241                         ? mTopBottomRowHeightDiff : 0));
2242         return outRect;
2243     }
2244 
2245     /** Gets the last computed task size */
getLastComputedTaskSize()2246     public Rect getLastComputedTaskSize() {
2247         return mLastComputedTaskSize;
2248     }
2249 
getLastComputedGridTaskSize()2250     public Rect getLastComputedGridTaskSize() {
2251         return mLastComputedGridTaskSize;
2252     }
2253 
getLastComputedCarouselTaskSize()2254     public Rect getLastComputedCarouselTaskSize() {
2255         return mLastComputedCarouselTaskSize;
2256     }
2257 
2258     /** Gets the task size for modal state. */
getModalTaskSize(Rect outRect)2259     public void getModalTaskSize(Rect outRect) {
2260         mSizeStrategy.calculateModalTaskSize(mContainer, mContainer.getDeviceProfile(), outRect,
2261                 getPagedOrientationHandler());
2262     }
2263 
2264     @Override
computeScrollHelper()2265     protected boolean computeScrollHelper() {
2266         boolean scrolling = super.computeScrollHelper();
2267         boolean isFlingingFast = false;
2268         updateCurveProperties();
2269         if (scrolling || isHandlingTouch()) {
2270             if (scrolling) {
2271                 // Check if we are flinging quickly to disable high res thumbnail loading
2272                 isFlingingFast = mScroller.getCurrVelocity() > mFastFlingVelocity;
2273             }
2274 
2275             // After scrolling, update the visible task's data
2276             loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
2277         }
2278 
2279         // Update ActionsView's visibility when scroll changes.
2280         updateActionsViewFocusedScroll();
2281 
2282         // Update the high res thumbnail loader state
2283         mModel.getThumbnailCache().getHighResLoadingState().setFlingingFast(isFlingingFast);
2284         return scrolling;
2285     }
2286 
updateActionsViewFocusedScroll()2287     private void updateActionsViewFocusedScroll() {
2288         if (showAsGrid()) {
2289             float actionsViewAlphaValue = isFocusedTaskInExpectedScrollPosition() ? 1 : 0;
2290             // If animation is already in progress towards the same end value, do not restart.
2291             if (mActionsViewAlphaAnimator == null || !mActionsViewAlphaAnimator.isStarted()
2292                     || (mActionsViewAlphaAnimator.isStarted()
2293                     && mActionsViewAlphaAnimatorFinalValue != actionsViewAlphaValue)) {
2294                 animateActionsViewAlpha(actionsViewAlphaValue,
2295                         DEFAULT_ACTIONS_VIEW_ALPHA_ANIMATION_DURATION);
2296             }
2297         }
2298     }
2299 
animateActionsViewAlpha(float alphaValue, long duration)2300     private void animateActionsViewAlpha(float alphaValue, long duration) {
2301         mActionsViewAlphaAnimator = ObjectAnimator.ofFloat(mActionsView.getVisibilityAlpha(),
2302                 AnimatedFloat.VALUE, alphaValue);
2303         mActionsViewAlphaAnimatorFinalValue = alphaValue;
2304         mActionsViewAlphaAnimator.setDuration(duration);
2305         // Set autocancel to prevent race-conditiony setting of alpha from other animations
2306         mActionsViewAlphaAnimator.setAutoCancel(true);
2307         mActionsViewAlphaAnimator.start();
2308     }
2309 
2310     /**
2311      * Scales and adjusts translation of adjacent pages as if on a curved carousel.
2312      */
updateCurveProperties()2313     public void updateCurveProperties() {
2314         if (getPageCount() == 0 || getPageAt(0).getMeasuredWidth() == 0) {
2315             return;
2316         }
2317         int scroll = getPagedOrientationHandler().getPrimaryScroll(this);
2318         mClearAllButton.onRecentsViewScroll(scroll, mOverviewGridEnabled);
2319 
2320         // Clear all button alpha was set by the previous line.
2321         mActionsView.getIndexScrollAlpha().updateValue(1 - mClearAllButton.getScrollAlpha());
2322     }
2323 
2324     @Override
getDestinationPage(int scaledScroll)2325     protected int getDestinationPage(int scaledScroll) {
2326         if (!mContainer.getDeviceProfile().isTablet) {
2327             return super.getDestinationPage(scaledScroll);
2328         }
2329         if (!isPageScrollsInitialized()) {
2330             Log.e(TAG,
2331                     "Cannot get destination page: RecentsView not properly initialized",
2332                     new IllegalStateException());
2333             return INVALID_PAGE;
2334         }
2335 
2336         // When in tablet with variable task width, return the page which scroll is closest to
2337         // screenStart instead of page nearest to center of screen.
2338         int minDistanceFromScreenStart = Integer.MAX_VALUE;
2339         int minDistanceFromScreenStartIndex = INVALID_PAGE;
2340         for (int i = 0; i < getChildCount(); ++i) {
2341             int distanceFromScreenStart = Math.abs(mPageScrolls[i] - scaledScroll);
2342             if (distanceFromScreenStart < minDistanceFromScreenStart) {
2343                 minDistanceFromScreenStart = distanceFromScreenStart;
2344                 minDistanceFromScreenStartIndex = i;
2345             }
2346         }
2347         return minDistanceFromScreenStartIndex;
2348     }
2349 
2350     /**
2351      * Iterates through all the tasks, and loads the associated task data for newly visible tasks,
2352      * and unloads the associated task data for tasks that are no longer visible.
2353      */
loadVisibleTaskData(@askView.TaskDataChanges int dataChanges)2354     public void loadVisibleTaskData(@TaskView.TaskDataChanges int dataChanges) {
2355         boolean hasLeftOverview = !mOverviewStateEnabled && mScroller.isFinished();
2356         if (hasLeftOverview || mTaskListChangeId == -1) {
2357             // Skip loading visible task data if we've already left the overview state, or if the
2358             // task list hasn't been loaded yet (the task views will not reflect the task list)
2359             return;
2360         }
2361 
2362         int lower = 0;
2363         int upper = 0;
2364         int visibleStart = 0;
2365         int visibleEnd = 0;
2366         if (showAsGrid()) {
2367             int screenStart = getPagedOrientationHandler().getPrimaryScroll(this);
2368             int pageOrientedSize = getPagedOrientationHandler().getMeasuredSize(this);
2369             // For GRID_ONLY_OVERVIEW, use +/- 1 task column as visible area for preloading
2370             // adjacent thumbnails, otherwise use +/-50% screen width
2371             int extraWidth = enableGridOnlyOverview() ? getLastComputedTaskSize().width()
2372                     + getPageSpacing() : pageOrientedSize / 2;
2373             visibleStart = screenStart - extraWidth;
2374             visibleEnd = screenStart + pageOrientedSize + extraWidth;
2375         } else {
2376             int centerPageIndex = getPageNearestToCenterOfScreen();
2377             int numChildren = getChildCount();
2378             lower = Math.max(0, centerPageIndex - 2);
2379             upper = Math.min(centerPageIndex + 2, numChildren - 1);
2380         }
2381 
2382         List<Integer> visibleTaskIds = new ArrayList<>();
2383 
2384         // Update the task data for the in/visible children
2385         for (int i = 0; i < getTaskViewCount(); i++) {
2386             TaskView taskView = requireTaskViewAt(i);
2387             List<TaskContainer> containers = taskView.getTaskContainers();
2388             if (containers.isEmpty()) {
2389                 continue;
2390             }
2391             int index = indexOfChild(taskView);
2392             boolean visible;
2393             if (showAsGrid()) {
2394                 visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd);
2395             } else {
2396                 visible = lower <= index && index <= upper;
2397             }
2398             if (visible) {
2399                 // Default update all non-null tasks, then remove running ones
2400                 List<Task> tasksToUpdate = containers.stream()
2401                         .map(TaskContainer::getTask)
2402                         .collect(Collectors.toCollection(ArrayList::new));
2403                 if (enableRefactorTaskThumbnail()) {
2404                     visibleTaskIds.addAll(
2405                             tasksToUpdate.stream().map((task) -> task.key.id).toList());
2406                 }
2407                 if (mTmpRunningTasks != null) {
2408                     for (Task t : mTmpRunningTasks) {
2409                         // Skip loading if this is the task that we are animating into
2410                         // TODO(b/280812109) change this equality check to use A.equals(B)
2411                         tasksToUpdate.removeIf(task -> task == t);
2412                     }
2413                 }
2414                 if (tasksToUpdate.isEmpty()) {
2415                     continue;
2416                 }
2417                 for (Task task : tasksToUpdate) {
2418                     if (!mHasVisibleTaskData.get(task.key.id)) {
2419                         // Ignore thumbnail update if it's current running task during the gesture
2420                         // We snapshot at end of gesture, it will update then
2421                         int changes = dataChanges;
2422                         if (taskView == getRunningTaskView() && isGestureActive()) {
2423                             changes &= ~TaskView.FLAG_UPDATE_THUMBNAIL;
2424                         }
2425                         taskView.onTaskListVisibilityChanged(true /* visible */, changes);
2426                     }
2427                     mHasVisibleTaskData.put(task.key.id, visible);
2428                 }
2429             } else {
2430                 for (TaskContainer container : containers) {
2431                     if (container == null) {
2432                         continue;
2433                     }
2434 
2435                     if (mHasVisibleTaskData.get(container.getTask().key.id)) {
2436                         taskView.onTaskListVisibilityChanged(false /* visible */, dataChanges);
2437                     }
2438                     mHasVisibleTaskData.delete(container.getTask().key.id);
2439                 }
2440             }
2441         }
2442         if (enableRefactorTaskThumbnail()) {
2443             mTasksRepository.setVisibleTasks(visibleTaskIds);
2444         }
2445     }
2446 
2447     /**
2448      * Unloads any associated data from the currently visible tasks
2449      */
unloadVisibleTaskData(@askView.TaskDataChanges int dataChanges)2450     private void unloadVisibleTaskData(@TaskView.TaskDataChanges int dataChanges) {
2451         for (int i = 0; i < mHasVisibleTaskData.size(); i++) {
2452             if (mHasVisibleTaskData.valueAt(i)) {
2453                 TaskView taskView = getTaskViewByTaskId(mHasVisibleTaskData.keyAt(i));
2454                 if (taskView != null) {
2455                     taskView.onTaskListVisibilityChanged(false /* visible */, dataChanges);
2456                 }
2457             }
2458         }
2459         mHasVisibleTaskData.clear();
2460     }
2461 
2462     @Override
onHighResLoadingStateChanged(boolean enabled)2463     public void onHighResLoadingStateChanged(boolean enabled) {
2464         // Preload cache when no overview task is visible (e.g. not in overview page), so when
2465         // user goes to overview next time, the task thumbnails would show up without delay
2466         if (mHasVisibleTaskData.size() == 0) {
2467             mModel.preloadCacheIfNeeded();
2468         }
2469 
2470         // Whenever the high res loading state changes, poke each of the visible tasks to see if
2471         // they want to updated their thumbnail state
2472         for (int i = 0; i < mHasVisibleTaskData.size(); i++) {
2473             if (mHasVisibleTaskData.valueAt(i)) {
2474                 TaskView taskView = getTaskViewByTaskId(mHasVisibleTaskData.keyAt(i));
2475                 if (taskView != null) {
2476                     // Poke the view again, which will trigger it to load high res if the state
2477                     // is enabled
2478                     taskView.onTaskListVisibilityChanged(true /* visible */);
2479                 }
2480             }
2481         }
2482     }
2483 
startHome()2484     public void startHome() {
2485         startHome(mContainer.isStarted());
2486     }
2487 
startHome(boolean animated)2488     public void startHome(boolean animated) {
2489         if (!canStartHomeSafely()) return;
2490         handleStartHome(animated);
2491     }
2492 
handleStartHome(boolean animated)2493     protected abstract void handleStartHome(boolean animated);
2494 
2495     /** Returns whether user can start home based on state in {@link OverviewCommandHelper}. */
canStartHomeSafely()2496     protected abstract boolean canStartHomeSafely();
2497 
2498     /** Returns the state manager used in RecentsView **/
2499     public abstract StateManager<STATE_TYPE,
getStateManager()2500             ? extends StatefulContainer<STATE_TYPE>> getStateManager();
2501 
reset()2502     public void reset() {
2503         setCurrentTask(-1);
2504         mCurrentPageScrollDiff = 0;
2505         mIgnoreResetTaskId = -1;
2506         mTaskListChangeId = -1;
2507         mFocusedTaskViewId = -1;
2508 
2509         Log.d(TAG, "reset - mEnableDrawingLiveTile: " + mEnableDrawingLiveTile
2510                 + ", mRecentsAnimationController: " + mRecentsAnimationController);
2511         if (mEnableDrawingLiveTile) {
2512             if (mRecentsAnimationController != null) {
2513                 // We owns mRecentsAnimationController, finish it now to clean up.
2514                 finishRecentsAnimation(true /* toRecents */, null);
2515             } else {
2516                 // Only clean up target set if we no longer owns mRecentsAnimationController.
2517                 runActionOnRemoteHandles(remoteTargetHandle ->
2518                         remoteTargetHandle.getTransformParams().setTargetSet(null));
2519             }
2520             setEnableDrawingLiveTile(false);
2521         }
2522         runActionOnRemoteHandles(remoteTargetHandle ->
2523                 remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(false));
2524         if (!FeatureFlags.enableSplitContextually()) {
2525             resetFromSplitSelectionState();
2526         }
2527 
2528         // These are relatively expensive and don't need to be done this frame (RecentsView isn't
2529         // visible anyway), so defer by a frame to get off the critical path, e.g. app to home.
2530         post(() -> {
2531             unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
2532             setCurrentPage(0);
2533             LayoutUtils.setViewEnabled(mActionsView, true);
2534             if (mOrientationState.setGestureActive(false)) {
2535                 updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false);
2536             }
2537         });
2538     }
2539 
getRunningTaskViewId()2540     public int getRunningTaskViewId() {
2541         return mRunningTaskViewId;
2542     }
2543 
getTaskIdsForRunningTaskView()2544     protected int[] getTaskIdsForRunningTaskView() {
2545         return getTaskIdsForTaskViewId(mRunningTaskViewId);
2546     }
2547 
getTaskIdsForTaskViewId(int taskViewId)2548     private int[] getTaskIdsForTaskViewId(int taskViewId) {
2549         // For now 2 distinct task IDs is max for split screen
2550         TaskView runningTaskView = getTaskViewFromTaskViewId(taskViewId);
2551         if (runningTaskView == null) {
2552             return new int[0];
2553         }
2554 
2555         return runningTaskView.getTaskIds();
2556     }
2557 
getRunningTaskView()2558     public @Nullable TaskView getRunningTaskView() {
2559         return getTaskViewFromTaskViewId(mRunningTaskViewId);
2560     }
2561 
getFocusedTaskView()2562     public @Nullable TaskView getFocusedTaskView() {
2563         return getTaskViewFromTaskViewId(mFocusedTaskViewId);
2564     }
2565 
2566     @Nullable
getTaskViewFromTaskViewId(int taskViewId)2567     private TaskView getTaskViewFromTaskViewId(int taskViewId) {
2568         if (taskViewId == -1) {
2569             return null;
2570         }
2571 
2572         for (int i = 0; i < getTaskViewCount(); i++) {
2573             TaskView taskView = requireTaskViewAt(i);
2574             if (taskView.getTaskViewId() == taskViewId) {
2575                 return taskView;
2576             }
2577         }
2578         return null;
2579     }
2580 
getRunningTaskIndex()2581     public int getRunningTaskIndex() {
2582         TaskView taskView = getRunningTaskView();
2583         return taskView == null ? -1 : indexOfChild(taskView);
2584     }
2585 
getHomeTaskView()2586     protected @Nullable TaskView getHomeTaskView() {
2587         return null;
2588     }
2589 
2590     /**
2591      * Handle the edge case where Recents could increment task count very high over long
2592      * period of device usage. Probably will never happen, but meh.
2593      */
getTaskViewFromPool(@askView.Type int type)2594     private TaskView getTaskViewFromPool(@TaskView.Type int type) {
2595         TaskView taskView;
2596         switch (type) {
2597             case TaskView.Type.GROUPED:
2598                 taskView = mGroupedTaskViewPool.getView();
2599                 break;
2600             case TaskView.Type.DESKTOP:
2601                 taskView = mDesktopTaskViewPool.getView();
2602                 break;
2603             case TaskView.Type.SINGLE:
2604             default:
2605                 taskView = mTaskViewPool.getView();
2606         }
2607         taskView.setTaskViewId(mTaskViewIdCount);
2608         if (mTaskViewIdCount == Integer.MAX_VALUE) {
2609             mTaskViewIdCount = 0;
2610         } else {
2611             mTaskViewIdCount++;
2612         }
2613 
2614         return taskView;
2615     }
2616 
2617     /**
2618      * Get the index of the task view whose id matches {@param taskId}.
2619      * @return -1 if there is no task view for the task id, else the index of the task view.
2620      */
getTaskIndexForId(int taskId)2621     public int getTaskIndexForId(int taskId) {
2622         TaskView tv = getTaskViewByTaskId(taskId);
2623         return tv == null ? -1 : indexOfChild(tv);
2624     }
2625 
2626     /**
2627      * Reloads the view if anything in recents changed.
2628      */
reloadIfNeeded()2629     public void reloadIfNeeded() {
2630         if (!mModel.isTaskListValid(mTaskListChangeId)) {
2631             mTaskListChangeId = mModel.getTasks(this::applyLoadPlan, RecentsFilterState
2632                     .getFilter(mFilterState.getPackageNameToFilter()));
2633             if (enableRefactorTaskThumbnail()) {
2634                 mTasksRepository.getAllTaskData(/* forceRefresh = */ true);
2635             }
2636         }
2637     }
2638 
2639     /**
2640      * Called when a gesture from an app is starting.
2641      */
onGestureAnimationStart( Task[] runningTasks, RotationTouchHelper rotationTouchHelper)2642     public void onGestureAnimationStart(
2643             Task[] runningTasks, RotationTouchHelper rotationTouchHelper) {
2644         Log.d(TAG, "onGestureAnimationStart - runningTasks: " + Arrays.toString(runningTasks));
2645         mActiveGestureRunningTasks = runningTasks;
2646         // This needs to be called before the other states are set since it can create the task view
2647         if (mOrientationState.setGestureActive(true)) {
2648             setLayoutRotation(rotationTouchHelper.getCurrentActiveRotation(),
2649                     rotationTouchHelper.getDisplayRotation());
2650             // Force update to ensure the initial task size is computed even if the orientation has
2651             // not changed.
2652             updateSizeAndPadding();
2653         }
2654 
2655         showCurrentTask(mActiveGestureRunningTasks);
2656         setEnableFreeScroll(false);
2657         setEnableDrawingLiveTile(false);
2658         setRunningTaskHidden(true);
2659         setTaskIconScaledDown(true);
2660     }
2661 
isGestureActive()2662     private boolean isGestureActive() {
2663         return mActiveGestureRunningTasks != null;
2664     }
2665 
2666     /**
2667      * Called only when a swipe-up gesture from an app has completed. Only called after
2668      * {@link #onGestureAnimationStart} and {@link #onGestureAnimationEnd()}.
2669      */
onSwipeUpAnimationSuccess()2670     public void onSwipeUpAnimationSuccess() {
2671         animateUpTaskIconScale();
2672         setSwipeDownShouldLaunchApp(true);
2673     }
2674 
animateRecentsRotationInPlace(int newRotation)2675     private void animateRecentsRotationInPlace(int newRotation) {
2676         if (mOrientationState.isRecentsActivityRotationAllowed()) {
2677             // Let system take care of the rotation
2678             return;
2679         }
2680 
2681         if (mRunningTaskShowScreenshot) {
2682             animateRotation(newRotation);
2683         } else {
2684             // Animate the rotation and stops running task
2685             switchToScreenshot(() -> {
2686                 animateRotation(newRotation);
2687                 finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
2688                         null /* onFinishComplete */);
2689             });
2690         }
2691     }
2692 
animateRotation(int newRotation)2693     private void animateRotation(int newRotation) {
2694         AbstractFloatingView.closeAllOpenViewsExcept(mContainer, false, TYPE_REBIND_SAFE);
2695         AnimatorSet pa = setRecentsChangedOrientation(true);
2696         pa.addListener(AnimatorListeners.forSuccessCallback(() -> {
2697             setLayoutRotation(newRotation, mOrientationState.getDisplayRotation());
2698             mContainer.getDragLayer().recreateControllers();
2699             setRecentsChangedOrientation(false).start();
2700         }));
2701         pa.start();
2702     }
2703 
setRecentsChangedOrientation(boolean fadeOut)2704     public AnimatorSet setRecentsChangedOrientation(boolean fadeOut) {
2705         AnimatorSet as = new AnimatorSet();
2706         as.play(ObjectAnimator.ofFloat(this, View.ALPHA, fadeOut ? 0 : 1));
2707         return as;
2708     }
2709 
updateChildTaskOrientations()2710     private void updateChildTaskOrientations() {
2711         for (int i = 0; i < getTaskViewCount(); i++) {
2712             requireTaskViewAt(i).setOrientationState(mOrientationState);
2713         }
2714         boolean shouldRotateMenuForFakeRotation =
2715                 !mOrientationState.isRecentsActivityRotationAllowed();
2716         if (!shouldRotateMenuForFakeRotation) {
2717             return;
2718         }
2719         TaskMenuView tv = (TaskMenuView) getTopOpenViewWithType(mContainer, TYPE_TASK_MENU);
2720         if (tv != null) {
2721             // Rotation is supported on phone (details at b/254198019#comment4)
2722             tv.onRotationChanged();
2723         }
2724     }
2725 
2726     /**
2727      * Called when a gesture from an app has finished, and an end target has been determined.
2728      */
onPrepareGestureEndAnimation( @ullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget, TaskViewSimulator[] taskViewSimulators)2729     public void onPrepareGestureEndAnimation(
2730             @Nullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget,
2731             TaskViewSimulator[] taskViewSimulators) {
2732         Log.d(TAG, "onPrepareGestureEndAnimation - endTarget: " + endTarget);
2733         mCurrentGestureEndTarget = endTarget;
2734         boolean isOverviewEndTarget = endTarget == GestureState.GestureEndTarget.RECENTS;
2735         if (isOverviewEndTarget) {
2736             updateGridProperties();
2737         }
2738 
2739         BaseState<?> endState = mSizeStrategy.stateFromGestureEndTarget(endTarget);
2740         if (endState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile())) {
2741             TaskView runningTaskView = getRunningTaskView();
2742             float runningTaskPrimaryGridTranslation = 0;
2743             float runningTaskSecondaryGridTranslation = 0;
2744             if (runningTaskView != null) {
2745                 // Apply the grid translation to running task unless it's being snapped to
2746                 // and removes the current translation applied to the running task.
2747                 runningTaskPrimaryGridTranslation = runningTaskView.getGridTranslationX()
2748                         - runningTaskView.getNonGridTranslationX();
2749                 runningTaskSecondaryGridTranslation = runningTaskView.getGridTranslationY();
2750             }
2751             for (TaskViewSimulator tvs : taskViewSimulators) {
2752                 if (animatorSet == null) {
2753                     setGridProgress(1);
2754                     tvs.taskPrimaryTranslation.value = runningTaskPrimaryGridTranslation;
2755                     tvs.taskSecondaryTranslation.value = runningTaskSecondaryGridTranslation;
2756                 } else {
2757                     animatorSet.play(ObjectAnimator.ofFloat(this, RECENTS_GRID_PROGRESS, 1));
2758                     animatorSet.play(tvs.carouselScale.animateToValue(1));
2759                     animatorSet.play(tvs.carouselPrimaryTranslation.animateToValue(0));
2760                     animatorSet.play(tvs.carouselSecondaryTranslation.animateToValue(0));
2761                     animatorSet.play(tvs.taskPrimaryTranslation.animateToValue(
2762                             runningTaskPrimaryGridTranslation));
2763                     animatorSet.play(tvs.taskSecondaryTranslation.animateToValue(
2764                             runningTaskSecondaryGridTranslation));
2765                 }
2766             }
2767         }
2768         int splashAlpha = endState.showTaskThumbnailSplash() ? 1 : 0;
2769         if (animatorSet == null) {
2770             setTaskThumbnailSplashAlpha(splashAlpha);
2771         } else {
2772             animatorSet.play(
2773                     ObjectAnimator.ofFloat(this, TASK_THUMBNAIL_SPLASH_ALPHA, splashAlpha));
2774         }
2775     }
2776 
2777     /**
2778      * Called when a gesture from an app has finished, and the animation to the target has ended.
2779      */
onGestureAnimationEnd()2780     public void onGestureAnimationEnd() {
2781         mActiveGestureRunningTasks = null;
2782         if (mOrientationState.setGestureActive(false)) {
2783             updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false);
2784         }
2785 
2786         setEnableFreeScroll(true);
2787         setEnableDrawingLiveTile(mCurrentGestureEndTarget == GestureState.GestureEndTarget.RECENTS);
2788         Log.d(TAG, "onGestureAnimationEnd - mEnableDrawingLiveTile: " + mEnableDrawingLiveTile);
2789         setRunningTaskHidden(false);
2790         animateUpTaskIconScale();
2791         animateActionsViewIn();
2792 
2793         mCurrentGestureEndTarget = null;
2794     }
2795 
2796     /**
2797      * Returns true if we should add a stub taskView for the running task id
2798      */
shouldAddStubTaskView(Task[] runningTasks)2799     protected boolean shouldAddStubTaskView(Task[] runningTasks) {
2800         int[] runningTaskIds = Arrays.stream(runningTasks).mapToInt(task -> task.key.id).toArray();
2801         TaskView matchingTaskView = null;
2802         if (hasDesktopTask(runningTasks) && runningTaskIds.length == 1) {
2803             // TODO(b/249371338): Unsure if it's expected, desktop runningTasks only have a single
2804             // taskId, therefore we match any DesktopTaskView that contains the runningTaskId.
2805             TaskView taskview = getTaskViewByTaskId(runningTaskIds[0]);
2806             if (taskview instanceof DesktopTaskView) {
2807                 matchingTaskView = taskview;
2808             }
2809         } else {
2810             matchingTaskView = getTaskViewByTaskIds(runningTaskIds);
2811         }
2812         return matchingTaskView == null;
2813     }
2814 
2815     /**
2816      * Creates a task view (if necessary) to represent the task with the {@param runningTaskId}.
2817      *
2818      * All subsequent calls to reload will keep the task as the first item until {@link #reset()}
2819      * is called.  Also scrolls the view to this task.
2820      */
showCurrentTask(Task[] runningTasks)2821     private void showCurrentTask(Task[] runningTasks) {
2822         Log.d(TAG, "showCurrentTask - runningTasks: " + Arrays.toString(runningTasks));
2823         if (runningTasks.length == 0) {
2824             return;
2825         }
2826         int runningTaskViewId = -1;
2827         boolean needGroupTaskView = runningTasks.length > 1;
2828         boolean needDesktopTask = hasDesktopTask(runningTasks);
2829         if (shouldAddStubTaskView(runningTasks)) {
2830             boolean wasEmpty = getChildCount() == 0;
2831             // Add an empty view for now until the task plan is loaded and applied
2832             final TaskView taskView;
2833             if (needDesktopTask) {
2834                 taskView = getTaskViewFromPool(TaskView.Type.DESKTOP);
2835                 mTmpRunningTasks = Arrays.copyOf(runningTasks, runningTasks.length);
2836                 ((DesktopTaskView) taskView).bind(Arrays.asList(mTmpRunningTasks),
2837                         mOrientationState, mTaskOverlayFactory);
2838             } else if (needGroupTaskView) {
2839                 taskView = getTaskViewFromPool(TaskView.Type.GROUPED);
2840                 mTmpRunningTasks = new Task[]{runningTasks[0], runningTasks[1]};
2841                 // When we create a placeholder task view mSplitBoundsConfig will be null, but with
2842                 // the actual app running we won't need to show the thumbnail until all the tasks
2843                 // load later anyways
2844                 ((GroupedTaskView)taskView).bind(mTmpRunningTasks[0], mTmpRunningTasks[1],
2845                         mOrientationState, mTaskOverlayFactory, mSplitBoundsConfig);
2846             } else {
2847                 taskView = getTaskViewFromPool(TaskView.Type.SINGLE);
2848                 // The temporary running task is only used for the duration between the start of the
2849                 // gesture and the task list is loaded and applied
2850                 mTmpRunningTasks = new Task[]{runningTasks[0]};
2851                 taskView.bind(mTmpRunningTasks[0], mOrientationState, mTaskOverlayFactory);
2852             }
2853             addView(taskView, 0);
2854             runningTaskViewId = taskView.getTaskViewId();
2855             if (wasEmpty) {
2856                 addView(mClearAllButton);
2857             }
2858 
2859             // Measure and layout immediately so that the scroll values is updated instantly
2860             // as the user might be quick-switching
2861             measure(makeMeasureSpec(getMeasuredWidth(), EXACTLY),
2862                     makeMeasureSpec(getMeasuredHeight(), EXACTLY));
2863             layout(getLeft(), getTop(), getRight(), getBottom());
2864         } else if (getTaskViewByTaskId(runningTasks[0].key.id) != null) {
2865             runningTaskViewId = getTaskViewByTaskId(runningTasks[0].key.id).getTaskViewId();
2866         }
2867 
2868         boolean runningTaskTileHidden = mRunningTaskTileHidden;
2869         setCurrentTask(runningTaskViewId);
2870         mFocusedTaskViewId = enableGridOnlyOverview() ? INVALID_TASK_ID : runningTaskViewId;
2871         runOnPageScrollsInitialized(() -> setCurrentPage(getRunningTaskIndex()));
2872         setRunningTaskViewShowScreenshot(false);
2873         setRunningTaskHidden(runningTaskTileHidden);
2874         // Update task size after setting current task.
2875         updateTaskSize();
2876         updateChildTaskOrientations();
2877 
2878         // Reload the task list
2879         reloadIfNeeded();
2880     }
2881 
hasDesktopTask(Task[] runningTasks)2882     private boolean hasDesktopTask(Task[] runningTasks) {
2883         if (!DesktopModeStatus.canEnterDesktopMode(mContext)) {
2884             return false;
2885         }
2886         for (Task task : runningTasks) {
2887             if (task.key.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM) {
2888                 return true;
2889             }
2890         }
2891         return false;
2892     }
2893 
2894     /**
2895      * Sets the running task id, cleaning up the old running task if necessary.
2896      */
setCurrentTask(int runningTaskViewId)2897     public void setCurrentTask(int runningTaskViewId) {
2898         if (mRunningTaskViewId == runningTaskViewId) {
2899             return;
2900         }
2901 
2902         if (mRunningTaskViewId != -1) {
2903             // Reset the state on the old running task view
2904             setTaskIconScaledDown(false);
2905             setRunningTaskViewShowScreenshot(true);
2906             setRunningTaskHidden(false);
2907         }
2908         setRunningTaskViewId(runningTaskViewId);
2909     }
2910 
setRunningTaskViewId(int runningTaskViewId)2911     private void setRunningTaskViewId(int runningTaskViewId) {
2912         int prevRunningTaskViewId = mRunningTaskViewId;
2913         mRunningTaskViewId = runningTaskViewId;
2914 
2915         if (Flags.enableRefactorTaskThumbnail()) {
2916             TaskView previousRunningTaskView = getTaskViewFromTaskViewId(prevRunningTaskViewId);
2917             if (previousRunningTaskView != null) {
2918                 previousRunningTaskView.notifyIsRunningTaskUpdated();
2919             }
2920             TaskView newRunningTaskView = getTaskViewFromTaskViewId(runningTaskViewId);
2921             if (newRunningTaskView != null) {
2922                 newRunningTaskView.notifyIsRunningTaskUpdated();
2923             }
2924         }
2925     }
2926 
getTaskViewIdFromTaskId(int taskId)2927     private int getTaskViewIdFromTaskId(int taskId) {
2928         TaskView taskView = getTaskViewByTaskId(taskId);
2929         return taskView != null ? taskView.getTaskViewId() : -1;
2930     }
2931 
2932     /**
2933      * Hides the tile associated with {@link #mRunningTaskViewId}
2934      */
setRunningTaskHidden(boolean isHidden)2935     public void setRunningTaskHidden(boolean isHidden) {
2936         mRunningTaskTileHidden = isHidden;
2937         TaskView runningTask = getRunningTaskView();
2938         if (runningTask != null) {
2939             runningTask.setStableAlpha(isHidden ? 0 : mContentAlpha);
2940             if (!isHidden) {
2941                 AccessibilityManagerCompat.sendCustomAccessibilityEvent(runningTask,
2942                         AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
2943             }
2944         }
2945     }
2946 
setRunningTaskViewShowScreenshot(boolean showScreenshot)2947     private void setRunningTaskViewShowScreenshot(boolean showScreenshot) {
2948         mRunningTaskShowScreenshot = showScreenshot;
2949         TaskView runningTaskView = getRunningTaskView();
2950         if (runningTaskView != null) {
2951             runningTaskView.setShouldShowScreenshot(mRunningTaskShowScreenshot);
2952         }
2953     }
2954 
setTaskIconScaledDown(boolean isScaledDown)2955     public void setTaskIconScaledDown(boolean isScaledDown) {
2956         if (mTaskIconScaledDown != isScaledDown) {
2957             mTaskIconScaledDown = isScaledDown;
2958             int taskCount = getTaskViewCount();
2959             for (int i = 0; i < taskCount; i++) {
2960                 requireTaskViewAt(i).setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
2961             }
2962         }
2963     }
2964 
animateActionsViewIn()2965     private void animateActionsViewIn() {
2966         if (!showAsGrid() || isFocusedTaskInExpectedScrollPosition()) {
2967             animateActionsViewAlpha(1, TaskView.SCALE_ICON_DURATION);
2968         }
2969     }
2970 
animateUpTaskIconScale()2971     public void animateUpTaskIconScale() {
2972         mTaskIconScaledDown = false;
2973         int taskCount = getTaskViewCount();
2974         for (int i = 0; i < taskCount; i++) {
2975             TaskView taskView = requireTaskViewAt(i);
2976             taskView.animateIconScaleAndDimIntoView();
2977         }
2978     }
2979 
2980     /**
2981      * Updates TaskView and ClearAllButtion scaling and translation required to turn into grid
2982      * layout.
2983      * This method is used when no task dismissal has occurred.
2984      */
updateGridProperties()2985     private void updateGridProperties() {
2986         updateGridProperties(false, Integer.MAX_VALUE);
2987     }
2988 
2989     /**
2990      * Updates TaskView and ClearAllButtion scaling and translation required to turn into grid
2991      * layout.
2992      *
2993      * This method is used when task dismissal has occurred, but rebalance is not needed.
2994      *
2995      * @param isTaskDismissal indicates if update was called due to task dismissal
2996      */
updateGridProperties(boolean isTaskDismissal)2997     private void updateGridProperties(boolean isTaskDismissal) {
2998         updateGridProperties(isTaskDismissal, Integer.MAX_VALUE);
2999     }
3000 
3001     /**
3002      * Updates TaskView and ClearAllButton scaling and translation required to turn into grid
3003      * layout.
3004      *
3005      * This method only calculates the potential position and depends on {@link #setGridProgress} to
3006      * apply the actual scaling and translation.
3007      *
3008      * @param isTaskDismissal    indicates if update was called due to task dismissal
3009      * @param startRebalanceAfter which view index to start rebalancing from. Use Integer.MAX_VALUE
3010      *                           to skip rebalance
3011      */
updateGridProperties(boolean isTaskDismissal, int startRebalanceAfter)3012     private void updateGridProperties(boolean isTaskDismissal, int startRebalanceAfter) {
3013         int taskCount = getTaskViewCount();
3014         if (taskCount == 0) {
3015             return;
3016         }
3017 
3018         DeviceProfile deviceProfile = mContainer.getDeviceProfile();
3019         int taskTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx;
3020 
3021         int topRowWidth = 0;
3022         int bottomRowWidth = 0;
3023         float topAccumulatedTranslationX = 0;
3024         float bottomAccumulatedTranslationX = 0;
3025 
3026         // Contains whether the child index is in top or bottom of grid (for non-focused task)
3027         // Different from mTopRowIdSet, which contains the taskViewId of what task is in top row
3028         IntSet topSet = new IntSet();
3029         IntSet bottomSet = new IntSet();
3030 
3031         // Horizontal grid translation for each task
3032         float[] gridTranslations = new float[taskCount];
3033 
3034         int focusedTaskIndex = Integer.MAX_VALUE;
3035         int focusedTaskShift = 0;
3036         int focusedTaskWidthAndSpacing = 0;
3037         int snappedTaskRowWidth = 0;
3038         int snappedPage = isKeyboardTaskFocusPending() ? mKeyboardTaskFocusIndex : getNextPage();
3039         TaskView snappedTaskView = getTaskViewAt(snappedPage);
3040         TaskView homeTaskView = getHomeTaskView();
3041         TaskView nextFocusedTaskView = null;
3042 
3043         if (!isTaskDismissal) {
3044             mTopRowIdSet.clear();
3045         }
3046         for (int i = 0; i < taskCount; i++) {
3047             TaskView taskView = requireTaskViewAt(i);
3048             int taskWidthAndSpacing = taskView.getLayoutParams().width + mPageSpacing;
3049             // Evenly distribute tasks between rows unless rearranging due to task dismissal, in
3050             // which case keep tasks in their respective rows. For the running task, don't join
3051             // the grid.
3052             if (taskView.isFocusedTask()) {
3053                 topRowWidth += taskWidthAndSpacing;
3054                 bottomRowWidth += taskWidthAndSpacing;
3055 
3056                 focusedTaskIndex = i;
3057                 focusedTaskWidthAndSpacing = taskWidthAndSpacing;
3058                 gridTranslations[i] += focusedTaskShift;
3059                 gridTranslations[i] += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
3060 
3061                 // Center view vertically in case it's from different orientation.
3062                 taskView.setGridTranslationY((mLastComputedTaskSize.height() + taskTopMargin
3063                         - taskView.getLayoutParams().height) / 2f);
3064 
3065                 if (taskView == snappedTaskView) {
3066                     // If focused task is snapped, the row width is just task width and spacing.
3067                     snappedTaskRowWidth = taskWidthAndSpacing;
3068                 }
3069             } else {
3070                 if (i > focusedTaskIndex) {
3071                     // For tasks after the focused task, shift by focused task's width and spacing.
3072                     gridTranslations[i] +=
3073                             mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing;
3074                 } else {
3075                     // For task before the focused task, accumulate the width and spacing to
3076                     // calculate the distance focused task need to shift.
3077                     focusedTaskShift += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
3078                 }
3079                 int taskViewId = taskView.getTaskViewId();
3080 
3081                 // Rebalance the grid starting after a certain index
3082                 boolean isTopRow;
3083                 if (isTaskDismissal) {
3084                     if (i > startRebalanceAfter) {
3085                         mTopRowIdSet.remove(taskViewId);
3086                         isTopRow = topRowWidth <= bottomRowWidth;
3087                     } else {
3088                         isTopRow = mTopRowIdSet.contains(taskViewId);
3089                     }
3090                 } else {
3091                     isTopRow = topRowWidth <= bottomRowWidth;
3092                 }
3093 
3094                 if (isTopRow) {
3095                     if (homeTaskView != null && nextFocusedTaskView == null) {
3096                         // TaskView will be focused when swipe up, don't count towards row width.
3097                         nextFocusedTaskView = taskView;
3098                     } else {
3099                         topRowWidth += taskWidthAndSpacing;
3100                     }
3101                     topSet.add(i);
3102                     mTopRowIdSet.add(taskViewId);
3103 
3104                     taskView.setGridTranslationY(mTaskGridVerticalDiff);
3105 
3106                     // Move horizontally into empty space.
3107                     float widthOffset = 0;
3108                     for (int j = i - 1; !topSet.contains(j) && j >= 0; j--) {
3109                         if (j == focusedTaskIndex) {
3110                             continue;
3111                         }
3112                         widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing;
3113                     }
3114 
3115                     float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset;
3116                     gridTranslations[i] += topAccumulatedTranslationX + currentTaskTranslationX;
3117                     topAccumulatedTranslationX += currentTaskTranslationX;
3118                 } else {
3119                     bottomRowWidth += taskWidthAndSpacing;
3120                     bottomSet.add(i);
3121 
3122                     // Move into bottom row.
3123                     taskView.setGridTranslationY(mTopBottomRowHeightDiff + mTaskGridVerticalDiff);
3124 
3125                     // Move horizontally into empty space.
3126                     float widthOffset = 0;
3127                     for (int j = i - 1; !bottomSet.contains(j) && j >= 0; j--) {
3128                         if (j == focusedTaskIndex) {
3129                             continue;
3130                         }
3131                         widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing;
3132                     }
3133 
3134                     float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset;
3135                     gridTranslations[i] += bottomAccumulatedTranslationX + currentTaskTranslationX;
3136                     bottomAccumulatedTranslationX += currentTaskTranslationX;
3137                 }
3138                 if (taskView == snappedTaskView) {
3139                     snappedTaskRowWidth = isTopRow ? topRowWidth : bottomRowWidth;
3140                 }
3141             }
3142         }
3143 
3144         // We need to maintain snapped task's page scroll invariant between quick switch and
3145         // overview, so we sure snapped task's grid translation is 0, and add a non-fullscreen
3146         // translationX that is the same as snapped task's full scroll adjustment.
3147         float snappedTaskNonGridScrollAdjustment = 0;
3148         float snappedTaskGridTranslationX = 0;
3149         if (snappedTaskView != null) {
3150             snappedTaskNonGridScrollAdjustment = snappedTaskView.getScrollAdjustment(
3151                     /*gridEnabled=*/false);
3152             snappedTaskGridTranslationX = gridTranslations[snappedPage];
3153         }
3154 
3155         // Use the accumulated translation of the row containing the last task.
3156         float clearAllAccumulatedTranslation = topSet.contains(taskCount - 1)
3157                 ? topAccumulatedTranslationX : bottomAccumulatedTranslationX;
3158 
3159         // If the last task is on the shorter row, ClearAllButton will embed into the shorter row
3160         // which is not what we want. Compensate the width difference of the 2 rows in that case.
3161         float shorterRowCompensation = 0;
3162         if (topRowWidth <= bottomRowWidth) {
3163             if (topSet.contains(taskCount - 1)) {
3164                 shorterRowCompensation = bottomRowWidth - topRowWidth;
3165             }
3166         } else {
3167             if (bottomSet.contains(taskCount - 1)) {
3168                 shorterRowCompensation = topRowWidth - bottomRowWidth;
3169             }
3170         }
3171         float clearAllShorterRowCompensation =
3172                 mIsRtl ? -shorterRowCompensation : shorterRowCompensation;
3173 
3174         // If the total width is shorter than one grid's width, move ClearAllButton further away
3175         // accordingly. Update longRowWidth if ClearAllButton has been moved.
3176         float clearAllShortTotalWidthTranslation = 0;
3177         int longRowWidth = Math.max(topRowWidth, bottomRowWidth);
3178         if (longRowWidth < mLastComputedGridSize.width()) {
3179             mClearAllShortTotalWidthTranslation =
3180                     (mIsRtl
3181                             ? mLastComputedTaskSize.right
3182                             : deviceProfile.widthPx - mLastComputedTaskSize.left)
3183                     - longRowWidth - deviceProfile.overviewGridSideMargin;
3184             clearAllShortTotalWidthTranslation = mIsRtl
3185                     ? -mClearAllShortTotalWidthTranslation : mClearAllShortTotalWidthTranslation;
3186             if (snappedTaskRowWidth == longRowWidth) {
3187                 // Updated snappedTaskRowWidth as well if it's same as longRowWidth.
3188                 snappedTaskRowWidth += mClearAllShortTotalWidthTranslation;
3189             }
3190             longRowWidth += mClearAllShortTotalWidthTranslation;
3191         } else {
3192             mClearAllShortTotalWidthTranslation = 0;
3193         }
3194 
3195         float clearAllTotalTranslationX =
3196                 clearAllAccumulatedTranslation + clearAllShorterRowCompensation
3197                         + clearAllShortTotalWidthTranslation + snappedTaskNonGridScrollAdjustment;
3198         if (focusedTaskIndex < taskCount) {
3199             // Shift by focused task's width and spacing if a task is focused.
3200             clearAllTotalTranslationX +=
3201                     mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing;
3202         }
3203 
3204         // Make sure there are enough space between snapped page and ClearAllButton, for the case
3205         // of swiping up after quick switch.
3206         if (snappedTaskView != null) {
3207             int distanceFromClearAll = longRowWidth - snappedTaskRowWidth;
3208             // ClearAllButton should be off screen when snapped task is in its snapped position.
3209             int minimumDistance =
3210                     (mIsRtl
3211                             ? mLastComputedTaskSize.left
3212                             : deviceProfile.widthPx - mLastComputedTaskSize.right)
3213                     - deviceProfile.overviewGridSideMargin - mPageSpacing
3214                     + (mTaskWidth - snappedTaskView.getLayoutParams().width)
3215                     - mClearAllShortTotalWidthTranslation;
3216             if (distanceFromClearAll < minimumDistance) {
3217                 int distanceDifference = minimumDistance - distanceFromClearAll;
3218                 snappedTaskGridTranslationX += mIsRtl ? distanceDifference : -distanceDifference;
3219             }
3220         }
3221 
3222         for (int i = 0; i < taskCount; i++) {
3223             TaskView taskView = requireTaskViewAt(i);
3224             taskView.setGridTranslationX(gridTranslations[i] - snappedTaskGridTranslationX
3225                     + snappedTaskNonGridScrollAdjustment);
3226         }
3227 
3228         final TaskView runningTask = getRunningTaskView();
3229         if (showAsGrid() && enableGridOnlyOverview() && runningTask != null) {
3230             runActionOnRemoteHandles(
3231                     remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
3232                             .taskSecondaryTranslation.value = runningTask.getGridTranslationY()
3233             );
3234         }
3235 
3236         mClearAllButton.setGridTranslationPrimary(
3237                 clearAllTotalTranslationX - snappedTaskGridTranslationX);
3238         mClearAllButton.setGridScrollOffset(
3239                 mIsRtl ? mLastComputedTaskSize.left - mLastComputedGridSize.left
3240                         : mLastComputedTaskSize.right - mLastComputedGridSize.right);
3241 
3242         setGridProgress(mGridProgress);
3243     }
3244 
isSameGridRow(TaskView taskView1, TaskView taskView2)3245     private boolean isSameGridRow(TaskView taskView1, TaskView taskView2) {
3246         if (taskView1 == null || taskView2 == null) {
3247             return false;
3248         }
3249         int taskViewId1 = taskView1.getTaskViewId();
3250         int taskViewId2 = taskView2.getTaskViewId();
3251         if (taskViewId1 == mFocusedTaskViewId || taskViewId2 == mFocusedTaskViewId) {
3252             return false;
3253         }
3254         return (mTopRowIdSet.contains(taskViewId1) && mTopRowIdSet.contains(taskViewId2)) || (
3255                 !mTopRowIdSet.contains(taskViewId1) && !mTopRowIdSet.contains(taskViewId2));
3256     }
3257 
3258     /**
3259      * Moves TaskView and ClearAllButton between carousel and 2 row grid.
3260      *
3261      * @param gridProgress 0 = carousel; 1 = 2 row grid.
3262      */
setGridProgress(float gridProgress)3263     private void setGridProgress(float gridProgress) {
3264         mGridProgress = gridProgress;
3265 
3266         int taskCount = getTaskViewCount();
3267         for (int i = 0; i < taskCount; i++) {
3268             requireTaskViewAt(i).setGridProgress(gridProgress);
3269         }
3270         mClearAllButton.setGridProgress(gridProgress);
3271     }
3272 
setTaskThumbnailSplashAlpha(float taskThumbnailSplashAlpha)3273     private void setTaskThumbnailSplashAlpha(float taskThumbnailSplashAlpha) {
3274         int taskCount = getTaskViewCount();
3275         if (taskCount == 0) {
3276             return;
3277         }
3278 
3279         mTaskThumbnailSplashAlpha = taskThumbnailSplashAlpha;
3280         for (int i = 0; i < taskCount; i++) {
3281             requireTaskViewAt(i).setTaskThumbnailSplashAlpha(taskThumbnailSplashAlpha);
3282         }
3283     }
3284 
enableLayoutTransitions()3285     private void enableLayoutTransitions() {
3286         if (mLayoutTransition == null) {
3287             mLayoutTransition = new LayoutTransition();
3288             mLayoutTransition.enableTransitionType(LayoutTransition.APPEARING);
3289             mLayoutTransition.setDuration(ADDITION_TASK_DURATION);
3290             mLayoutTransition.setStartDelay(LayoutTransition.APPEARING, 0);
3291 
3292             mLayoutTransition.addTransitionListener(new TransitionListener() {
3293                 @Override
3294                 public void startTransition(LayoutTransition transition, ViewGroup viewGroup,
3295                     View view, int i) {
3296                 }
3297 
3298                 @Override
3299                 public void endTransition(LayoutTransition transition, ViewGroup viewGroup,
3300                     View view, int i) {
3301                     // When the unpinned task is added, snap to first page and disable transitions
3302                     if (view instanceof TaskView) {
3303                         snapToPage(0);
3304                         setLayoutTransition(null);
3305                     }
3306 
3307                 }
3308             });
3309         }
3310         setLayoutTransition(mLayoutTransition);
3311     }
3312 
setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp)3313     public void setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp) {
3314         mSwipeDownShouldLaunchApp = swipeDownShouldLaunchApp;
3315     }
3316 
shouldSwipeDownLaunchApp()3317     public boolean shouldSwipeDownLaunchApp() {
3318         return mSwipeDownShouldLaunchApp;
3319     }
3320 
setIgnoreResetTask(int taskId)3321     public void setIgnoreResetTask(int taskId) {
3322         mIgnoreResetTaskId = taskId;
3323     }
3324 
clearIgnoreResetTask(int taskId)3325     public void clearIgnoreResetTask(int taskId) {
3326         if (mIgnoreResetTaskId == taskId) {
3327             mIgnoreResetTaskId = -1;
3328         }
3329     }
3330 
addDismissedTaskAnimations(TaskView taskView, long duration, PendingAnimation anim)3331     private void addDismissedTaskAnimations(TaskView taskView, long duration,
3332             PendingAnimation anim) {
3333         // Use setFloat instead of setViewAlpha as we want to keep the view visible even when it's
3334         // alpha is set to 0 so that it can be recycled in the view pool properly
3335         anim.setFloat(taskView, VIEW_ALPHA, 0,
3336                 clampToProgress(isOnGridBottomRow(taskView) ? ACCELERATE : FINAL_FRAME, 0, 0.5f));
3337         FloatProperty<TaskView> secondaryViewTranslate =
3338                 taskView.getSecondaryDismissTranslationProperty();
3339         int secondaryTaskDimension = getPagedOrientationHandler().getSecondaryDimension(taskView);
3340         int verticalFactor = getPagedOrientationHandler().getSecondaryTranslationDirectionFactor();
3341 
3342         ResourceProvider rp = DynamicResource.provider(mContainer);
3343         SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_START)
3344                 .setDampingRatio(rp.getFloat(R.dimen.dismiss_task_trans_y_damping_ratio))
3345                 .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_y_stiffness));
3346 
3347         anim.add(ObjectAnimator.ofFloat(taskView, secondaryViewTranslate,
3348                 verticalFactor * secondaryTaskDimension * 2).setDuration(duration), LINEAR, sp);
3349 
3350         if (taskView.isRunningTask()) {
3351             anim.addOnFrameCallback(() -> {
3352                 if (!mEnableDrawingLiveTile) return;
3353                 runActionOnRemoteHandles(
3354                         remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
3355                                 .taskSecondaryTranslation.value = getPagedOrientationHandler()
3356                                 .getSecondaryValue(taskView.getTranslationX(),
3357                                         taskView.getTranslationY()
3358                                 ));
3359                 redrawLiveTile();
3360             });
3361         }
3362     }
3363 
3364     /**
3365      * Places an {@link FloatingTaskView} on top of the thumbnail for {@link #mSplitHiddenTaskView}
3366      * and then animates it into the split position that was desired
3367      */
createInitialSplitSelectAnimation(PendingAnimation anim)3368     private void createInitialSplitSelectAnimation(PendingAnimation anim) {
3369         getPagedOrientationHandler().getInitialSplitPlaceholderBounds(mSplitPlaceholderSize,
3370                 mSplitPlaceholderInset, mContainer.getDeviceProfile(),
3371                 mSplitSelectStateController.getActiveSplitStagePosition(), mTempRect);
3372         SplitAnimationTimings timings =
3373                 AnimUtils.getDeviceOverviewToSplitTimings(mContainer.getDeviceProfile().isTablet);
3374 
3375         RectF startingTaskRect = new RectF();
3376         safeRemoveDragLayerView(mSplitSelectStateController.getFirstFloatingTaskView());
3377         SplitAnimInitProps splitAnimInitProps =
3378                 mSplitSelectStateController.getSplitAnimationController().getFirstAnimInitViews(
3379                         () -> mSplitHiddenTaskView, () -> mSplitSelectSource);
3380         if (mSplitSelectStateController.isAnimateCurrentTaskDismissal()) {
3381             // Create the split select animation from Overview
3382             mSplitHiddenTaskView.setThumbnailVisibility(INVISIBLE,
3383                     mSplitSelectStateController.getInitialTaskId());
3384             anim.setViewAlpha(splitAnimInitProps.getIconView(), 0, clampToProgress(LINEAR,
3385                     timings.getIconFadeStartOffset(),
3386                     timings.getIconFadeEndOffset()));
3387         }
3388 
3389         FloatingTaskView firstFloatingTaskView = FloatingTaskView.getFloatingTaskView(mContainer,
3390                 splitAnimInitProps.getOriginalView(),
3391                 splitAnimInitProps.getOriginalBitmap(),
3392                 splitAnimInitProps.getIconDrawable(), startingTaskRect);
3393         firstFloatingTaskView.setAlpha(1);
3394         firstFloatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect,
3395                 splitAnimInitProps.getFadeWithThumbnail(), splitAnimInitProps.isStagedTask());
3396         mSplitSelectStateController.setFirstFloatingTaskView(firstFloatingTaskView);
3397 
3398         // Allow user to click staged app to launch into fullscreen
3399         firstFloatingTaskView.setOnClickListener(view ->
3400                 mSplitSelectStateController.getSplitAnimationController().
3401                         playAnimPlaceholderToFullscreen(mContainer, view,
3402                                 Optional.of(() -> resetFromSplitSelectionState())));
3403 
3404         // SplitInstructionsView: animate in
3405         safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView());
3406         SplitInstructionsView splitInstructionsView =
3407                 SplitInstructionsView.getSplitInstructionsView(mContainer);
3408         splitInstructionsView.setAlpha(0);
3409         anim.setViewAlpha(splitInstructionsView, 1, clampToProgress(LINEAR,
3410                 timings.getInstructionsContainerFadeInStartOffset(),
3411                 timings.getInstructionsContainerFadeInEndOffset()));
3412         anim.addFloat(splitInstructionsView, splitInstructionsView.UNFOLD, 0.1f, 1,
3413                 clampToProgress(EMPHASIZED_DECELERATE,
3414                         timings.getInstructionsUnfoldStartOffset(),
3415                         timings.getInstructionsUnfoldEndOffset()));
3416         mSplitSelectStateController.setSplitInstructionsView(splitInstructionsView);
3417 
3418         InteractionJankMonitorWrapper.begin(this, Cuj.CUJ_SPLIT_SCREEN_ENTER,
3419                 "First tile selected");
3420         anim.addListener(new AnimatorListenerAdapter() {
3421             @Override
3422             public void onAnimationStart(Animator animation) {
3423                 if (mSplitHiddenTaskView == getRunningTaskView()) {
3424                     finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
3425                             null /* onFinishComplete */);
3426                 } else {
3427                     switchToScreenshot(
3428                             () -> finishRecentsAnimation(true /* toRecents */,
3429                                     false /* shouldPip */, null /* onFinishComplete */));
3430                 }
3431             }
3432         });
3433         anim.addEndListener(success -> {
3434             if (success) {
3435                 InteractionJankMonitorWrapper.end(Cuj.CUJ_SPLIT_SCREEN_ENTER);
3436             } else {
3437                 // If transition to split select was interrupted, clean up to prevent glitches
3438                 if (FeatureFlags.enableSplitContextually()) {
3439                     mSplitSelectStateController.resetState();
3440                 } else {
3441                     resetFromSplitSelectionState();
3442                 }
3443                 InteractionJankMonitorWrapper.cancel(Cuj.CUJ_SPLIT_SCREEN_ENTER);
3444             }
3445 
3446             updateCurrentTaskActionsVisibility();
3447         });
3448     }
3449 
3450     /**
3451      * Creates a {@link PendingAnimation} for dismissing the specified {@link TaskView}.
3452      * @param dismissedTaskView the {@link TaskView} to be dismissed
3453      * @param animateTaskView whether the {@link TaskView} to be dismissed should be animated
3454      * @param shouldRemoveTask whether the associated {@link Task} should be removed from
3455      *                         ActivityManager after dismissal
3456      * @param duration duration of the animation
3457      * @param dismissingForSplitSelection task dismiss animation is used for entering split
3458      *                                    selection state from app icon
3459      */
createTaskDismissAnimation(PendingAnimation anim, TaskView dismissedTaskView, boolean animateTaskView, boolean shouldRemoveTask, long duration, boolean dismissingForSplitSelection)3460     public void createTaskDismissAnimation(PendingAnimation anim, TaskView dismissedTaskView,
3461             boolean animateTaskView, boolean shouldRemoveTask, long duration,
3462             boolean dismissingForSplitSelection) {
3463         if (mPendingAnimation != null) {
3464             mPendingAnimation.createPlaybackController().dispatchOnCancel().dispatchOnEnd();
3465         }
3466 
3467         int count = getPageCount();
3468         if (count == 0) {
3469             return;
3470         }
3471 
3472         boolean showAsGrid = showAsGrid();
3473         int taskCount = getTaskViewCount();
3474         int dismissedIndex = indexOfChild(dismissedTaskView);
3475         int dismissedTaskViewId = dismissedTaskView.getTaskViewId();
3476 
3477         // Grid specific properties.
3478         boolean isFocusedTaskDismissed = false;
3479         boolean isStagingFocusedTask = false;
3480         TaskView nextFocusedTaskView = null;
3481         boolean nextFocusedTaskFromTop = false;
3482         float dismissedTaskWidth = 0;
3483         float nextFocusedTaskWidth = 0;
3484 
3485         // Non-grid specific properties.
3486         int[] oldScroll = new int[count];
3487         int[] newScroll = new int[count];
3488         int scrollDiffPerPage = 0;
3489         boolean needsCurveUpdates = false;
3490 
3491         if (showAsGrid) {
3492             dismissedTaskWidth = dismissedTaskView.getLayoutParams().width + mPageSpacing;
3493             isFocusedTaskDismissed = dismissedTaskViewId == mFocusedTaskViewId;
3494             if (isFocusedTaskDismissed) {
3495                 if (isSplitSelectionActive()) {
3496                     isStagingFocusedTask = true;
3497                 } else {
3498                     nextFocusedTaskFromTop =
3499                             mTopRowIdSet.size() > 0 && mTopRowIdSet.size() >= (taskCount - 1) / 2f;
3500                     // Pick the next focused task from the preferred row.
3501                     for (int i = 0; i < taskCount; i++) {
3502                         TaskView taskView = requireTaskViewAt(i);
3503                         if (taskView == dismissedTaskView) {
3504                             continue;
3505                         }
3506                         boolean isTopRow = mTopRowIdSet.contains(taskView.getTaskViewId());
3507                         if ((nextFocusedTaskFromTop && isTopRow
3508                                 || (!nextFocusedTaskFromTop && !isTopRow))) {
3509                             nextFocusedTaskView = taskView;
3510                             break;
3511                         }
3512                     }
3513                     if (nextFocusedTaskView != null) {
3514                         nextFocusedTaskWidth =
3515                                 nextFocusedTaskView.getLayoutParams().width + mPageSpacing;
3516                     }
3517                 }
3518             }
3519         } else {
3520             getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
3521             getPageScrolls(newScroll, false,
3522                     v -> v.getVisibility() != GONE && v != dismissedTaskView);
3523             if (count > 1) {
3524                 scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]);
3525             }
3526         }
3527 
3528         float dismissTranslationInterpolationEnd = 1;
3529         boolean closeGapBetweenClearAll = false;
3530         boolean isClearAllHidden = isClearAllHidden();
3531         boolean snapToLastTask = false;
3532         boolean isLeftRightSplit =
3533                 mContainer.getDeviceProfile().isLeftRightSplit && isSplitSelectionActive();
3534         TaskView lastGridTaskView = showAsGrid ? getLastGridTaskView() : null;
3535         int currentPageScroll = getScrollForPage(mCurrentPage);
3536         int lastGridTaskScroll = getScrollForPage(indexOfChild(lastGridTaskView));
3537         boolean currentPageSnapsToEndOfGrid = currentPageScroll == lastGridTaskScroll;
3538         if (lastGridTaskView != null && lastGridTaskView.isVisibleToUser()) {
3539             // After dismissal, animate translation of the remaining tasks to fill any gap left
3540             // between the end of the grid and the clear all button. Only animate if the clear
3541             // all button is visible or would become visible after dismissal.
3542             float longGridRowWidthDiff = 0;
3543 
3544             int topGridRowSize = mTopRowIdSet.size();
3545             int bottomGridRowSize = taskCount - mTopRowIdSet.size()
3546                     - (enableGridOnlyOverview() ? 0 : 1);
3547             boolean topRowLonger = topGridRowSize > bottomGridRowSize;
3548             boolean bottomRowLonger = bottomGridRowSize > topGridRowSize;
3549             boolean dismissedTaskFromTop = mTopRowIdSet.contains(dismissedTaskViewId);
3550             boolean dismissedTaskFromBottom = !dismissedTaskFromTop && !isFocusedTaskDismissed;
3551             if (dismissedTaskFromTop || (isFocusedTaskDismissed && nextFocusedTaskFromTop)) {
3552                 topGridRowSize--;
3553             }
3554             if (dismissedTaskFromBottom || (isFocusedTaskDismissed && !nextFocusedTaskFromTop)) {
3555                 bottomGridRowSize--;
3556             }
3557             int longRowWidth = Math.max(topGridRowSize, bottomGridRowSize)
3558                     * (mLastComputedGridTaskSize.width() + mPageSpacing);
3559             if (!enableGridOnlyOverview() && !isStagingFocusedTask) {
3560                 longRowWidth += mLastComputedTaskSize.width() + mPageSpacing;
3561             }
3562 
3563             float gapWidth = 0;
3564             if ((topRowLonger && dismissedTaskFromTop)
3565                     || (bottomRowLonger && dismissedTaskFromBottom)) {
3566                 gapWidth = dismissedTaskWidth;
3567             } else if (nextFocusedTaskView != null
3568                     && ((topRowLonger && nextFocusedTaskFromTop)
3569                     || (bottomRowLonger && !nextFocusedTaskFromTop))) {
3570                 gapWidth = nextFocusedTaskWidth;
3571             }
3572             if (gapWidth > 0) {
3573                 if (mClearAllShortTotalWidthTranslation == 0) {
3574                     // Compensate the removed gap if we don't already have shortTotalCompensation,
3575                     // and adjust accordingly to the new shortTotalCompensation after dismiss.
3576                     int newClearAllShortTotalWidthTranslation = 0;
3577                     if (longRowWidth < mLastComputedGridSize.width()) {
3578                         DeviceProfile deviceProfile = mContainer.getDeviceProfile();
3579                         newClearAllShortTotalWidthTranslation =
3580                                 (mIsRtl
3581                                         ? mLastComputedTaskSize.right
3582                                         : deviceProfile.widthPx - mLastComputedTaskSize.left)
3583                                         - longRowWidth - deviceProfile.overviewGridSideMargin;
3584                     }
3585                     float gapCompensation = gapWidth - newClearAllShortTotalWidthTranslation;
3586                     longGridRowWidthDiff += mIsRtl ? -gapCompensation : gapCompensation;
3587                 }
3588                 if (isClearAllHidden) {
3589                     // If ClearAllButton isn't fully shown, snap to the last task.
3590                     snapToLastTask = true;
3591                 }
3592             }
3593             if (isLeftRightSplit && !isStagingFocusedTask) {
3594                 // LastTask's scroll is the minimum scroll in split select, if current scroll is
3595                 // beyond that, we'll need to snap to last task instead.
3596                 TaskView lastTask = getLastGridTaskView();
3597                 if (lastTask != null) {
3598                     int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this);
3599                     int lastTaskScroll = getScrollForPage(indexOfChild(lastTask));
3600                     if ((mIsRtl && primaryScroll < lastTaskScroll)
3601                             || (!mIsRtl && primaryScroll > lastTaskScroll)) {
3602                         snapToLastTask = true;
3603                     }
3604                 }
3605             }
3606             if (snapToLastTask) {
3607                 longGridRowWidthDiff += getSnapToLastTaskScrollDiff();
3608             } else if (isLeftRightSplit && currentPageSnapsToEndOfGrid) {
3609                 // Use last task as reference point for scroll diff and snapping calculation as it's
3610                 // the only invariant point in landscape split screen.
3611                 snapToLastTask = true;
3612             }
3613 
3614             // If we need to animate the grid to compensate the clear all gap, we split the second
3615             // half of the dismiss pending animation (in which the non-dismissed tasks slide into
3616             // place) in half again, making the first quarter the existing non-dismissal sliding
3617             // and the second quarter this new animation of gap filling. This is due to the fact
3618             // that PendingAnimation is a single animation, not a sequence of animations, so we
3619             // fake it using interpolation.
3620             if (longGridRowWidthDiff != 0) {
3621                 closeGapBetweenClearAll = true;
3622                 // Stagger the offsets of each additional task for a delayed animation. We use
3623                 // half here as this animation is half of half of an animation (1/4th).
3624                 float halfAdditionalDismissTranslationOffset =
3625                         (0.5f * ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET);
3626                 dismissTranslationInterpolationEnd = Utilities.boundToRange(
3627                         END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
3628                                 + (taskCount - 1) * halfAdditionalDismissTranslationOffset,
3629                         END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET, 1);
3630                 for (int i = 0; i < taskCount; i++) {
3631                     TaskView taskView = requireTaskViewAt(i);
3632                     anim.setFloat(taskView, TaskView.GRID_END_TRANSLATION_X, longGridRowWidthDiff,
3633                             clampToProgress(LINEAR, dismissTranslationInterpolationEnd, 1));
3634                     dismissTranslationInterpolationEnd = Utilities.boundToRange(
3635                             dismissTranslationInterpolationEnd
3636                                     - halfAdditionalDismissTranslationOffset,
3637                             END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET, 1);
3638                     if (mEnableDrawingLiveTile && taskView.isRunningTask()) {
3639                         anim.addOnFrameCallback(() -> {
3640                             runActionOnRemoteHandles(
3641                                     remoteTargetHandle ->
3642                                             remoteTargetHandle.getTaskViewSimulator()
3643                                                     .taskPrimaryTranslation.value =
3644                                                     TaskView.GRID_END_TRANSLATION_X.get(taskView));
3645                             redrawLiveTile();
3646                         });
3647                     }
3648                 }
3649 
3650                 // Change alpha of clear all if translating grid to hide it
3651                 if (isClearAllHidden) {
3652                     anim.setFloat(mClearAllButton, DISMISS_ALPHA, 0, LINEAR);
3653                     anim.addListener(new AnimatorListenerAdapter() {
3654                         @Override
3655                         public void onAnimationEnd(Animator animation) {
3656                             super.onAnimationEnd(animation);
3657                             mClearAllButton.setDismissAlpha(1);
3658                         }
3659                     });
3660                 }
3661             }
3662         }
3663 
3664         SplitAnimationTimings splitTimings =
3665                 AnimUtils.getDeviceOverviewToSplitTimings(mContainer.getDeviceProfile().isTablet);
3666 
3667         int distanceFromDismissedTask = 0;
3668         for (int i = 0; i < count; i++) {
3669             View child = getChildAt(i);
3670             if (child == dismissedTaskView) {
3671                 if (animateTaskView) {
3672                     if (dismissingForSplitSelection) {
3673                         createInitialSplitSelectAnimation(anim);
3674                     } else {
3675                         addDismissedTaskAnimations(dismissedTaskView, duration, anim);
3676                     }
3677                 }
3678             } else if (!showAsGrid) {
3679                 // Compute scroll offsets from task dismissal for animation.
3680                 // If we just take newScroll - oldScroll, everything to the right of dragged task
3681                 // translates to the left. We need to offset this in some cases:
3682                 // - In RTL, add page offset to all pages, since we want pages to move to the right
3683                 // Additionally, add a page offset if:
3684                 // - Current page is rightmost page (leftmost for RTL)
3685                 // - Dragging an adjacent page on the left side (right side for RTL)
3686                 int offset = mIsRtl ? scrollDiffPerPage : 0;
3687                 if (mCurrentPage == dismissedIndex) {
3688                     int lastPage = taskCount - 1;
3689                     if (mCurrentPage == lastPage) {
3690                         offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
3691                     }
3692                 } else {
3693                     // Dismissing an adjacent page.
3694                     int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR)
3695                     if (dismissedIndex == negativeAdjacent) {
3696                         offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
3697                     }
3698                 }
3699 
3700                 int scrollDiff = newScroll[i] - oldScroll[i] + offset;
3701                 if (scrollDiff != 0) {
3702                     FloatProperty translationProperty = child instanceof TaskView
3703                             ? ((TaskView) child).getPrimaryDismissTranslationProperty()
3704                             : getPagedOrientationHandler().getPrimaryViewTranslate();
3705 
3706                     float additionalDismissDuration =
3707                             ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * Math.abs(
3708                                     i - dismissedIndex);
3709 
3710                     // We are in non-grid layout.
3711                     // If dismissing for split select, use split timings.
3712                     // If not, use dismiss timings.
3713                     float animationStartProgress = isSplitSelectionActive()
3714                             ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset(), 0f, 1f)
3715                             : Utilities.boundToRange(
3716                                     INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
3717                                             + additionalDismissDuration, 0f, 1f);
3718 
3719                     float animationEndProgress = isSplitSelectionActive()
3720                             ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset()
3721                                             + splitTimings.getGridSlideDurationOffset(), 0f, 1f)
3722                             : 1f;
3723 
3724                     // Slide tiles in horizontally to fill dismissed area
3725                     anim.setFloat(child, translationProperty, scrollDiff,
3726                             clampToProgress(
3727                                     splitTimings.getGridSlidePrimaryInterpolator(),
3728                                     animationStartProgress,
3729                                     animationEndProgress
3730                             )
3731                     );
3732 
3733                     if (mEnableDrawingLiveTile && child instanceof TaskView
3734                             && ((TaskView) child).isRunningTask()) {
3735                         anim.addOnFrameCallback(() -> {
3736                             runActionOnRemoteHandles(
3737                                     remoteTargetHandle ->
3738                                             remoteTargetHandle.getTaskViewSimulator()
3739                                                     .taskPrimaryTranslation.value =
3740                                                     getPagedOrientationHandler().getPrimaryValue(
3741                                                             child.getTranslationX(),
3742                                                             child.getTranslationY()
3743                                                     ));
3744                             redrawLiveTile();
3745                         });
3746                     }
3747                     needsCurveUpdates = true;
3748                 }
3749             } else if (child instanceof TaskView) {
3750                 TaskView taskView = (TaskView) child;
3751                 if (isFocusedTaskDismissed) {
3752                     if (nextFocusedTaskView != null &&
3753                             !isSameGridRow(taskView, nextFocusedTaskView)) {
3754                         continue;
3755                     }
3756                 } else {
3757                     if (i < dismissedIndex || !isSameGridRow(taskView, dismissedTaskView)) {
3758                         continue;
3759                     }
3760                 }
3761                 // Animate task with index >= dismissed index and in the same row as the
3762                 // dismissed index or next focused index. Offset successive task dismissal
3763                 // durations for a staggered effect.
3764                 distanceFromDismissedTask++;
3765                 int staggerColumn =  isStagingFocusedTask
3766                         ? (int) Math.ceil(distanceFromDismissedTask / 2f)
3767                         : distanceFromDismissedTask;
3768                 // Set timings based on if user is initiating splitscreen on the focused task,
3769                 // or splitting/dismissing some other task.
3770                 float animationStartProgress = isStagingFocusedTask
3771                         ? Utilities.boundToRange(
3772                                 splitTimings.getGridSlideStartOffset()
3773                                         + (splitTimings.getGridSlideStaggerOffset()
3774                                         * staggerColumn),
3775                         0f,
3776                         dismissTranslationInterpolationEnd)
3777                         : Utilities.boundToRange(
3778                                 INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
3779                                         + ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
3780                                         * staggerColumn, 0f, dismissTranslationInterpolationEnd);
3781                 float animationEndProgress = isStagingFocusedTask
3782                         ? Utilities.boundToRange(
3783                                 splitTimings.getGridSlideStartOffset()
3784                                         + (splitTimings.getGridSlideStaggerOffset() * staggerColumn)
3785                                         + splitTimings.getGridSlideDurationOffset(),
3786                         0f,
3787                         dismissTranslationInterpolationEnd)
3788                         : dismissTranslationInterpolationEnd;
3789                 Interpolator dismissInterpolator = isStagingFocusedTask ? OVERSHOOT_0_75 : LINEAR;
3790 
3791                 if (taskView == nextFocusedTaskView) {
3792                     // Enlarge the task to be focused next, and translate into focus position.
3793                     float scale = mTaskWidth / (float) mLastComputedGridTaskSize.width();
3794                     anim.setFloat(taskView, TaskView.DISMISS_SCALE, scale,
3795                             clampToProgress(LINEAR, animationStartProgress,
3796                                     dismissTranslationInterpolationEnd));
3797                     anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(),
3798                             mIsRtl ? dismissedTaskWidth : -dismissedTaskWidth,
3799                             clampToProgress(LINEAR, animationStartProgress,
3800                                     dismissTranslationInterpolationEnd));
3801                     float secondaryTranslation = -mTaskGridVerticalDiff;
3802                     if (!nextFocusedTaskFromTop) {
3803                         secondaryTranslation -= mTopBottomRowHeightDiff;
3804                     }
3805                     anim.setFloat(taskView, taskView.getSecondaryDismissTranslationProperty(),
3806                             secondaryTranslation, clampToProgress(LINEAR, animationStartProgress,
3807                                     dismissTranslationInterpolationEnd));
3808                     anim.add(taskView.getFocusTransitionScaleAndDimOutAnimator(),
3809                             clampToProgress(LINEAR, 0f, ANIMATION_DISMISS_PROGRESS_MIDPOINT));
3810                 } else {
3811                     float primaryTranslation =
3812                             nextFocusedTaskView != null ? nextFocusedTaskWidth : dismissedTaskWidth;
3813                     if (isStagingFocusedTask) {
3814                         // Moves less if focused task is not in scroll position.
3815                         int focusedTaskScroll = getScrollForPage(dismissedIndex);
3816                         int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this);
3817                         int focusedTaskScrollDiff = primaryScroll - focusedTaskScroll;
3818                         primaryTranslation +=
3819                                 mIsRtl ? focusedTaskScrollDiff : -focusedTaskScrollDiff;
3820                     }
3821 
3822                     anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(),
3823                             mIsRtl ? primaryTranslation : -primaryTranslation,
3824                             clampToProgress(dismissInterpolator, animationStartProgress,
3825                                     animationEndProgress));
3826                 }
3827             }
3828         }
3829 
3830         if (needsCurveUpdates) {
3831             anim.addOnFrameCallback(this::updateCurveProperties);
3832         }
3833 
3834         // Add a tiny bit of translation Z, so that it draws on top of other views. This is relevant
3835         // (e.g.) when we dismiss a task by sliding it upward: if there is a row of icons above, we
3836         // want the dragged task to stay above all other views.
3837         if (animateTaskView) {
3838             dismissedTaskView.setTranslationZ(0.1f);
3839         }
3840 
3841         mPendingAnimation = anim;
3842         final TaskView finalNextFocusedTaskView = nextFocusedTaskView;
3843         final boolean finalCloseGapBetweenClearAll = closeGapBetweenClearAll;
3844         final boolean finalSnapToLastTask = snapToLastTask;
3845         final boolean finalIsFocusedTaskDismissed = isFocusedTaskDismissed;
3846         mPendingAnimation.addEndListener(new Consumer<Boolean>() {
3847             @Override
3848             public void accept(Boolean success) {
3849                 if (mEnableDrawingLiveTile && dismissedTaskView.isRunningTask() && success) {
3850                     finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
3851                             () -> onEnd(success));
3852                 } else {
3853                     onEnd(success);
3854                 }
3855             }
3856 
3857             @SuppressWarnings("WrongCall")
3858             private void onEnd(boolean success) {
3859                 // Reset task translations as they may have updated via animations in
3860                 // createTaskDismissAnimation
3861                 resetTaskVisuals();
3862 
3863                 if (success) {
3864                     if (shouldRemoveTask) {
3865                         if (dismissedTaskView.isRunningTask()) {
3866                             finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
3867                                     () -> removeTaskInternal(dismissedTaskViewId));
3868                         } else {
3869                             removeTaskInternal(dismissedTaskViewId);
3870                         }
3871                         announceForAccessibility(
3872                                 getResources().getString(R.string.task_view_closed));
3873                         mContainer.getStatsLogManager().logger()
3874                                 .withItemInfo(dismissedTaskView.getFirstItemInfo())
3875                                 .log(LAUNCHER_TASK_DISMISS_SWIPE_UP);
3876                     }
3877 
3878                     int pageToSnapTo = mCurrentPage;
3879                     mCurrentPageScrollDiff = 0;
3880                     int taskViewIdToSnapTo = -1;
3881                     if (showAsGrid) {
3882                         if (finalCloseGapBetweenClearAll) {
3883                             if (finalSnapToLastTask) {
3884                                 // Last task will be determined after removing dismissed task.
3885                                 pageToSnapTo = -1;
3886                             } else if (taskCount > 2) {
3887                                 pageToSnapTo = indexOfChild(mClearAllButton);
3888                             } else if (isClearAllHidden) {
3889                                 // Snap to focused task if clear all is hidden.
3890                                 pageToSnapTo = 0;
3891                             }
3892                         } else {
3893                             // Get the id of the task view we will snap to based on the current
3894                             // page's relative position as the order of indices change over time due
3895                             // to dismissals.
3896                             TaskView snappedTaskView = getTaskViewAt(mCurrentPage);
3897                             boolean calculateScrollDiff = true;
3898                             if (snappedTaskView != null && !finalSnapToLastTask) {
3899                                 if (snappedTaskView.getTaskViewId() == mFocusedTaskViewId) {
3900                                     if (finalNextFocusedTaskView != null) {
3901                                         taskViewIdToSnapTo =
3902                                                 finalNextFocusedTaskView.getTaskViewId();
3903                                     } else if (dismissedTaskViewId != mFocusedTaskViewId) {
3904                                         taskViewIdToSnapTo = mFocusedTaskViewId;
3905                                     } else {
3906                                         // Won't focus next task in split select, so snap to the
3907                                         // first task.
3908                                         pageToSnapTo = 0;
3909                                         calculateScrollDiff = false;
3910                                     }
3911                                 } else {
3912                                     int snappedTaskViewId = snappedTaskView.getTaskViewId();
3913                                     boolean isSnappedTaskInTopRow = mTopRowIdSet.contains(
3914                                             snappedTaskViewId);
3915                                     IntArray taskViewIdArray =
3916                                             isSnappedTaskInTopRow ? getTopRowIdArray()
3917                                                     : getBottomRowIdArray();
3918                                     int snappedIndex = taskViewIdArray.indexOf(snappedTaskViewId);
3919                                     taskViewIdArray.removeValue(dismissedTaskViewId);
3920                                     if (finalNextFocusedTaskView != null) {
3921                                         taskViewIdArray.removeValue(
3922                                                 finalNextFocusedTaskView.getTaskViewId());
3923                                     }
3924                                     if (snappedIndex >= 0
3925                                             && snappedIndex < taskViewIdArray.size()) {
3926                                         taskViewIdToSnapTo = taskViewIdArray.get(snappedIndex);
3927                                     } else if (snappedIndex == taskViewIdArray.size()) {
3928                                         // If the snapped task is the last item from the
3929                                         // dismissed row,
3930                                         // snap to the same column in the other grid row
3931                                         IntArray inverseRowTaskViewIdArray =
3932                                                 isSnappedTaskInTopRow ? getBottomRowIdArray()
3933                                                         : getTopRowIdArray();
3934                                         if (snappedIndex < inverseRowTaskViewIdArray.size()) {
3935                                             taskViewIdToSnapTo = inverseRowTaskViewIdArray.get(
3936                                                     snappedIndex);
3937                                         }
3938                                     }
3939                                 }
3940                             }
3941 
3942                             if (calculateScrollDiff) {
3943                                 int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(
3944                                         RecentsView.this);
3945                                 int currentPageScroll = getScrollForPage(mCurrentPage);
3946                                 mCurrentPageScrollDiff = primaryScroll - currentPageScroll;
3947                             }
3948                         }
3949                     } else if (dismissedIndex < pageToSnapTo || pageToSnapTo == taskCount - 1) {
3950                         pageToSnapTo--;
3951                     }
3952                     boolean isHomeTaskDismissed = dismissedTaskView == getHomeTaskView();
3953                     removeViewInLayout(dismissedTaskView);
3954                     mTopRowIdSet.remove(dismissedTaskViewId);
3955 
3956                     if (taskCount == 1) {
3957                         removeViewInLayout(mClearAllButton);
3958                         if (isHomeTaskDismissed) {
3959                             updateEmptyMessage();
3960                         } else if (!mSplitSelectStateController.isSplitSelectActive()) {
3961                             startHome();
3962                         }
3963                     } else {
3964                         // Update focus task and its size.
3965                         if (finalIsFocusedTaskDismissed && finalNextFocusedTaskView != null) {
3966                             mFocusedTaskViewId = enableGridOnlyOverview()
3967                                     ? INVALID_TASK_ID
3968                                     : finalNextFocusedTaskView.getTaskViewId();
3969                             mTopRowIdSet.remove(mFocusedTaskViewId);
3970                             finalNextFocusedTaskView.animateIconScaleAndDimIntoView();
3971                         }
3972                         updateTaskSize(/*isTaskDismissal=*/ true);
3973                         updateChildTaskOrientations();
3974                         // Update scroll and snap to page.
3975                         updateScrollSynchronously();
3976 
3977                         if (showAsGrid) {
3978                             // Rebalance tasks in the grid
3979                             int highestVisibleTaskIndex = getHighestVisibleTaskIndex();
3980                             if (highestVisibleTaskIndex < Integer.MAX_VALUE) {
3981                                 TaskView taskView = requireTaskViewAt(highestVisibleTaskIndex);
3982 
3983                                 boolean shouldRebalance;
3984                                 int screenStart = getPagedOrientationHandler().getPrimaryScroll(
3985                                         RecentsView.this);
3986                                 int taskStart = getPagedOrientationHandler().getChildStart(taskView)
3987                                         + (int) taskView.getOffsetAdjustment(/*gridEnabled=*/ true);
3988 
3989                                 // Rebalance only if there is a maximum gap between the task and the
3990                                 // screen's edge; this ensures that rebalanced tasks are outside the
3991                                 // visible screen.
3992                                 if (mIsRtl) {
3993                                     shouldRebalance = taskStart <= screenStart + mPageSpacing;
3994                                 } else {
3995                                     int screenEnd = screenStart
3996                                             + getPagedOrientationHandler().getMeasuredSize(
3997                                             RecentsView.this);
3998                                     int taskSize = (int) (
3999                                             getPagedOrientationHandler().getMeasuredSize(
4000                                             taskView) * taskView
4001                                             .getSizeAdjustment(/*fullscreenEnabled=*/false));
4002                                     int taskEnd = taskStart + taskSize;
4003 
4004                                     shouldRebalance = taskEnd >= screenEnd - mPageSpacing;
4005                                 }
4006 
4007                                 if (shouldRebalance) {
4008                                     updateGridProperties(/*isTaskDismissal=*/ true,
4009                                             highestVisibleTaskIndex);
4010                                     updateScrollSynchronously();
4011                                 }
4012                             }
4013 
4014                             IntArray topRowIdArray = getTopRowIdArray();
4015                             IntArray bottomRowIdArray = getBottomRowIdArray();
4016                             if (finalSnapToLastTask) {
4017                                 // If snapping to last task, find the last task after dismissal.
4018                                 pageToSnapTo = indexOfChild(
4019                                         getLastGridTaskView(topRowIdArray, bottomRowIdArray));
4020                             } else if (taskViewIdToSnapTo != -1) {
4021                                 // If snapping to another page due to indices rearranging, find
4022                                 // the new index after dismissal & rearrange using the task view id.
4023                                 pageToSnapTo = indexOfChild(
4024                                         getTaskViewFromTaskViewId(taskViewIdToSnapTo));
4025                                 if (!currentPageSnapsToEndOfGrid) {
4026                                     // If it wasn't snapped to one of the last pages, but is now
4027                                     // snapped to last pages, we'll need to compensate for the
4028                                     // offset from the page's scroll to its visual position.
4029                                     mCurrentPageScrollDiff += getOffsetFromScrollPosition(
4030                                             pageToSnapTo, topRowIdArray, bottomRowIdArray);
4031                                 }
4032                             }
4033                         }
4034                         pageBeginTransition();
4035                         setCurrentPage(pageToSnapTo);
4036                         // Update various scroll-dependent UI.
4037                         dispatchScrollChanged();
4038                         updateActionsViewFocusedScroll();
4039                         if (isClearAllHidden() && !mContainer.getDeviceProfile().isTablet) {
4040                             mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING,
4041                                     false);
4042                         }
4043                     }
4044                 }
4045                 updateCurrentTaskActionsVisibility();
4046                 onDismissAnimationEnds();
4047                 mPendingAnimation = null;
4048             }
4049         });
4050     }
4051 
4052     /**
4053      * Hides all overview actions if user is halfway through split selection, shows otherwise.
4054      * We only show split option if:
4055      * * Focused view is a single app
4056      * * Device is large screen
4057      */
updateCurrentTaskActionsVisibility()4058     private void updateCurrentTaskActionsVisibility() {
4059         TaskView taskView = getCurrentPageTaskView();
4060         boolean isCurrentSplit = taskView instanceof GroupedTaskView;
4061         GroupedTaskView groupedTaskView = isCurrentSplit ? (GroupedTaskView) taskView : null;
4062         // Update flags to see if entire actions bar should be hidden.
4063         if (!FeatureFlags.enableAppPairs()) {
4064             mActionsView.updateHiddenFlags(HIDDEN_SPLIT_SCREEN, isCurrentSplit);
4065         }
4066         mActionsView.updateHiddenFlags(HIDDEN_SPLIT_SELECT_ACTIVE, isSplitSelectionActive());
4067         // Update flags to see if actions bar should show buttons for a single task or a pair of
4068         // tasks.
4069         boolean canSaveAppPair = isCurrentSplit && supportsAppPairs() &&
4070                 getSplitSelectController().getAppPairsController().canSaveAppPair(groupedTaskView);
4071         mActionsView.updateForGroupedTask(isCurrentSplit, canSaveAppPair);
4072 
4073         boolean isCurrentDesktop = taskView instanceof DesktopTaskView;
4074         mActionsView.updateHiddenFlags(HIDDEN_DESKTOP, isCurrentDesktop);
4075     }
4076 
4077     /** Returns if app pairs are supported in this launcher. Overridden in subclasses. */
supportsAppPairs()4078     public boolean supportsAppPairs() {
4079         return true;
4080     }
4081 
4082     /**
4083      * Returns all the tasks in the top row, without the focused task
4084      */
getTopRowIdArray()4085     private IntArray getTopRowIdArray() {
4086         if (mTopRowIdSet.isEmpty()) {
4087             return new IntArray(0);
4088         }
4089         IntArray topArray = new IntArray(mTopRowIdSet.size());
4090         int taskViewCount = getTaskViewCount();
4091         for (int i = 0; i < taskViewCount; i++) {
4092             int taskViewId = requireTaskViewAt(i).getTaskViewId();
4093             if (mTopRowIdSet.contains(taskViewId)) {
4094                 topArray.add(taskViewId);
4095             }
4096         }
4097         return topArray;
4098     }
4099 
4100     /**
4101      * Returns all the tasks in the bottom row, without the focused task
4102      */
getBottomRowIdArray()4103     private IntArray getBottomRowIdArray() {
4104         int bottomRowIdArraySize = getBottomRowTaskCountForTablet();
4105         if (bottomRowIdArraySize <= 0) {
4106             return new IntArray(0);
4107         }
4108         IntArray bottomArray = new IntArray(bottomRowIdArraySize);
4109         int taskViewCount = getTaskViewCount();
4110         for (int i = 0; i < taskViewCount; i++) {
4111             int taskViewId = requireTaskViewAt(i).getTaskViewId();
4112             if (!mTopRowIdSet.contains(taskViewId) && taskViewId != mFocusedTaskViewId) {
4113                 bottomArray.add(taskViewId);
4114             }
4115         }
4116         return bottomArray;
4117     }
4118 
4119     /**
4120      * Iterate the grid by columns instead of by TaskView index, starting after the focused task and
4121      * up to the last balanced column.
4122      *
4123      * @return the highest visible TaskView index between both rows
4124      */
getHighestVisibleTaskIndex()4125     private int getHighestVisibleTaskIndex() {
4126         if (mTopRowIdSet.isEmpty()) return Integer.MAX_VALUE; // return earlier
4127 
4128         int lastVisibleIndex = Integer.MAX_VALUE;
4129         IntArray topRowIdArray = getTopRowIdArray();
4130         IntArray bottomRowIdArray = getBottomRowIdArray();
4131         int balancedColumns = Math.min(bottomRowIdArray.size(), topRowIdArray.size());
4132 
4133         for (int i = 0; i < balancedColumns; i++) {
4134             TaskView topTask = getTaskViewFromTaskViewId(topRowIdArray.get(i));
4135 
4136             if (isTaskViewVisible(topTask)) {
4137                 TaskView bottomTask = getTaskViewFromTaskViewId(bottomRowIdArray.get(i));
4138                 lastVisibleIndex = Math.max(indexOfChild(topTask), indexOfChild(bottomTask));
4139             } else if (lastVisibleIndex < Integer.MAX_VALUE) {
4140                 break;
4141             }
4142         }
4143 
4144         return lastVisibleIndex;
4145     }
4146 
removeTaskInternal(int dismissedTaskViewId)4147     private void removeTaskInternal(int dismissedTaskViewId) {
4148         int[] taskIds = getTaskIdsForTaskViewId(dismissedTaskViewId);
4149         UI_HELPER_EXECUTOR.getHandler().post(
4150                 () -> {
4151                     for (int taskId : taskIds) {
4152                         if (taskId != -1) {
4153                             ActivityManagerWrapper.getInstance().removeTask(taskId);
4154                         }
4155                     }
4156                 });
4157     }
4158 
onDismissAnimationEnds()4159     protected void onDismissAnimationEnds() {
4160         AccessibilityManagerCompat.sendTestProtocolEventToTest(getContext(),
4161                 DISMISS_ANIMATION_ENDS_MESSAGE);
4162     }
4163 
createAllTasksDismissAnimation(long duration)4164     public PendingAnimation createAllTasksDismissAnimation(long duration) {
4165         if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) {
4166             throw new IllegalStateException("Another pending animation is still running");
4167         }
4168         PendingAnimation anim = new PendingAnimation(duration);
4169 
4170         int count = getTaskViewCount();
4171         for (int i = 0; i < count; i++) {
4172             addDismissedTaskAnimations(requireTaskViewAt(i), duration, anim);
4173         }
4174 
4175         mPendingAnimation = anim;
4176         mPendingAnimation.addEndListener(isSuccess -> {
4177             if (isSuccess) {
4178                 // Remove all the task views now
4179                 finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, () -> {
4180                     UI_HELPER_EXECUTOR.getHandler().post(
4181                             ActivityManagerWrapper.getInstance()::removeAllRecentTasks);
4182                     removeTasksViewsAndClearAllButton();
4183                     startHome();
4184                 });
4185             }
4186             mPendingAnimation = null;
4187         });
4188         return anim;
4189     }
4190 
snapToPageRelative(int delta, boolean cycle, @TaskGridNavHelper.TASK_NAV_DIRECTION int direction)4191     private boolean snapToPageRelative(int delta, boolean cycle,
4192             @TaskGridNavHelper.TASK_NAV_DIRECTION int direction) {
4193         // Set next page if scroll animation is still running, otherwise cannot snap to the
4194         // next page on successive key presses. Setting the current page aborts the scroll.
4195         if (!mScroller.isFinished()) {
4196             setCurrentPage(getNextPage());
4197         }
4198         int pageCount = getPageCount();
4199         if (pageCount == 0) {
4200             return false;
4201         }
4202         final int newPageUnbound = getNextPageInternal(delta, direction, cycle);
4203         if (!cycle && (newPageUnbound < 0 || newPageUnbound > pageCount)) {
4204             return false;
4205         }
4206         snapToPage((newPageUnbound + pageCount) % pageCount);
4207         getChildAt(getNextPage()).requestFocus();
4208         return true;
4209     }
4210 
getNextPageInternal(int delta, @TaskGridNavHelper.TASK_NAV_DIRECTION int direction, boolean cycle)4211     private int getNextPageInternal(int delta, @TaskGridNavHelper.TASK_NAV_DIRECTION int direction,
4212             boolean cycle) {
4213         if (!showAsGrid()) {
4214             return getNextPage() + delta;
4215         }
4216 
4217         // Init task grid nav helper with top/bottom id arrays.
4218         TaskGridNavHelper taskGridNavHelper = new TaskGridNavHelper(getTopRowIdArray(),
4219                 getBottomRowIdArray(), mFocusedTaskViewId);
4220 
4221         // Get current page's task view ID.
4222         TaskView currentPageTaskView = getCurrentPageTaskView();
4223         int currentPageTaskViewId;
4224         if (currentPageTaskView != null) {
4225             currentPageTaskViewId = currentPageTaskView.getTaskViewId();
4226         } else if (mCurrentPage == indexOfChild(mClearAllButton)) {
4227             currentPageTaskViewId = TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID;
4228         } else {
4229             return INVALID_PAGE;
4230         }
4231 
4232         int nextGridPage =
4233                 taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
4234         return nextGridPage == TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID
4235                 ? indexOfChild(mClearAllButton)
4236                 : indexOfChild(getTaskViewFromTaskViewId(nextGridPage));
4237     }
4238 
runDismissAnimation(PendingAnimation pendingAnim)4239     private void runDismissAnimation(PendingAnimation pendingAnim) {
4240         AnimatorPlaybackController controller = pendingAnim.createPlaybackController();
4241         controller.dispatchOnStart();
4242         controller.getAnimationPlayer().setInterpolator(FAST_OUT_SLOW_IN);
4243         controller.start();
4244     }
4245 
4246     @UiThread
dismissTask(int taskId)4247     private void dismissTask(int taskId) {
4248         TaskView taskView = getTaskViewByTaskId(taskId);
4249         if (taskView == null) {
4250             return;
4251         }
4252         dismissTask(taskView, true /* animate */, false /* removeTask */);
4253     }
4254 
dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask)4255     public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) {
4256         PendingAnimation pa = new PendingAnimation(DISMISS_TASK_DURATION);
4257         createTaskDismissAnimation(pa, taskView, animateTaskView, removeTask, DISMISS_TASK_DURATION,
4258                 false /* dismissingForSplitSelection*/);
4259         runDismissAnimation(pa);
4260     }
4261 
4262     @SuppressWarnings("unused")
dismissAllTasks(View view)4263     private void dismissAllTasks(View view) {
4264         runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION));
4265         mContainer.getStatsLogManager().logger().log(LAUNCHER_TASK_CLEAR_ALL);
4266     }
4267 
dismissCurrentTask()4268     private void dismissCurrentTask() {
4269         TaskView taskView = getNextPageTaskView();
4270         if (taskView != null) {
4271             dismissTask(taskView, true /*animateTaskView*/, true /*removeTask*/);
4272         }
4273     }
4274 
4275     @Override
dispatchKeyEvent(KeyEvent event)4276     public boolean dispatchKeyEvent(KeyEvent event) {
4277         if (isHandlingTouch() || event.getAction() != KeyEvent.ACTION_DOWN) {
4278             return super.dispatchKeyEvent(event);
4279         }
4280         switch (event.getKeyCode()) {
4281             case KeyEvent.KEYCODE_TAB:
4282                 return snapToPageRelative(event.isShiftPressed() ? -1 : 1, true /* cycle */,
4283                         DIRECTION_TAB);
4284             case KeyEvent.KEYCODE_DPAD_RIGHT:
4285                 return snapToPageRelative(mIsRtl ? -1 : 1, true /* cycle */, DIRECTION_RIGHT);
4286             case KeyEvent.KEYCODE_DPAD_LEFT:
4287                 return snapToPageRelative(mIsRtl ? 1 : -1, true /* cycle */, DIRECTION_LEFT);
4288             case KeyEvent.KEYCODE_DPAD_UP:
4289                 return snapToPageRelative(1, false /* cycle */, DIRECTION_UP);
4290             case KeyEvent.KEYCODE_DPAD_DOWN:
4291                 return snapToPageRelative(1, false /* cycle */, DIRECTION_DOWN);
4292             case KeyEvent.KEYCODE_DEL:
4293             case KeyEvent.KEYCODE_FORWARD_DEL:
4294                 dismissCurrentTask();
4295                 return true;
4296             case KeyEvent.KEYCODE_NUMPAD_DOT:
4297                 if (event.isAltPressed()) {
4298                     // Numpad DEL pressed while holding Alt.
4299                     dismissCurrentTask();
4300                     return true;
4301                 }
4302         }
4303         return super.dispatchKeyEvent(event);
4304     }
4305 
4306     @Override
onFocusChanged(boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect)4307     protected void onFocusChanged(boolean gainFocus, int direction,
4308             @Nullable Rect previouslyFocusedRect) {
4309         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
4310         if (gainFocus && getChildCount() > 0) {
4311             switch (direction) {
4312                 case FOCUS_FORWARD:
4313                     setCurrentPage(0);
4314                     break;
4315                 case FOCUS_BACKWARD:
4316                 case FOCUS_RIGHT:
4317                 case FOCUS_LEFT:
4318                     setCurrentPage(getChildCount() - 1);
4319                     break;
4320             }
4321         }
4322     }
4323 
getContentAlpha()4324     public float getContentAlpha() {
4325         return mContentAlpha;
4326     }
4327 
setContentAlpha(float alpha)4328     public void setContentAlpha(float alpha) {
4329         if (alpha == mContentAlpha) {
4330             return;
4331         }
4332         alpha = Utilities.boundToRange(alpha, 0, 1);
4333         mContentAlpha = alpha;
4334 
4335         TaskView runningTaskView = getRunningTaskView();
4336         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
4337             TaskView child = requireTaskViewAt(i);
4338             if (runningTaskView != null && mRunningTaskTileHidden && child == runningTaskView) {
4339                 continue;
4340             }
4341             child.setStableAlpha(alpha);
4342         }
4343         mClearAllButton.setContentAlpha(mContentAlpha);
4344         int alphaInt = Math.round(alpha * 255);
4345         mEmptyMessagePaint.setAlpha(alphaInt);
4346         mEmptyIcon.setAlpha(alphaInt);
4347         mActionsView.getContentAlpha().updateValue(mContentAlpha);
4348 
4349         if (alpha > 0) {
4350             setVisibility(VISIBLE);
4351         } else if (!mFreezeViewVisibility) {
4352             setVisibility(INVISIBLE);
4353         }
4354     }
4355 
4356     /**
4357      * Freezes the view visibility change. When frozen, the view will not change its visibility
4358      * to gone due to alpha changes.
4359      */
setFreezeViewVisibility(boolean freezeViewVisibility)4360     public void setFreezeViewVisibility(boolean freezeViewVisibility) {
4361         if (mFreezeViewVisibility != freezeViewVisibility) {
4362             mFreezeViewVisibility = freezeViewVisibility;
4363             if (!mFreezeViewVisibility) {
4364                 setVisibility(mContentAlpha > 0 ? VISIBLE : INVISIBLE);
4365             }
4366         }
4367     }
4368 
4369     @Override
setVisibility(int visibility)4370     public void setVisibility(int visibility) {
4371         super.setVisibility(visibility);
4372         if (mActionsView != null) {
4373             mActionsView.updateHiddenFlags(HIDDEN_NO_RECENTS, visibility != VISIBLE);
4374             if (visibility != VISIBLE) {
4375                 mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false);
4376             }
4377         }
4378     }
4379 
4380     @Override
onConfigurationChanged(Configuration newConfig)4381     protected void onConfigurationChanged(Configuration newConfig) {
4382         super.onConfigurationChanged(newConfig);
4383         updateRecentsRotation();
4384         onOrientationChanged();
4385     }
4386 
4387     /**
4388      * Updates {@link RecentsOrientedState}'s cached RecentsView rotation.
4389      */
updateRecentsRotation()4390     public void updateRecentsRotation() {
4391         final int rotation = TraceHelper.allowIpcs(
4392                 "RecentsView.updateRecentsRotation", () -> mContainer.getDisplay().getRotation());
4393         // Log real orientation change.
4394         if (mOrientationState.setRecentsRotation(rotation)) {
4395             logOrientationChanged();
4396         }
4397     }
4398 
setLayoutRotation(int touchRotation, int displayRotation)4399     public void setLayoutRotation(int touchRotation, int displayRotation) {
4400         if (mOrientationState.update(touchRotation, displayRotation)) {
4401             updateOrientationHandler();
4402         }
4403     }
4404 
getPagedViewOrientedState()4405     public RecentsOrientedState getPagedViewOrientedState() {
4406         return mOrientationState;
4407     }
4408 
getPagedOrientationHandler()4409     public RecentsPagedOrientationHandler getPagedOrientationHandler() {
4410         return (RecentsPagedOrientationHandler) super.getPagedOrientationHandler();
4411     }
4412 
4413     @Nullable
getNextTaskView()4414     public TaskView getNextTaskView() {
4415         return getTaskViewAt(getRunningTaskIndex() + 1);
4416     }
4417 
4418     @Nullable
getCurrentPageTaskView()4419     public TaskView getCurrentPageTaskView() {
4420         return getTaskViewAt(getCurrentPage());
4421     }
4422 
4423     @Nullable
getNextPageTaskView()4424     public TaskView getNextPageTaskView() {
4425         return getTaskViewAt(getNextPage());
4426     }
4427 
4428     @Nullable
getTaskViewNearestToCenterOfScreen()4429     public TaskView getTaskViewNearestToCenterOfScreen() {
4430         return getTaskViewAt(getPageNearestToCenterOfScreen());
4431     }
4432 
4433     /**
4434      * Returns null instead of indexOutOfBoundsError when index is not in range
4435      */
4436     @Nullable
getTaskViewAt(int index)4437     public TaskView getTaskViewAt(int index) {
4438         View child = getChildAt(index);
4439         return child instanceof TaskView ? (TaskView) child : null;
4440     }
4441 
4442     /**
4443      * A version of {@link #getTaskViewAt} when the caller is sure about the input index.
4444      */
4445     @NonNull
requireTaskViewAt(int index)4446     private TaskView requireTaskViewAt(int index) {
4447         return Objects.requireNonNull(getTaskViewAt(index));
4448     }
4449 
setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener listener)4450     public void setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener listener) {
4451         mOnEmptyMessageUpdatedListener = listener;
4452     }
4453 
updateEmptyMessage()4454     public void updateEmptyMessage() {
4455         boolean isEmpty = getTaskViewCount() == 0;
4456         boolean hasSizeChanged = mLastMeasureSize.x != getWidth()
4457                 || mLastMeasureSize.y != getHeight();
4458         if (isEmpty == mShowEmptyMessage && !hasSizeChanged) {
4459             return;
4460         }
4461         setContentDescription(isEmpty ? mEmptyMessage : "");
4462         mShowEmptyMessage = isEmpty;
4463         updateEmptyStateUi(hasSizeChanged);
4464         invalidate();
4465 
4466         if (mOnEmptyMessageUpdatedListener != null) {
4467             mOnEmptyMessageUpdatedListener.onEmptyMessageUpdated(mShowEmptyMessage);
4468         }
4469     }
4470 
4471     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)4472     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
4473         // If we're going to a state without overview panel, avoid unnecessary onLayout that
4474         // cause TaskViews to re-arrange during animation to that state.
4475         if (!mOverviewStateEnabled && !mFirstLayout) {
4476             return;
4477         }
4478 
4479         mShowAsGridLastOnLayout = showAsGrid();
4480 
4481         super.onLayout(changed, left, top, right, bottom);
4482 
4483         updateEmptyStateUi(changed);
4484 
4485         setTaskModalness(mTaskModalness);
4486         mLastComputedTaskStartPushOutDistance = null;
4487         mLastComputedTaskEndPushOutDistance = null;
4488         updatePageOffsets();
4489         runActionOnRemoteHandles(
4490                 remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
4491                         .setScroll(getScrollOffset()));
4492         setImportantForAccessibility(isModal() ? IMPORTANT_FOR_ACCESSIBILITY_NO
4493                 : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
4494     }
4495 
updatePivots()4496     private void updatePivots() {
4497         if (mOverviewSelectEnabled) {
4498             if (enableGridOnlyOverview()) {
4499                 getModalTaskSize(mTempRect);
4500                 Rect selectedTaskPosition = getSelectedTaskBounds();
4501                 Utilities.getPivotsForScalingRectToRect(mTempRect, selectedTaskPosition,
4502                         mTempPointF);
4503             } else {
4504                 mTempPointF.set(mLastComputedTaskSize.centerX(), mLastComputedTaskSize.bottom);
4505             }
4506         } else {
4507             // Only update pivot when it is tablet and not in grid yet, so the pivot is correct
4508             // for non-current tasks when swiping up to overview
4509             if (enableGridOnlyOverview() && mContainer.getDeviceProfile().isTablet
4510                     && !mOverviewGridEnabled) {
4511                 mTempRect.set(mLastComputedCarouselTaskSize);
4512             } else {
4513                 mTempRect.set(mLastComputedTaskSize);
4514             }
4515             getPagedViewOrientedState().getFullScreenScaleAndPivot(mTempRect,
4516                     mContainer.getDeviceProfile(), mTempPointF);
4517         }
4518         setPivotX(mTempPointF.x);
4519         setPivotY(mTempPointF.y);
4520     }
4521 
4522     /**
4523      * Sets whether we should force-override the page offset mid-point to the current task, rather
4524      * than the running task, when updating page offsets.
4525      */
setOffsetMidpointIndexOverride(int offsetMidpointIndexOverride)4526     public void setOffsetMidpointIndexOverride(int offsetMidpointIndexOverride) {
4527         if (!enableAdditionalHomeAnimations()) {
4528             return;
4529         }
4530         mOffsetMidpointIndexOverride = offsetMidpointIndexOverride;
4531         updatePageOffsets();
4532     }
4533 
updatePageOffsets()4534     private void updatePageOffsets() {
4535         float offset = mAdjacentPageHorizontalOffset;
4536         float modalOffset = ACCELERATE_0_75.getInterpolation(mTaskModalness);
4537         int count = getChildCount();
4538         boolean showAsGrid = showAsGrid();
4539 
4540         TaskView runningTask = mRunningTaskViewId == INVALID_PAGE || !mRunningTaskTileHidden
4541                 ? null : getRunningTaskView();
4542         int midpoint = mOffsetMidpointIndexOverride == INVALID_PAGE
4543                 ? (runningTask == null ? INVALID_PAGE : indexOfChild(runningTask))
4544                 : mOffsetMidpointIndexOverride;
4545         int modalMidpoint = getCurrentPage();
4546         boolean isModalGridWithoutFocusedTask =
4547                 showAsGrid && enableGridOnlyOverview() && mTaskModalness > 0;
4548         if (isModalGridWithoutFocusedTask) {
4549             modalMidpoint = indexOfChild(mSelectedTask);
4550         }
4551 
4552         float midpointOffsetSize = 0;
4553         float leftOffsetSize = midpoint - 1 >= 0
4554                 ? getHorizontalOffsetSize(midpoint - 1, midpoint, offset)
4555                 : 0;
4556         float rightOffsetSize = midpoint + 1 < count
4557                 ? getHorizontalOffsetSize(midpoint + 1, midpoint, offset)
4558                 : 0;
4559 
4560         float modalMidpointOffsetSize = 0;
4561         float modalLeftOffsetSize = 0;
4562         float modalRightOffsetSize = 0;
4563         float gridOffsetSize = 0;
4564 
4565         if (showAsGrid) {
4566             // In grid, we only focus the task on the side. The reference index used for offset
4567             // calculation is the task directly next to the focus task in the grid.
4568             int referenceIndex = modalMidpoint == 0 ? 1 : 0;
4569             gridOffsetSize = referenceIndex < count
4570                     ? getHorizontalOffsetSize(referenceIndex, modalMidpoint, modalOffset)
4571                     : 0;
4572         } else {
4573             modalLeftOffsetSize = modalMidpoint - 1 >= 0
4574                     ? getHorizontalOffsetSize(modalMidpoint - 1, modalMidpoint, modalOffset)
4575                     : 0;
4576             modalRightOffsetSize = modalMidpoint + 1 < count
4577                     ? getHorizontalOffsetSize(modalMidpoint + 1, modalMidpoint, modalOffset)
4578                     : 0;
4579         }
4580 
4581         for (int i = 0; i < count; i++) {
4582             float translation = i == midpoint
4583                     ? midpointOffsetSize
4584                     : i < midpoint
4585                             ? leftOffsetSize
4586                             : rightOffsetSize;
4587             if (isModalGridWithoutFocusedTask) {
4588                 gridOffsetSize = getHorizontalOffsetSize(i, modalMidpoint, modalOffset);
4589                 gridOffsetSize = Math.abs(gridOffsetSize) * (i <= modalMidpoint ? 1 : -1);
4590             }
4591             float modalTranslation = i == modalMidpoint
4592                     ? modalMidpointOffsetSize
4593                     : showAsGrid
4594                             ? gridOffsetSize
4595                             : i < modalMidpoint ? modalLeftOffsetSize : modalRightOffsetSize;
4596             float totalTranslationX = translation + modalTranslation;
4597             View child = getChildAt(i);
4598             FloatProperty translationPropertyX = child instanceof TaskView
4599                     ? ((TaskView) child).getPrimaryTaskOffsetTranslationProperty()
4600                     : getPagedOrientationHandler().getPrimaryViewTranslate();
4601             translationPropertyX.set(child, totalTranslationX);
4602             if (mEnableDrawingLiveTile && i == getRunningTaskIndex()) {
4603                 runActionOnRemoteHandles(
4604                         remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
4605                                 .taskPrimaryTranslation.value = totalTranslationX);
4606                 redrawLiveTile();
4607             }
4608 
4609             if (showAsGrid && enableGridOnlyOverview() && child instanceof TaskView) {
4610                 float totalTranslationY = getVerticalOffsetSize(i, modalOffset);
4611                 FloatProperty translationPropertyY =
4612                         ((TaskView) child).getSecondaryTaskOffsetTranslationProperty();
4613                 translationPropertyY.set(child, totalTranslationY);
4614             }
4615         }
4616         updateCurveProperties();
4617     }
4618 
4619     /**
4620      * Computes the child position with persistent translation considered (see
4621      * {@link TaskView#getPersistentTranslationX()}.
4622      */
4623     private void getPersistentChildPosition(int childIndex, int midPointScroll, RectF outRect) {
4624         View child = getChildAt(childIndex);
4625         outRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
4626         if (child instanceof TaskView) {
4627             TaskView taskView = (TaskView) child;
4628             outRect.offset(taskView.getPersistentTranslationX(),
4629                     taskView.getPersistentTranslationY());
4630             outRect.top += mContainer.getDeviceProfile().overviewTaskThumbnailTopMarginPx;
4631 
4632             mTempMatrix.reset();
4633             float persistentScale = taskView.getPersistentScale();
4634             mTempMatrix.postScale(persistentScale, persistentScale,
4635                     mIsRtl ? outRect.right : outRect.left, outRect.top);
4636             mTempMatrix.mapRect(outRect);
4637         }
4638         outRect.offset(getPagedOrientationHandler().getPrimaryValue(-midPointScroll, 0),
4639                 getPagedOrientationHandler().getSecondaryValue(-midPointScroll, 0));
4640     }
4641 
4642     /**
4643      * Computes the distance to offset the given child such that it is completely offscreen when
4644      * translating away from the given midpoint.
4645      * @param offsetProgress From 0 to 1 where 0 means no offset and 1 means offset offscreen.
4646      */
4647     private float getHorizontalOffsetSize(int childIndex, int midpointIndex, float offsetProgress) {
4648         if (offsetProgress == 0) {
4649             // Don't bother calculating everything below if we won't offset anyway.
4650             return 0;
4651         }
4652 
4653         // First, get the position of the task relative to the midpoint. If there is no midpoint
4654         // then we just use the normal (centered) task position.
4655         RectF taskPosition = mTempRectF;
4656         // Whether the task should be shifted to start direction (i.e. left edge for portrait, top
4657         // edge for landscape/seascape).
4658         boolean isStartShift;
4659         if (midpointIndex > -1) {
4660             // When there is a midpoint reference task, adjacent tasks have less distance to travel
4661             // to reach offscreen. Offset the task position to the task's starting point, and offset
4662             // by current page's scroll diff.
4663             int midpointScroll = getScrollForPage(midpointIndex)
4664                     + getPagedOrientationHandler().getPrimaryScroll(this)
4665                     - getScrollForPage(mCurrentPage);
4666 
4667             getPersistentChildPosition(midpointIndex, midpointScroll, taskPosition);
4668             float midpointStart = getPagedOrientationHandler().getStart(taskPosition);
4669 
4670             getPersistentChildPosition(childIndex, midpointScroll, taskPosition);
4671             // Assume child does not overlap with midPointChild.
4672             isStartShift = getPagedOrientationHandler().getStart(taskPosition) < midpointStart;
4673         } else {
4674             // Position the task at scroll position.
4675             getPersistentChildPosition(childIndex, getScrollForPage(childIndex), taskPosition);
4676             isStartShift = mIsRtl;
4677         }
4678 
4679         // Next, calculate the distance to move the task off screen. We also need to account for
4680         // RecentsView scale, because it moves tasks based on its pivot. To do this, we move the
4681         // task position to where it would be offscreen at scale = 1 (computed above), then we
4682         // apply the scale via getMatrix() to determine how much that moves the task from its
4683         // desired position, and adjust the computed distance accordingly.
4684         float distanceToOffscreen;
4685         if (isStartShift) {
4686             float desiredStart = -getPagedOrientationHandler().getPrimarySize(taskPosition);
4687             distanceToOffscreen = -getPagedOrientationHandler().getEnd(taskPosition);
4688             if (mLastComputedTaskStartPushOutDistance == null) {
4689                 taskPosition.offsetTo(
4690                         getPagedOrientationHandler().getPrimaryValue(desiredStart, 0f),
4691                         getPagedOrientationHandler().getSecondaryValue(desiredStart, 0f));
4692                 getMatrix().mapRect(taskPosition);
4693                 mLastComputedTaskStartPushOutDistance = getPagedOrientationHandler().getEnd(
4694                         taskPosition) / getPagedOrientationHandler().getPrimaryScale(this);
4695             }
4696             distanceToOffscreen -= mLastComputedTaskStartPushOutDistance;
4697         } else {
4698             float desiredStart = getPagedOrientationHandler().getPrimarySize(this);
4699             distanceToOffscreen = desiredStart - getPagedOrientationHandler().getStart(
4700                     taskPosition);
4701             if (mLastComputedTaskEndPushOutDistance == null) {
4702                 taskPosition.offsetTo(
4703                         getPagedOrientationHandler().getPrimaryValue(desiredStart, 0f),
4704                         getPagedOrientationHandler().getSecondaryValue(desiredStart, 0f));
4705                 getMatrix().mapRect(taskPosition);
4706                 mLastComputedTaskEndPushOutDistance = (getPagedOrientationHandler().getStart(
4707                         taskPosition) - desiredStart)
4708                         / getPagedOrientationHandler().getPrimaryScale(this);
4709             }
4710             distanceToOffscreen -= mLastComputedTaskEndPushOutDistance;
4711         }
4712         return distanceToOffscreen * offsetProgress;
4713     }
4714 
4715     /**
4716      * Computes the vertical distance to offset a given child such that it is completely offscreen.
4717      *
4718      * @param offsetProgress From 0 to 1 where 0 means no offset and 1 means offset offscreen.
4719      */
4720     private float getVerticalOffsetSize(int childIndex, float offsetProgress) {
4721         if (offsetProgress == 0 || !(showAsGrid() && enableGridOnlyOverview())
4722                 || mSelectedTask == null) {
4723             // Don't bother calculating everything below if we won't offset vertically.
4724             return 0;
4725         }
4726 
4727         // First, get the position of the task relative to the top row.
4728         TaskView child = getTaskViewAt(childIndex);
4729         Rect taskPosition = getTaskBounds(child);
4730 
4731         boolean isSelectedTaskTopRow = mTopRowIdSet.contains(mSelectedTask.getTaskViewId());
4732         boolean isChildTopRow = mTopRowIdSet.contains(child.getTaskViewId());
4733         // Whether the task should be shifted to the top.
4734         boolean isTopShift = !isSelectedTaskTopRow && isChildTopRow;
4735         boolean isBottomShift = isSelectedTaskTopRow && !isChildTopRow;
4736 
4737         // Next, calculate the distance to move the task off screen at scale = 1.
4738         float distanceToOffscreen = 0;
4739         if (isTopShift) {
4740             distanceToOffscreen = -taskPosition.bottom;
4741         } else if (isBottomShift) {
4742             distanceToOffscreen = mContainer.getDeviceProfile().heightPx - taskPosition.top;
4743         }
4744         return distanceToOffscreen * offsetProgress;
4745     }
4746 
4747     protected void setTaskViewsResistanceTranslation(float translation) {
4748         mTaskViewsSecondaryTranslation = translation;
4749         for (int i = 0; i < getTaskViewCount(); i++) {
4750             TaskView task = requireTaskViewAt(i);
4751             task.getTaskResistanceTranslationProperty().set(task, translation / getScaleY());
4752         }
4753         runActionOnRemoteHandles(
4754                 remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
4755                         .recentsViewSecondaryTranslation.value = translation);
4756     }
4757 
4758     private void updateTaskViewsSnapshotRadius() {
4759         for (int i = 0; i < getTaskViewCount(); i++) {
4760             requireTaskViewAt(i).updateSnapshotRadius();
4761         }
4762     }
4763 
4764     protected void setTaskViewsPrimarySplitTranslation(float translation) {
4765         mTaskViewsPrimarySplitTranslation = translation;
4766         for (int i = 0; i < getTaskViewCount(); i++) {
4767             TaskView task = requireTaskViewAt(i);
4768             task.getPrimarySplitTranslationProperty().set(task, translation);
4769         }
4770     }
4771 
4772     protected void setTaskViewsSecondarySplitTranslation(float translation) {
4773         mTaskViewsSecondarySplitTranslation = translation;
4774         for (int i = 0; i < getTaskViewCount(); i++) {
4775             TaskView taskView = requireTaskViewAt(i);
4776             if (taskView == mSplitHiddenTaskView && !taskView.containsMultipleTasks()) {
4777                 continue;
4778             }
4779             taskView.getSecondarySplitTranslationProperty().set(taskView, translation);
4780         }
4781     }
4782 
4783     /**
4784      * Resets the visuals when exit modal state.
4785      */
4786     public void resetModalVisuals() {
4787         if (mSelectedTask != null) {
4788             mSelectedTask.taskContainers.forEach(
4789                     taskContainer -> taskContainer.getOverlay().resetModalVisuals());
4790         }
4791     }
4792 
4793     /**
4794      * Primarily used by overview actions to initiate split from focused task, logs the source
4795      * of split invocation as such.
4796      */
4797     public void initiateSplitSelect(TaskView taskView) {
4798         int defaultSplitPosition = getPagedOrientationHandler()
4799                 .getDefaultSplitPosition(mContainer.getDeviceProfile());
4800         initiateSplitSelect(taskView, defaultSplitPosition, LAUNCHER_OVERVIEW_ACTIONS_SPLIT);
4801     }
4802 
4803     /** TODO(b/266477929): Consolidate this call w/ the one below */
4804     public void initiateSplitSelect(TaskView taskView, @StagePosition int stagePosition,
4805             StatsLogManager.EventEnum splitEvent) {
4806         mSplitHiddenTaskView = taskView;
4807         mSplitSelectStateController.setInitialTaskSelect(null /*intent*/, stagePosition,
4808                 taskView.getFirstItemInfo(), splitEvent, taskView.getFirstTask().key.id);
4809         mSplitSelectStateController.setAnimateCurrentTaskDismissal(
4810                 true /*animateCurrentTaskDismissal*/);
4811         mSplitHiddenTaskViewIndex = indexOfChild(taskView);
4812         updateDesktopTaskVisibility(false /* visible */);
4813     }
4814 
4815     /**
4816      * Called when staging a split from Home/AllApps/Overview (Taskbar),
4817      * using the icon long-press menu.
4818      * Attempts to initiate split with an existing taskView, if one exists
4819      */
4820     public void initiateSplitSelect(SplitSelectSource splitSelectSource) {
4821         TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "enterSplitSelect");
4822         mSplitSelectSource = splitSelectSource;
4823         mSplitHiddenTaskView = getTaskViewByTaskId(splitSelectSource.alreadyRunningTaskId);
4824         mSplitHiddenTaskViewIndex = indexOfChild(mSplitHiddenTaskView);
4825         mSplitSelectStateController
4826                 .setAnimateCurrentTaskDismissal(splitSelectSource.animateCurrentTaskDismissal);
4827 
4828         // Prevent dismissing whole task if we're only initiating from one of 2 tasks in split pair
4829         mSplitSelectStateController.setDismissingFromSplitPair(mSplitHiddenTaskView != null
4830                 && mSplitHiddenTaskView instanceof GroupedTaskView);
4831         mSplitSelectStateController.setInitialTaskSelect(splitSelectSource.intent,
4832                 splitSelectSource.position.stagePosition, splitSelectSource.itemInfo,
4833                 splitSelectSource.splitEvent, splitSelectSource.alreadyRunningTaskId);
4834         updateDesktopTaskVisibility(false /* visible */);
4835     }
4836 
4837     private void updateDesktopTaskVisibility(boolean visible) {
4838         if (mDesktopTaskView != null) {
4839             mDesktopTaskView.setVisibility(visible ? VISIBLE : GONE);
4840         }
4841     }
4842 
4843     /**
4844      * Modifies a PendingAnimation with the animations for entering split staging
4845      */
4846     public void createSplitSelectInitAnimation(PendingAnimation builder, int duration) {
4847         boolean isInitiatingSplitFromTaskView =
4848                 mSplitSelectStateController.isAnimateCurrentTaskDismissal();
4849         boolean isInitiatingTaskViewSplitPair =
4850                 mSplitSelectStateController.isDismissingFromSplitPair();
4851         if (isInitiatingSplitFromTaskView && isInitiatingTaskViewSplitPair
4852                 && mSplitHiddenTaskView instanceof GroupedTaskView) {
4853             // Splitting from Overview for split pair task
4854             createInitialSplitSelectAnimation(builder);
4855 
4856             // Animate pair thumbnail into full thumbnail
4857             boolean primaryTaskSelected = mSplitHiddenTaskView.getTaskIds()[0]
4858                     == mSplitSelectStateController.getInitialTaskId();
4859             TaskContainer taskContainer = mSplitHiddenTaskView
4860                     .getTaskContainers().get(primaryTaskSelected ? 1 : 0);
4861             TaskThumbnailViewDeprecated thumbnail = taskContainer.getThumbnailViewDeprecated();
4862             mSplitSelectStateController.getSplitAnimationController()
4863                     .addInitialSplitFromPair(taskContainer, builder,
4864                             mContainer.getDeviceProfile(),
4865                             mSplitHiddenTaskView.getWidth(), mSplitHiddenTaskView.getHeight(),
4866                             primaryTaskSelected);
4867             builder.addOnFrameCallback(() ->{
4868                 thumbnail.refreshSplashView();
4869                 mSplitHiddenTaskView.updateSnapshotRadius();
4870             });
4871         } else if (isInitiatingSplitFromTaskView) {
4872             // Splitting from Overview for fullscreen task
4873             createTaskDismissAnimation(builder, mSplitHiddenTaskView, true, false, duration,
4874                     true /* dismissingForSplitSelection*/);
4875         } else {
4876             // Splitting from Home
4877             createInitialSplitSelectAnimation(builder);
4878         }
4879     }
4880 
4881     /**
4882      * Confirms the selection of the next split task. The extra data is passed through because the
4883      * user may be selecting a subtask in a group.
4884      *
4885      * @param containerTaskView If our second selected app is currently running in Recents, this is
4886      *                          the "container" TaskView from Recents. If we are starting a fresh
4887      *                          instance of the app from an Intent, this will be null.
4888      * @param task The Task corresponding to our second selected app. If we are starting a fresh
4889      *             instance of the app from an Intent, this will be null.
4890      * @param drawable The Drawable corresponding to our second selected app's icon.
4891      * @param secondView The View representing the current space on the screen where the second app
4892      *                   is (either the ThumbnailView or the tapped icon).
4893      * @param intent If we are launching a fresh instance of the app, this is the Intent for it. If
4894      *               the second app is already running in Recents, this will be null.
4895      * @param user If we are launching a fresh instance of the app, this is the UserHandle for it.
4896      *             If the second app is already running in Recents, this will be null.
4897      * @return true if waiting for confirmation of second app or if split animations are running,
4898      *          false otherwise
4899      */
4900     public boolean confirmSplitSelect(TaskView containerTaskView, Task task, Drawable drawable,
4901             View secondView, @Nullable Bitmap thumbnail, Intent intent, UserHandle user,
4902             ItemInfo itemInfo) {
4903         if (canLaunchFullscreenTask()) {
4904             return false;
4905         }
4906         if (mSplitSelectStateController.isBothSplitAppsConfirmed()) {
4907             Log.w(TAG, splitFailureMessage(
4908                     "confirmSplitSelect", "both apps have already been set"));
4909             return true;
4910         }
4911         // Second task is selected either as an already-running Task or an Intent
4912         if (task != null) {
4913             if (!task.isDockable) {
4914                 // Task does not support split screen
4915                 mSplitUnsupportedToast.show();
4916                 Log.w(TAG, splitFailureMessage("confirmSplitSelect",
4917                         "selected Task (" + task.key.getPackageName()
4918                                 + ") is not dockable / does not support splitscreen"));
4919                 return true;
4920             }
4921             mSplitSelectStateController.setSecondTask(task, itemInfo);
4922         } else {
4923             mSplitSelectStateController.setSecondTask(intent, user, itemInfo);
4924         }
4925 
4926         RectF secondTaskStartingBounds = new RectF();
4927         Rect secondTaskEndingBounds = new Rect();
4928         // TODO(194414938) starting bounds seem slightly off, investigate
4929         Rect firstTaskStartingBounds = new Rect();
4930         Rect firstTaskEndingBounds = mTempRect;
4931 
4932         boolean isTablet = mContainer.getDeviceProfile().isTablet;
4933         SplitAnimationTimings timings = AnimUtils.getDeviceSplitToConfirmTimings(isTablet);
4934         PendingAnimation pendingAnimation = new PendingAnimation(timings.getDuration());
4935 
4936         int halfDividerSize = getResources()
4937                 .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2;
4938         getPagedOrientationHandler().getFinalSplitPlaceholderBounds(halfDividerSize,
4939                 mContainer.getDeviceProfile(),
4940                 mSplitSelectStateController.getActiveSplitStagePosition(), firstTaskEndingBounds,
4941                 secondTaskEndingBounds);
4942 
4943         mSplitScrim = mSplitSelectStateController.getSplitAnimationController()
4944                 .addScrimBehindAnim(pendingAnimation, mContainer, getContext());
4945         FloatingTaskView firstFloatingTaskView =
4946                 mSplitSelectStateController.getFirstFloatingTaskView();
4947         firstFloatingTaskView.getBoundsOnScreen(firstTaskStartingBounds);
4948         firstFloatingTaskView.addConfirmAnimation(pendingAnimation,
4949                 new RectF(firstTaskStartingBounds), firstTaskEndingBounds,
4950                 false /* fadeWithThumbnail */, true /* isStagedTask */);
4951 
4952         safeRemoveDragLayerView(mSecondFloatingTaskView);
4953 
4954         mSecondFloatingTaskView = FloatingTaskView.getFloatingTaskView(mContainer, secondView,
4955                 thumbnail, drawable, secondTaskStartingBounds);
4956         mSecondFloatingTaskView.setAlpha(1);
4957         mSecondFloatingTaskView.addConfirmAnimation(pendingAnimation, secondTaskStartingBounds,
4958                 secondTaskEndingBounds, true /* fadeWithThumbnail */, false /* isStagedTask */);
4959 
4960         pendingAnimation.setViewAlpha(mSplitSelectStateController.getSplitInstructionsView(), 0,
4961                 clampToProgress(LINEAR, timings.getInstructionsFadeStartOffset(),
4962                         timings.getInstructionsFadeEndOffset()));
4963 
4964         pendingAnimation.addEndListener(aBoolean -> {
4965             mSplitSelectStateController.launchSplitTasks(
4966                     aBoolean1 -> {
4967                         if (FeatureFlags.enableSplitContextually()) {
4968                             mSplitSelectStateController.resetState();
4969                         } else {
4970                             resetFromSplitSelectionState();
4971                         }
4972                         InteractionJankMonitorWrapper.end(Cuj.CUJ_SPLIT_SCREEN_ENTER);
4973                     });
4974         });
4975 
4976         mSecondSplitHiddenView = containerTaskView;
4977         if (mSecondSplitHiddenView != null) {
4978             mSecondSplitHiddenView.setThumbnailVisibility(INVISIBLE,
4979                     mSplitSelectStateController.getSecondTaskId());
4980         }
4981 
4982         InteractionJankMonitorWrapper.begin(this, Cuj.CUJ_SPLIT_SCREEN_ENTER,
4983                 "Second tile selected");
4984 
4985         // Fade out all other views underneath placeholders
4986         ObjectAnimator tvFade = ObjectAnimator.ofFloat(this, RecentsView.CONTENT_ALPHA,1, 0);
4987         pendingAnimation.add(tvFade, DECELERATE_2, SpringProperty.DEFAULT);
4988         pendingAnimation.buildAnim().start();
4989         return true;
4990     }
4991 
4992     @SuppressLint("WrongCall")
4993     protected void resetFromSplitSelectionState() {
4994         if (mSplitSelectSource != null || mSplitHiddenTaskViewIndex != -1 ||
4995                 FeatureFlags.enableSplitContextually()) {
4996             safeRemoveDragLayerView(mSplitSelectStateController.getFirstFloatingTaskView());
4997             safeRemoveDragLayerView(mSecondFloatingTaskView);
4998             safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView());
4999             safeRemoveDragLayerView(mSplitScrim);
5000             mSecondFloatingTaskView = null;
5001             mSplitSelectSource = null;
5002             mSplitSelectStateController.getSplitAnimationController()
5003                     .removeSplitInstructionsView(mContainer);
5004         }
5005 
5006         if (mSecondSplitHiddenView != null) {
5007             mSecondSplitHiddenView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID);
5008             mSecondSplitHiddenView = null;
5009         }
5010 
5011         // We are leaving split selection state, so it is safe to reset thumbnail translations for
5012         // the next time split is invoked.
5013         setTaskViewsPrimarySplitTranslation(0);
5014         setTaskViewsSecondarySplitTranslation(0);
5015 
5016         if (!FeatureFlags.enableSplitContextually()) {
5017             // When flag is on, this method gets called from resetState() call below, let's avoid
5018             // infinite recursion today
5019             mSplitSelectStateController.resetState();
5020         }
5021         if (mSplitHiddenTaskViewIndex == -1) {
5022             return;
5023         }
5024         if (!mContainer.getDeviceProfile().isTablet) {
5025             int pageToSnapTo = mCurrentPage;
5026             if (mSplitHiddenTaskViewIndex <= pageToSnapTo) {
5027                 pageToSnapTo += 1;
5028             } else {
5029                 pageToSnapTo = mSplitHiddenTaskViewIndex;
5030             }
5031             snapToPageImmediately(pageToSnapTo);
5032         }
5033         onLayout(false /*  changed */, getLeft(), getTop(), getRight(), getBottom());
5034 
5035         resetTaskVisuals();
5036         mSplitHiddenTaskViewIndex = -1;
5037         if (mSplitHiddenTaskView != null) {
5038             mSplitHiddenTaskView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID);
5039             mSplitHiddenTaskView = null;
5040         }
5041         updateDesktopTaskVisibility(true /* visible */);
5042     }
5043 
5044     private void safeRemoveDragLayerView(@Nullable View viewToRemove) {
5045         if (viewToRemove != null) {
5046             mContainer.getDragLayer().removeView(viewToRemove);
5047         }
5048     }
5049 
5050     /**
5051      * Returns how much additional translation there should be for each of the child TaskViews.
5052      * Note that the translation can be its primary or secondary dimension.
5053      */
5054     public float getSplitSelectTranslation() {
5055         DeviceProfile deviceProfile = mContainer.getDeviceProfile();
5056         RecentsPagedOrientationHandler orientationHandler = getPagedOrientationHandler();
5057         int splitPosition = getSplitSelectController().getActiveSplitStagePosition();
5058         int splitPlaceholderSize =
5059                 mContainer.getResources().getDimensionPixelSize(R.dimen.split_placeholder_size);
5060         int direction = orientationHandler.getSplitTranslationDirectionFactor(
5061                 splitPosition, deviceProfile);
5062 
5063         if (deviceProfile.isTablet && deviceProfile.isLeftRightSplit) {
5064             // Only shift TaskViews if there is not enough space on the side of
5065             // mLastComputedTaskSize to minimize motion.
5066             int sideSpace = mIsRtl
5067                     ? deviceProfile.widthPx - mLastComputedTaskSize.right
5068                     : mLastComputedTaskSize.left;
5069             int extraSpace = splitPlaceholderSize + mPageSpacing - sideSpace;
5070             if (extraSpace <= 0f) {
5071                 return 0f;
5072             }
5073 
5074             return extraSpace * direction;
5075         }
5076 
5077         return splitPlaceholderSize * direction;
5078     }
5079 
5080     protected void onRotateInSplitSelectionState() {
5081         getPagedOrientationHandler().getInitialSplitPlaceholderBounds(mSplitPlaceholderSize,
5082                 mSplitPlaceholderInset, mContainer.getDeviceProfile(),
5083                 mSplitSelectStateController.getActiveSplitStagePosition(), mTempRect);
5084         mTempRectF.set(mTempRect);
5085         FloatingTaskView firstFloatingTaskView =
5086                 mSplitSelectStateController.getFirstFloatingTaskView();
5087         firstFloatingTaskView.updateOrientationHandler(getPagedOrientationHandler());
5088         firstFloatingTaskView.update(mTempRectF, /*progress=*/1f);
5089 
5090         RecentsPagedOrientationHandler orientationHandler = getPagedOrientationHandler();
5091         Pair<FloatProperty<RecentsView>, FloatProperty<RecentsView>> taskViewsFloat =
5092                 orientationHandler.getSplitSelectTaskOffset(
5093                         TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
5094                         mContainer.getDeviceProfile());
5095         taskViewsFloat.first.set(this, getSplitSelectTranslation());
5096         taskViewsFloat.second.set(this, 0f);
5097 
5098         if (mSplitSelectStateController.getSplitInstructionsView() != null) {
5099             mSplitSelectStateController.getSplitInstructionsView().ensureProperRotation();
5100         }
5101     }
5102 
5103     private void updateDeadZoneRects() {
5104         // Get the deadzone rect surrounding the clear all button to not dismiss overview to home
5105         mClearAllButtonDeadZoneRect.setEmpty();
5106         if (mClearAllButton.getWidth() > 0) {
5107             int verticalMargin = getResources()
5108                     .getDimensionPixelSize(R.dimen.recents_clear_all_deadzone_vertical_margin);
5109             mClearAllButton.getHitRect(mClearAllButtonDeadZoneRect);
5110             mClearAllButtonDeadZoneRect.inset(-getPaddingRight() / 2, -verticalMargin);
5111         }
5112 
5113         // Get the deadzone rect between the task views
5114         mTaskViewDeadZoneRect.setEmpty();
5115         int count = getTaskViewCount();
5116         if (count > 0) {
5117             final View taskView = requireTaskViewAt(0);
5118             requireTaskViewAt(count - 1).getHitRect(mTaskViewDeadZoneRect);
5119             mTaskViewDeadZoneRect.union(taskView.getLeft(), taskView.getTop(), taskView.getRight(),
5120                     taskView.getBottom());
5121         }
5122     }
5123 
5124     private void updateEmptyStateUi(boolean sizeChanged) {
5125         boolean hasValidSize = getWidth() > 0 && getHeight() > 0;
5126         if (sizeChanged && hasValidSize) {
5127             mEmptyTextLayout = null;
5128             mLastMeasureSize.set(getWidth(), getHeight());
5129         }
5130 
5131         if (mShowEmptyMessage && hasValidSize && mEmptyTextLayout == null) {
5132             int availableWidth = mLastMeasureSize.x - mEmptyMessagePadding - mEmptyMessagePadding;
5133             mEmptyTextLayout = StaticLayout.Builder.obtain(mEmptyMessage, 0, mEmptyMessage.length(),
5134                     mEmptyMessagePaint, availableWidth)
5135                     .setAlignment(Layout.Alignment.ALIGN_CENTER)
5136                     .build();
5137             int totalHeight = mEmptyTextLayout.getHeight()
5138                     + mEmptyMessagePadding + mEmptyIcon.getIntrinsicHeight();
5139 
5140             int top = (mLastMeasureSize.y - totalHeight) / 2;
5141             int left = (mLastMeasureSize.x - mEmptyIcon.getIntrinsicWidth()) / 2;
5142             mEmptyIcon.setBounds(left, top, left + mEmptyIcon.getIntrinsicWidth(),
5143                     top + mEmptyIcon.getIntrinsicHeight());
5144         }
5145     }
5146 
5147     @Override
5148     protected boolean verifyDrawable(Drawable who) {
5149         return super.verifyDrawable(who) || (mShowEmptyMessage && who == mEmptyIcon);
5150     }
5151 
5152     protected void maybeDrawEmptyMessage(Canvas canvas) {
5153         if (mShowEmptyMessage && mEmptyTextLayout != null) {
5154             // Offsets icon and text up so that the vertical center of screen (accounting for
5155             // insets) is between icon and text.
5156             int offset = (mEmptyIcon.getIntrinsicHeight() + mEmptyMessagePadding) / 2;
5157 
5158             canvas.save();
5159             canvas.translate(getScrollX() + (mInsets.left - mInsets.right) / 2f,
5160                     (mInsets.top - mInsets.bottom) / 2f - offset);
5161             mEmptyIcon.draw(canvas);
5162             canvas.translate(mEmptyMessagePadding,
5163                     mEmptyIcon.getBounds().bottom + mEmptyMessagePadding);
5164             mEmptyTextLayout.draw(canvas);
5165             canvas.restore();
5166         }
5167     }
5168 
5169     /**
5170      * Animate adjacent tasks off screen while scaling up.
5171      *
5172      * If launching one of the adjacent tasks, parallax the center task and other adjacent task
5173      * to the right.
5174      */
5175     @SuppressLint("Recycle")
5176     public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv) {
5177         AnimatorSet anim = new AnimatorSet();
5178 
5179         int taskIndex = indexOfChild(tv);
5180         int centerTaskIndex = getCurrentPage();
5181 
5182         float toScale = getMaxScaleForFullScreen();
5183         boolean showAsGrid = showAsGrid();
5184         boolean launchingCenterTask = showAsGrid
5185                 ? tv.isFocusedTask() && isTaskViewFullyVisible(tv)
5186                 : taskIndex == centerTaskIndex;
5187         if (launchingCenterTask) {
5188             anim.play(ObjectAnimator.ofFloat(this, RECENTS_SCALE_PROPERTY, toScale));
5189             anim.play(ObjectAnimator.ofFloat(this, FULLSCREEN_PROGRESS, 1));
5190         } else if (!showAsGrid) {
5191             // We are launching an adjacent task, so parallax the center and other adjacent task.
5192             float displacementX = tv.getWidth() * (toScale - 1f);
5193             float primaryTranslation = mIsRtl ? -displacementX : displacementX;
5194             anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex),
5195                     getPagedOrientationHandler().getPrimaryViewTranslate(), primaryTranslation));
5196             int runningTaskIndex = getRunningTaskIndex();
5197             if (runningTaskIndex != -1 && runningTaskIndex != taskIndex
5198                     && getRemoteTargetHandles() != null) {
5199                 for (RemoteTargetHandle remoteHandle : getRemoteTargetHandles()) {
5200                     anim.play(ObjectAnimator.ofFloat(
5201                             remoteHandle.getTaskViewSimulator().taskPrimaryTranslation,
5202                             AnimatedFloat.VALUE,
5203                             primaryTranslation));
5204                 }
5205             }
5206 
5207             int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - taskIndex);
5208             if (otherAdjacentTaskIndex >= 0 && otherAdjacentTaskIndex < getPageCount()) {
5209                 PropertyValuesHolder[] properties = new PropertyValuesHolder[3];
5210                 properties[0] = PropertyValuesHolder.ofFloat(
5211                         getPagedOrientationHandler().getPrimaryViewTranslate(), primaryTranslation);
5212                 properties[1] = PropertyValuesHolder.ofFloat(View.SCALE_X, 1);
5213                 properties[2] = PropertyValuesHolder.ofFloat(View.SCALE_Y, 1);
5214 
5215                 anim.play(ObjectAnimator.ofPropertyValuesHolder(getPageAt(otherAdjacentTaskIndex),
5216                         properties));
5217             }
5218         }
5219         anim.play(ObjectAnimator.ofFloat(this, TASK_THUMBNAIL_SPLASH_ALPHA, 0, 1));
5220         return anim;
5221     }
5222 
5223     /**
5224      * Returns the scale up required on the view, so that it coves the screen completely
5225      */
5226     public float getMaxScaleForFullScreen() {
5227         if (enableGridOnlyOverview() && mContainer.getDeviceProfile().isTablet
5228                 && !mOverviewGridEnabled) {
5229             if (mLastComputedCarouselTaskSize.isEmpty()) {
5230                 mSizeStrategy.calculateCarouselTaskSize(mContainer, mContainer.getDeviceProfile(),
5231                         mLastComputedCarouselTaskSize, getPagedOrientationHandler());
5232             }
5233             mTempRect.set(mLastComputedCarouselTaskSize);
5234         } else {
5235             if (mLastComputedTaskSize.isEmpty()) {
5236                 getTaskSize(mLastComputedTaskSize);
5237             }
5238             mTempRect.set(mLastComputedTaskSize);
5239         }
5240         return getPagedViewOrientedState().getFullScreenScaleAndPivot(
5241                 mTempRect, mContainer.getDeviceProfile(), mTempPointF);
5242     }
5243 
5244     public PendingAnimation createTaskLaunchAnimation(
5245             TaskView tv, long duration, Interpolator interpolator) {
5246         if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) {
5247             throw new IllegalStateException("Another pending animation is still running");
5248         }
5249 
5250         int count = getTaskViewCount();
5251         if (count == 0) {
5252             return new PendingAnimation(duration);
5253         }
5254 
5255         // When swiping down from overview to tasks, ensures the snapped page's scroll maintain
5256         // invariant between quick switch and overview, to ensure a smooth animation transition.
5257         updateGridProperties();
5258         updateScrollSynchronously();
5259 
5260         int targetSysUiFlags = tv.getFirstThumbnailViewDeprecated().getSysUiStatusNavFlags();
5261         final boolean[] passedOverviewThreshold = new boolean[] {false};
5262         ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1);
5263         progressAnim.addUpdateListener(animator -> {
5264             // Once we pass a certain threshold, update the sysui flags to match the target
5265             // tasks' flags
5266             if (animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD) {
5267                 mContainer.getSystemUiController().updateUiState(
5268                         UI_STATE_FULLSCREEN_TASK, targetSysUiFlags);
5269             } else {
5270                 mContainer.getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, 0);
5271             }
5272 
5273             // Passing the threshold from taskview to fullscreen app will vibrate
5274             final boolean passed = animator.getAnimatedFraction() >=
5275                     SUCCESS_TRANSITION_PROGRESS;
5276             if (passed != passedOverviewThreshold[0]) {
5277                 passedOverviewThreshold[0] = passed;
5278                 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
5279                         HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
5280                 // Also update recents animation controller state if it is ongoing.
5281                 if (mRecentsAnimationController != null) {
5282                     mRecentsAnimationController.setWillFinishToHome(!passed);
5283                 }
5284             }
5285         });
5286 
5287         AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv);
5288 
5289         DepthController depthController = getDepthController();
5290         if (depthController != null) {
5291             ObjectAnimator depthAnimator = ObjectAnimator.ofFloat(depthController.stateDepth,
5292                     MULTI_PROPERTY_VALUE, BACKGROUND_APP.getDepth(mContainer));
5293             anim.play(depthAnimator);
5294         }
ObjectAnimator.ofFloat(this, TASK_THUMBNAIL_SPLASH_ALPHA, 0f, 1f)5295         anim.play(ObjectAnimator.ofFloat(this, TASK_THUMBNAIL_SPLASH_ALPHA, 0f, 1f));
5296 
anim.play(progressAnim)5297         anim.play(progressAnim);
anim.setInterpolator(interpolator)5298         anim.setInterpolator(interpolator);
5299 
5300         mPendingAnimation = new PendingAnimation(duration);
mPendingAnimation.add(anim)5301         mPendingAnimation.add(anim);
5302         runActionOnRemoteHandles(
5303                 remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
5304                         .addOverviewToAppAnim(mPendingAnimation, interpolator));
mPendingAnimation.addOnFrameCallback(this::redrawLiveTile)5305         mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
5306         mPendingAnimation.addEndListener(isSuccess -> {
5307             if (isSuccess) {
5308                 if (tv instanceof GroupedTaskView && hasAllValidTaskIds(tv.getTaskIds())
5309                         && mRemoteTargetHandles != null) {
5310                     // TODO(b/194414938): make this part of the animations instead.
5311                     TaskViewUtils.createSplitAuxiliarySurfacesAnimator(
5312                             mRemoteTargetHandles[0].getTransformParams().getTargetSet().nonApps,
5313                             true /*shown*/, (dividerAnimator) -> {
5314                                 dividerAnimator.start();
5315                                 dividerAnimator.end();
5316                             });
5317                 }
5318                 if (tv.isRunningTask()) {
5319                     finishRecentsAnimation(false /* toRecents */, null);
5320                     onTaskLaunchAnimationEnd(true /* success */);
5321                 } else {
5322                     tv.launchTask(this::onTaskLaunchAnimationEnd);
5323                 }
5324                 mContainer.getStatsLogManager().logger().withItemInfo(tv.getFirstItemInfo())
5325                         .log(LAUNCHER_TASK_LAUNCH_SWIPE_DOWN);
5326             } else {
5327                 onTaskLaunchAnimationEnd(false);
5328             }
5329             mPendingAnimation = null;
5330         });
5331         return mPendingAnimation;
5332     }
5333 
onTaskLaunchAnimationEnd(boolean success)5334     protected Unit onTaskLaunchAnimationEnd(boolean success) {
5335         if (success) {
5336             resetTaskVisuals();
5337         }
5338         return Unit.INSTANCE;
5339     }
5340 
5341     @Override
notifyPageSwitchListener(int prevPage)5342     protected void notifyPageSwitchListener(int prevPage) {
5343         super.notifyPageSwitchListener(prevPage);
5344         updateCurrentTaskActionsVisibility();
5345         loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
5346         updateEnabledOverlays();
5347     }
5348 
5349     @Override
getCurrentPageDescription()5350     protected String getCurrentPageDescription() {
5351         return "";
5352     }
5353 
5354     @Override
addChildrenForAccessibility(ArrayList<View> outChildren)5355     public void addChildrenForAccessibility(ArrayList<View> outChildren) {
5356         // Add children in reverse order
5357         for (int i = getChildCount() - 1; i >= 0; --i) {
5358             outChildren.add(getChildAt(i));
5359         }
5360     }
5361 
5362     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)5363     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
5364         super.onInitializeAccessibilityNodeInfo(info);
5365         final AccessibilityNodeInfo.CollectionInfo
5366                 collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain(
5367                 1, getTaskViewCount(), false,
5368                 AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE);
5369         info.setCollectionInfo(collectionInfo);
5370     }
5371 
5372     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)5373     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
5374         super.onInitializeAccessibilityEvent(event);
5375 
5376         final int taskViewCount = getTaskViewCount();
5377         event.setScrollable(taskViewCount > 0);
5378 
5379         if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
5380             final int[] visibleTasks = getVisibleChildrenRange();
5381             event.setFromIndex(taskViewCount - visibleTasks[1]);
5382             event.setToIndex(taskViewCount - visibleTasks[0]);
5383             event.setItemCount(taskViewCount);
5384         }
5385     }
5386 
5387     @Override
getAccessibilityClassName()5388     public CharSequence getAccessibilityClassName() {
5389         // To hear position-in-list related feedback from Talkback.
5390         return ListView.class.getName();
5391     }
5392 
5393     @Override
isPageOrderFlipped()5394     protected boolean isPageOrderFlipped() {
5395         return true;
5396     }
5397 
setEnableDrawingLiveTile(boolean enableDrawingLiveTile)5398     public void setEnableDrawingLiveTile(boolean enableDrawingLiveTile) {
5399         mEnableDrawingLiveTile = enableDrawingLiveTile;
5400     }
5401 
redrawLiveTile()5402     public void redrawLiveTile() {
5403         runActionOnRemoteHandles(remoteTargetHandle -> {
5404             TransformParams params = remoteTargetHandle.getTransformParams();
5405             if (params.getTargetSet() != null) {
5406                 remoteTargetHandle.getTaskViewSimulator().apply(params);
5407             }
5408         });
5409     }
5410 
getRemoteTargetHandles()5411     public RemoteTargetHandle[] getRemoteTargetHandles() {
5412         return mRemoteTargetHandles;
5413     }
5414 
5415     // TODO: To be removed in a follow up CL
setRecentsAnimationTargets(RecentsAnimationController recentsAnimationController, RecentsAnimationTargets recentsAnimationTargets)5416     public void setRecentsAnimationTargets(RecentsAnimationController recentsAnimationController,
5417             RecentsAnimationTargets recentsAnimationTargets) {
5418         Log.d(TAG, "setRecentsAnimationTargets "
5419                 + "- recentsAnimationController: " + recentsAnimationController
5420                 + ", recentsAnimationTargets: " + recentsAnimationTargets);
5421         mRecentsAnimationController = recentsAnimationController;
5422         mSplitSelectStateController.setRecentsAnimationRunning(true);
5423         if (recentsAnimationTargets == null || recentsAnimationTargets.apps.length == 0) {
5424             return;
5425         }
5426 
5427         RemoteTargetGluer gluer;
5428         if (recentsAnimationTargets.hasDesktopTasks()) {
5429             gluer = new RemoteTargetGluer(getContext(), getSizeStrategy(), recentsAnimationTargets,
5430                     true /* forDesktop */);
5431             mRemoteTargetHandles = gluer.assignTargetsForDesktop(recentsAnimationTargets);
5432         } else {
5433             gluer = new RemoteTargetGluer(getContext(), getSizeStrategy(), recentsAnimationTargets,
5434                     false);
5435             mRemoteTargetHandles = gluer.assignTargetsForSplitScreen(recentsAnimationTargets);
5436         }
5437         mSplitBoundsConfig = gluer.getSplitBounds();
5438         // Add release check to the targets from the RemoteTargetGluer and not the targets
5439         // passed in because in the event we're in split screen, we use the passed in targets
5440         // to create new RemoteAnimationTargets in assignTargetsForSplitScreen(), and the
5441         // mSyncTransactionApplier doesn't get transferred over
5442         runActionOnRemoteHandles(remoteTargetHandle -> {
5443             final TransformParams params = remoteTargetHandle.getTransformParams();
5444             if (mSyncTransactionApplier != null) {
5445                 params.setSyncTransactionApplier(mSyncTransactionApplier);
5446                 params.getTargetSet().addReleaseCheck(mSyncTransactionApplier);
5447             }
5448 
5449             TaskViewSimulator tvs = remoteTargetHandle.getTaskViewSimulator();
5450             tvs.setOrientationState(mOrientationState);
5451             tvs.setDp(mContainer.getDeviceProfile());
5452             tvs.recentsViewScale.value = 1;
5453         });
5454 
5455         TaskView runningTaskView = getRunningTaskView();
5456         if (runningTaskView instanceof GroupedTaskView) {
5457             // We initially create a GroupedTaskView in showCurrentTask() before launcher even
5458             // receives the leashes for the remote apps, so the mSplitBoundsConfig that gets passed
5459             // in there is either null or outdated, so we need to update here as soon as we're
5460             // notified.
5461             ((GroupedTaskView) runningTaskView).updateSplitBoundsConfig(mSplitBoundsConfig);
5462         }
5463     }
5464 
5465     /** Helper to avoid writing some for-loops to iterate over {@link #mRemoteTargetHandles} */
runActionOnRemoteHandles(Consumer<RemoteTargetHandle> consumer)5466     public void runActionOnRemoteHandles(Consumer<RemoteTargetHandle> consumer) {
5467         if (mRemoteTargetHandles == null) {
5468             return;
5469         }
5470 
5471         for (RemoteTargetHandle handle : mRemoteTargetHandles) {
5472             consumer.accept(handle);
5473         }
5474     }
5475 
5476     /**
5477      * Finish recents animation.
5478      */
finishRecentsAnimation(boolean toRecents, @Nullable Runnable onFinishComplete)5479     public void finishRecentsAnimation(boolean toRecents, @Nullable Runnable onFinishComplete) {
5480         finishRecentsAnimation(toRecents, true /* shouldPip */, onFinishComplete);
5481     }
5482 
5483     /**
5484      * NOTE: Whatever value gets passed through to the toRecents param may need to also be set on
5485      * {@link #mRecentsAnimationController#setWillFinishToHome}.
5486      */
finishRecentsAnimation(boolean toRecents, boolean shouldPip, @Nullable Runnable onFinishComplete)5487     public void finishRecentsAnimation(boolean toRecents, boolean shouldPip,
5488             @Nullable Runnable onFinishComplete) {
5489         Log.d(TAG, "finishRecentsAnimation - mRecentsAnimationController: "
5490                 + mRecentsAnimationController);
5491         // TODO(b/197232424#comment#10) Move this back into onRecentsAnimationComplete(). Maybe?
5492         cleanupRemoteTargets();
5493 
5494         if (mRecentsAnimationController == null) {
5495             if (onFinishComplete != null) {
5496                 onFinishComplete.run();
5497             }
5498             return;
5499         }
5500 
5501         final boolean sendUserLeaveHint = toRecents && shouldPip;
5502         if (sendUserLeaveHint) {
5503             // Notify the SysUI to use fade-in animation when entering PiP from live tile.
5504             final SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(getContext());
5505             systemUiProxy.setPipAnimationTypeToAlpha();
5506             systemUiProxy.setShelfHeight(true, mContainer.getDeviceProfile().hotseatBarSizePx);
5507             // Transaction to hide the task to avoid flicker for entering PiP from split-screen.
5508             // See also {@link AbsSwipeUpHandler#maybeFinishSwipeToHome}.
5509             PictureInPictureSurfaceTransaction tx =
5510                     new PictureInPictureSurfaceTransaction.Builder()
5511                             .setAlpha(0f)
5512                             .build();
5513             tx.setShouldDisableCanAffectSystemUiFlags(false);
5514             int[] taskIds = TopTaskTracker.INSTANCE.get(getContext()).getRunningSplitTaskIds();
5515             for (int taskId : taskIds) {
5516                 mRecentsAnimationController.setFinishTaskTransaction(taskId,
5517                         tx, null /* overlay */);
5518             }
5519         }
5520         mRecentsAnimationController.finish(toRecents, () -> {
5521             if (onFinishComplete != null) {
5522                 onFinishComplete.run();
5523             }
5524             onRecentsAnimationComplete();
5525         }, sendUserLeaveHint);
5526     }
5527 
5528     /**
5529      * Called when a running recents animation has finished or canceled.
5530      */
onRecentsAnimationComplete()5531     public void onRecentsAnimationComplete() {
5532         Log.d(TAG, "onRecentsAnimationComplete "
5533                 + "- mRecentsAnimationController: " + mRecentsAnimationController
5534                 + ", mSideTaskLaunchCallback: " + mSideTaskLaunchCallback);
5535         // At this point, the recents animation is not running and if the animation was canceled
5536         // by a display rotation then reset this state to show the screenshot
5537         setRunningTaskViewShowScreenshot(true);
5538         // After we finish the recents animation, the current task id should be correctly
5539         // reset so that when the task is launched from Overview later, it goes through the
5540         // flow of starting a new task instead of finishing recents animation to app. A
5541         // typical example of this is (1) user swipes up from app to Overview (2) user
5542         // taps on QSB (3) user goes back to Overview and launch the most recent task.
5543         setCurrentTask(-1);
5544         mRecentsAnimationController = null;
5545         mSplitSelectStateController.setRecentsAnimationRunning(false);
5546         executeSideTaskLaunchCallback();
5547     }
5548 
setDisallowScrollToClearAll(boolean disallowScrollToClearAll)5549     public void setDisallowScrollToClearAll(boolean disallowScrollToClearAll) {
5550         if (mDisallowScrollToClearAll != disallowScrollToClearAll) {
5551             mDisallowScrollToClearAll = disallowScrollToClearAll;
5552             updateMinAndMaxScrollX();
5553         }
5554     }
5555 
5556     /**
5557      * Updates page scroll synchronously after measure and layout child views.
5558      */
5559     @SuppressLint("WrongCall")
updateScrollSynchronously()5560     public void updateScrollSynchronously() {
5561         // onMeasure is needed to update child's measured width which is used in scroll calculation,
5562         // in case TaskView sizes has changed when being focused/unfocused.
5563         onMeasure(makeMeasureSpec(getMeasuredWidth(), EXACTLY),
5564                 makeMeasureSpec(getMeasuredHeight(), EXACTLY));
5565         onLayout(false /*  changed */, getLeft(), getTop(), getRight(), getBottom());
5566         updateMinAndMaxScrollX();
5567     }
5568 
5569     @Override
getChildGap(int fromIndex, int toIndex)5570     protected int getChildGap(int fromIndex, int toIndex) {
5571         int clearAllIndex = indexOfChild(mClearAllButton);
5572         return fromIndex == clearAllIndex || toIndex == clearAllIndex
5573                 ? getClearAllExtraPageSpacing() : 0;
5574     }
5575 
getClearAllExtraPageSpacing()5576     protected int getClearAllExtraPageSpacing() {
5577         return showAsGrid()
5578                 ? Math.max(mContainer.getDeviceProfile().overviewGridSideMargin - mPageSpacing, 0)
5579                 : 0;
5580     }
5581 
5582     @Override
updateMinAndMaxScrollX()5583     protected void updateMinAndMaxScrollX() {
5584         super.updateMinAndMaxScrollX();
5585         if (DEBUG) {
5586             Log.d(TAG, "updateMinAndMaxScrollX - mMinScroll: " + mMinScroll);
5587             Log.d(TAG, "updateMinAndMaxScrollX - mMaxScroll: " + mMaxScroll);
5588         }
5589     }
5590 
5591     @Override
computeMinScroll()5592     protected int computeMinScroll() {
5593         if (getTaskViewCount() <= 0) {
5594             return super.computeMinScroll();
5595         }
5596 
5597         return getScrollForPage(mIsRtl ? getLastViewIndex() : getFirstViewIndex());
5598     }
5599 
5600     @Override
computeMaxScroll()5601     protected int computeMaxScroll() {
5602         if (getTaskViewCount() <= 0) {
5603             return super.computeMaxScroll();
5604         }
5605 
5606         return getScrollForPage(mIsRtl ? getFirstViewIndex() : getLastViewIndex());
5607     }
5608 
getFirstViewIndex()5609     private int getFirstViewIndex() {
5610         TaskView focusedTaskView = mShowAsGridLastOnLayout ? getFocusedTaskView() : null;
5611         return focusedTaskView != null ? indexOfChild(focusedTaskView) : 0;
5612     }
5613 
getLastViewIndex()5614     private int getLastViewIndex() {
5615         if (!mDisallowScrollToClearAll) {
5616             return indexOfChild(mClearAllButton);
5617         }
5618 
5619         if (!mShowAsGridLastOnLayout) {
5620             return getTaskViewCount() - 1;
5621         }
5622 
5623         TaskView lastGridTaskView = getLastGridTaskView();
5624         if (lastGridTaskView != null) {
5625             return indexOfChild(lastGridTaskView);
5626         }
5627 
5628         // Returns focus task if there are no grid tasks.
5629         return indexOfChild(getFocusedTaskView());
5630     }
5631 
5632     /**
5633      * Returns page scroll of ClearAllButton.
5634      */
getClearAllScroll()5635     public int getClearAllScroll() {
5636         return getScrollForPage(indexOfChild(mClearAllButton));
5637     }
5638 
5639     @Override
getPageScrolls(int[] outPageScrolls, boolean layoutChildren, ComputePageScrollsLogic scrollLogic)5640     protected boolean getPageScrolls(int[] outPageScrolls, boolean layoutChildren,
5641             ComputePageScrollsLogic scrollLogic) {
5642         int[] newPageScrolls = new int[outPageScrolls.length];
5643         super.getPageScrolls(newPageScrolls, layoutChildren, scrollLogic);
5644         boolean showAsFullscreen = showAsFullscreen();
5645         boolean showAsGrid = showAsGrid();
5646 
5647         // Align ClearAllButton to the left (RTL) or right (non-RTL), which is different from other
5648         // TaskViews. This must be called after laying out ClearAllButton.
5649         if (layoutChildren) {
5650             int clearAllWidthDiff = getPagedOrientationHandler().getPrimaryValue(mTaskWidth,
5651                     mTaskHeight) - getPagedOrientationHandler().getPrimarySize(mClearAllButton);
5652             mClearAllButton.setScrollOffsetPrimary(mIsRtl ? clearAllWidthDiff : -clearAllWidthDiff);
5653         }
5654 
5655         boolean pageScrollChanged = false;
5656 
5657         int clearAllIndex = indexOfChild(mClearAllButton);
5658         int clearAllScroll = 0;
5659         int clearAllWidth = getPagedOrientationHandler().getPrimarySize(mClearAllButton);
5660         if (clearAllIndex != -1 && clearAllIndex < outPageScrolls.length) {
5661             float scrollDiff = mClearAllButton.getScrollAdjustment(showAsFullscreen, showAsGrid);
5662             clearAllScroll = newPageScrolls[clearAllIndex] + (int) scrollDiff;
5663             if (outPageScrolls[clearAllIndex] != clearAllScroll) {
5664                 pageScrollChanged = true;
5665                 outPageScrolls[clearAllIndex] = clearAllScroll;
5666             }
5667         }
5668 
5669         final int taskCount = getTaskViewCount();
5670         int lastTaskScroll = getLastTaskScroll(clearAllScroll, clearAllWidth);
5671         for (int i = 0; i < taskCount; i++) {
5672             TaskView taskView = requireTaskViewAt(i);
5673             float scrollDiff = taskView.getScrollAdjustment(showAsGrid);
5674             int pageScroll = newPageScrolls[i] + Math.round(scrollDiff);
5675             if ((mIsRtl && pageScroll < lastTaskScroll)
5676                     || (!mIsRtl && pageScroll > lastTaskScroll)) {
5677                 pageScroll = lastTaskScroll;
5678             }
5679             if (outPageScrolls[i] != pageScroll) {
5680                 pageScrollChanged = true;
5681                 outPageScrolls[i] = pageScroll;
5682             }
5683             if (DEBUG) {
5684                 Log.d(TAG, "getPageScrolls - outPageScrolls[" + i + "]: " + outPageScrolls[i]);
5685             }
5686         }
5687         if (DEBUG) {
5688             Log.d(TAG, "getPageScrolls - clearAllScroll: " + clearAllScroll);
5689         }
5690         return pageScrollChanged;
5691     }
5692 
5693     @Override
getChildOffset(int index)5694     protected int getChildOffset(int index) {
5695         int childOffset = super.getChildOffset(index);
5696         View child = getChildAt(index);
5697         if (child instanceof TaskView) {
5698             childOffset += ((TaskView) child).getOffsetAdjustment(showAsGrid());
5699         } else if (child instanceof ClearAllButton) {
5700             childOffset += ((ClearAllButton) child).getOffsetAdjustment(mOverviewFullscreenEnabled,
5701                     showAsGrid());
5702         }
5703         return childOffset;
5704     }
5705 
5706     @Override
getChildVisibleSize(int index)5707     protected int getChildVisibleSize(int index) {
5708         final TaskView taskView = getTaskViewAt(index);
5709         if (taskView == null) {
5710             return super.getChildVisibleSize(index);
5711         }
5712         return (int) (super.getChildVisibleSize(index) * taskView.getSizeAdjustment(
5713                 showAsFullscreen()));
5714     }
5715 
getClearAllButton()5716     public ClearAllButton getClearAllButton() {
5717         return mClearAllButton;
5718     }
5719 
5720     /**
5721      * @return How many pixels the running task is offset on the currently laid out dominant axis.
5722      */
getScrollOffset()5723     public int getScrollOffset() {
5724         return getScrollOffset(getRunningTaskIndex());
5725     }
5726 
5727     /**
5728      * Returns how many pixels the running task is offset on the currently laid out dominant axis
5729      * specifically during a Keyboard task focus.
5730      */
getScrollOffsetForKeyboardTaskFocus()5731     public int getScrollOffsetForKeyboardTaskFocus() {
5732         if (!isKeyboardTaskFocusPending()) {
5733             return getScrollOffset(getRunningTaskIndex());
5734         }
5735         return getPagedOrientationHandler().getPrimaryScroll(this)
5736                 - getScrollForPage(mKeyboardTaskFocusIndex)
5737                 + getScrollOffset(getRunningTaskIndex());
5738     }
5739 
5740     /**
5741      * Sets whether or not we should clamp the scroll offset.
5742      * This is used to avoid x-axis movement when swiping up transient taskbar.
5743      * Should only be set at the beginning and end of the gesture, otherwise a jump may occur.
5744      * @param clampScrollOffset When true, we clamp the scroll to 0 before the clamp threshold is
5745      *                          met.
5746      */
setClampScrollOffset(boolean clampScrollOffset)5747     public void setClampScrollOffset(boolean clampScrollOffset) {
5748         mShouldClampScrollOffset = clampScrollOffset;
5749     }
5750 
5751     /**
5752      * Returns how many pixels the page is offset on the currently laid out dominant axis.
5753      */
getScrollOffset(int pageIndex)5754     public int getScrollOffset(int pageIndex) {
5755         int unclampedOffset = getUnclampedScrollOffset(pageIndex);
5756         if (!mShouldClampScrollOffset) {
5757             return unclampedOffset;
5758         }
5759         if (Math.abs(unclampedOffset) < mClampedScrollOffsetBound) {
5760             return 0;
5761         }
5762         return unclampedOffset
5763                 - Math.round(Math.signum(unclampedOffset) * mClampedScrollOffsetBound);
5764     }
5765 
5766     /**
5767      * Returns how many pixels the page is offset on the currently laid out dominant axis.
5768      */
getUnclampedScrollOffset(int pageIndex)5769     private int getUnclampedScrollOffset(int pageIndex) {
5770         if (pageIndex == -1) {
5771             return 0;
5772         }
5773         // Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so that
5774         // the page can move freely given there's no visual indication why it shouldn't.
5775         int overScrollShift = mAdjacentPageHorizontalOffset > 0
5776                         ? (int) Utilities.mapRange(
5777                                 mAdjacentPageHorizontalOffset,
5778                                 getOverScrollShift(),
5779                                 getUndampedOverScrollShift())
5780                         : getOverScrollShift();
5781         return getScrollForPage(pageIndex) - getPagedOrientationHandler().getPrimaryScroll(this)
5782                 + overScrollShift + getOffsetFromScrollPosition(pageIndex);
5783     }
5784 
5785     /**
5786      * Returns how many pixels the page is offset from its scroll position.
5787      */
getOffsetFromScrollPosition(int pageIndex)5788     private int getOffsetFromScrollPosition(int pageIndex) {
5789         return getOffsetFromScrollPosition(pageIndex, getTopRowIdArray(), getBottomRowIdArray());
5790     }
5791 
getOffsetFromScrollPosition( int pageIndex, IntArray topRowIdArray, IntArray bottomRowIdArray)5792     private int getOffsetFromScrollPosition(
5793             int pageIndex, IntArray topRowIdArray, IntArray bottomRowIdArray) {
5794         if (!showAsGrid()) {
5795             return 0;
5796         }
5797 
5798         TaskView taskView = getTaskViewAt(pageIndex);
5799         if (taskView == null) {
5800             return 0;
5801         }
5802 
5803         TaskView lastGridTaskView = getLastGridTaskView(topRowIdArray, bottomRowIdArray);
5804         if (lastGridTaskView == null) {
5805             return 0;
5806         }
5807 
5808         if (getScrollForPage(pageIndex) != getScrollForPage(indexOfChild(lastGridTaskView))) {
5809             return 0;
5810         }
5811 
5812         // Check distance from lastGridTaskView to taskView.
5813         int lastGridTaskViewPosition =
5814                 getPositionInRow(lastGridTaskView, topRowIdArray, bottomRowIdArray);
5815         int taskViewPosition = getPositionInRow(taskView, topRowIdArray, bottomRowIdArray);
5816         int gridTaskSizeAndSpacing = mLastComputedGridTaskSize.width() + mPageSpacing;
5817         int positionDiff = gridTaskSizeAndSpacing * (lastGridTaskViewPosition - taskViewPosition);
5818 
5819         int taskEnd = getLastTaskEnd() + (mIsRtl ? positionDiff : -positionDiff);
5820         int normalTaskEnd = mIsRtl
5821                 ? mLastComputedGridTaskSize.left
5822                 : mLastComputedGridTaskSize.right;
5823         return taskEnd - normalTaskEnd;
5824     }
5825 
getLastTaskEnd()5826     private int getLastTaskEnd() {
5827         return mIsRtl
5828                 ? mLastComputedGridSize.left + mPageSpacing + mClearAllShortTotalWidthTranslation
5829                 : mLastComputedGridSize.right - mPageSpacing - mClearAllShortTotalWidthTranslation;
5830     }
5831 
getPositionInRow( TaskView taskView, IntArray topRowIdArray, IntArray bottomRowIdArray)5832     private int getPositionInRow(
5833             TaskView taskView, IntArray topRowIdArray, IntArray bottomRowIdArray) {
5834         int position = topRowIdArray.indexOf(taskView.getTaskViewId());
5835         return position != -1 ? position : bottomRowIdArray.indexOf(taskView.getTaskViewId());
5836     }
5837 
5838     /**
5839      * @return true if the task in on the top of the grid
5840      */
isOnGridBottomRow(TaskView taskView)5841     public boolean isOnGridBottomRow(TaskView taskView) {
5842         return showAsGrid()
5843                 && !mTopRowIdSet.contains(taskView.getTaskViewId())
5844                 && taskView.getTaskViewId() != mFocusedTaskViewId;
5845     }
5846 
getEventDispatcher(float navbarRotation)5847     public Consumer<MotionEvent> getEventDispatcher(float navbarRotation) {
5848         float degreesRotated;
5849         if (navbarRotation == 0) {
5850             degreesRotated = getPagedOrientationHandler().getDegreesRotated();
5851         } else {
5852             degreesRotated = -navbarRotation;
5853         }
5854         if (degreesRotated == 0) {
5855             return super::onTouchEvent;
5856         }
5857 
5858         // At this point the event coordinates have already been transformed, so we need to
5859         // undo that transformation since PagedView also accommodates for the transformation via
5860         // PagedOrientationHandler
5861         return e -> {
5862             if (navbarRotation != 0
5863                     && mOrientationState.isMultipleOrientationSupportedByDevice()
5864                     && !mOrientationState.getOrientationHandler().isLayoutNaturalToLauncher()) {
5865                 mOrientationState.flipVertical(e);
5866                 super.onTouchEvent(e);
5867                 mOrientationState.flipVertical(e);
5868                 return;
5869             }
5870             mOrientationState.transformEvent(-degreesRotated, e, true);
5871             super.onTouchEvent(e);
5872             mOrientationState.transformEvent(-degreesRotated, e, false);
5873         };
5874     }
5875 
5876     private void updateEnabledOverlays() {
5877         TaskView focusedTaskView = getFocusedTaskView();
5878         int taskCount = getTaskViewCount();
5879         for (int i = 0; i < taskCount; i++) {
5880             TaskView taskView = requireTaskViewAt(i);
5881             if (taskView == focusedTaskView) {
5882                 continue;
5883             }
5884             taskView.setOverlayEnabled(mOverlayEnabled && isTaskViewFullyVisible(taskView));
5885         }
5886         // Focus task overlay should be enabled and refreshed at last
5887         if (focusedTaskView != null) {
5888             focusedTaskView.setOverlayEnabled(
5889                     mOverlayEnabled && isTaskViewFullyVisible(focusedTaskView));
5890         }
5891     }
5892 
5893     public void setOverlayEnabled(boolean overlayEnabled) {
5894         if (mOverlayEnabled != overlayEnabled) {
5895             mOverlayEnabled = overlayEnabled;
5896             updateEnabledOverlays();
5897         }
5898     }
5899 
5900     public void setOverviewGridEnabled(boolean overviewGridEnabled) {
5901         if (mOverviewGridEnabled != overviewGridEnabled) {
5902             mOverviewGridEnabled = overviewGridEnabled;
5903             updateActionsViewFocusedScroll();
5904             // Request layout to ensure scroll position is recalculated with updated mGridProgress.
5905             requestLayout();
5906         }
5907     }
5908 
5909     public void setOverviewFullscreenEnabled(boolean overviewFullscreenEnabled) {
5910         if (mOverviewFullscreenEnabled != overviewFullscreenEnabled) {
5911             mOverviewFullscreenEnabled = overviewFullscreenEnabled;
5912             // Request layout to ensure scroll position is recalculated with updated
5913             // mFullscreenProgress.
5914             requestLayout();
5915         }
5916     }
5917 
5918     /**
5919      * Update whether RecentsView is in select mode. Should be enabled before transitioning to
5920      * select mode, and only disabled after transitioning from select mode.
5921      */
5922     public void setOverviewSelectEnabled(boolean overviewSelectEnabled) {
5923         if (mOverviewSelectEnabled != overviewSelectEnabled) {
5924             mOverviewSelectEnabled = overviewSelectEnabled;
5925             updatePivots();
5926             if (!mOverviewSelectEnabled) {
5927                 setSelectedTask(INVALID_TASK_ID);
5928             }
5929         }
5930     }
5931 
5932     /**
5933      * Switch the current running task view to static snapshot mode,
5934      * capturing the snapshot at the same time.
5935      */
5936     public void switchToScreenshot(Runnable onFinishRunnable) {
5937         if (mRecentsAnimationController == null) {
5938             if (onFinishRunnable != null) {
5939                 onFinishRunnable.run();
5940             }
5941             return;
5942         }
5943 
5944         switchToScreenshotInternal(onFinishRunnable);
5945     }
5946 
5947     private void switchToScreenshotInternal(Runnable onFinishRunnable) {
5948         TaskView taskView = getRunningTaskView();
5949         if (taskView == null) {
5950             onFinishRunnable.run();
5951             return;
5952         }
5953 
5954         setRunningTaskViewShowScreenshot(true);
5955         for (TaskContainer container : taskView.getTaskContainers()) {
5956             if (container == null) {
5957                 continue;
5958             }
5959 
5960             ThumbnailData td =
5961                     mRecentsAnimationController.screenshotTask(container.getTask().key.id);
5962             TaskThumbnailViewDeprecated thumbnailView = container.getThumbnailViewDeprecated();
5963             if (td != null) {
5964                 thumbnailView.setThumbnail(container.getTask(), td);
5965             } else {
5966                 thumbnailView.refresh();
5967             }
5968         }
5969         ViewUtils.postFrameDrawn(taskView, onFinishRunnable);
5970     }
5971 
5972     /**
5973      * Switch the current running task view to static snapshot mode, using the
5974      * provided thumbnail data as the snapshot.
5975      * TODO(b/195609063) Consolidate this method w/ the one above, except this thumbnail data comes
5976      *  from gesture state, which is a larger change of it having to keep track of multiple tasks.
5977      *  OR. Maybe it doesn't need to pass in a thumbnail and we can use the exact same flow as above
5978      */
5979     public void switchToScreenshot(@Nullable HashMap<Integer, ThumbnailData> thumbnailDatas,
5980             Runnable onFinishRunnable) {
5981         final TaskView taskView = getRunningTaskView();
5982         if (taskView != null) {
5983             taskView.setShouldShowScreenshot(true);
5984             taskView.refreshThumbnails(thumbnailDatas);
5985             ViewUtils.postFrameDrawn(taskView, onFinishRunnable);
5986         } else {
5987             onFinishRunnable.run();
5988         }
5989     }
5990 
5991     /**
5992      * The current task is fully modal (modalness = 1) when it is shown on its own in a modal
5993      * way. Modalness 0 means the task is shown in context with all the other tasks.
5994      */
5995     private void setTaskModalness(float modalness) {
5996         mTaskModalness = modalness;
5997         updatePageOffsets();
5998         if (mSelectedTask != null) {
5999             mSelectedTask.setModalness(modalness);
6000         } else if (getCurrentPageTaskView() != null) {
6001             getCurrentPageTaskView().setModalness(modalness);
6002         }
6003         // Only show actions view when it's modal for in-place landscape mode.
6004         boolean inPlaceLandscape = !mOrientationState.isRecentsActivityRotationAllowed()
6005                 && mOrientationState.getTouchRotation() != ROTATION_0;
6006         mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION, modalness < 1 && inPlaceLandscape);
6007     }
6008 
6009     @Nullable
6010     protected DepthController getDepthController() {
6011         return null;
6012     }
6013 
6014     @Nullable
6015     protected DesktopRecentsTransitionController getDesktopRecentsController() {
6016         return mDesktopRecentsTransitionController;
6017     }
6018 
6019     /** Enables or disables modal state for RecentsView */
6020     public abstract void setModalStateEnabled(int taskId, boolean animate);
6021 
6022     public TaskOverlayFactory getTaskOverlayFactory() {
6023         return mTaskOverlayFactory;
6024     }
6025 
6026     public BaseContainerInterface getSizeStrategy() {
6027         return mSizeStrategy;
6028     }
6029 
6030     /**
6031      * Set all the task views to color tint scrim mode, dimming or tinting them all. Allows the
6032      * tasks to be dimmed while other elements in the recents view are left alone.
6033      */
6034     public void showForegroundScrim(boolean show) {
6035         if (!show && mColorTint == 0) {
6036             if (mTintingAnimator != null) {
6037                 mTintingAnimator.cancel();
6038                 mTintingAnimator = null;
6039             }
6040             return;
6041         }
6042 
6043         mTintingAnimator = ObjectAnimator.ofFloat(this, COLOR_TINT,
6044                 show ? FOREGROUND_SCRIM_TINT : 0f);
6045         mTintingAnimator.setAutoCancel(true);
6046         mTintingAnimator.start();
6047     }
6048 
6049     /** Tint the RecentsView and TaskViews in to simulate a scrim. */
6050     // TODO(b/187528071): Replace this tinting with a scrim on top of RecentsView
6051     private void setColorTint(float tintAmount) {
6052         mColorTint = tintAmount;
6053 
6054         for (int i = 0; i < getTaskViewCount(); i++) {
6055             requireTaskViewAt(i).setColorTint(mColorTint, mTintingColor);
6056         }
6057 
6058         Drawable scrimBg = mContainer.getScrimView().getBackground();
6059         if (scrimBg != null) {
6060             if (tintAmount == 0f) {
6061                 scrimBg.setTintList(null);
6062             } else {
6063                 scrimBg.setTintBlendMode(BlendMode.SRC_OVER);
6064                 scrimBg.setTint(
6065                         ColorUtils.setAlphaComponent(mTintingColor, (int) (255 * tintAmount)));
6066             }
6067         }
6068     }
6069 
6070     private float getColorTint() {
6071         return mColorTint;
6072     }
6073 
6074     /** Returns {@code true} if the overview tasks are displayed as a grid. */
6075     public boolean showAsGrid() {
6076         return mOverviewGridEnabled || (mCurrentGestureEndTarget != null
6077                 && mSizeStrategy.stateFromGestureEndTarget(mCurrentGestureEndTarget)
6078                     .displayOverviewTasksAsGrid(mContainer.getDeviceProfile()));
6079     }
6080 
6081     private boolean showAsFullscreen() {
6082         return mOverviewFullscreenEnabled
6083                 && mCurrentGestureEndTarget != GestureState.GestureEndTarget.RECENTS;
6084     }
6085 
6086     public void cleanupRemoteTargets() {
6087         Log.d(TAG, "cleanupRemoteTargets - mRemoteTargetHandles: " + Arrays.toString(
6088                 mRemoteTargetHandles));
6089         mRemoteTargetHandles = null;
6090     }
6091 
6092     /**
6093      * Used to register callbacks for when our empty message state changes.
6094      *
6095      * @see #setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener)
6096      * @see #updateEmptyMessage()
6097      */
6098     public interface OnEmptyMessageUpdatedListener {
6099         /** @param isEmpty Whether RecentsView is empty (i.e. has no children) */
6100         void onEmptyMessageUpdated(boolean isEmpty);
6101     }
6102 
6103     /**
6104      * Adds a listener for scroll changes
6105      */
6106     public void addOnScrollChangedListener(OnScrollChangedListener listener) {
6107         mScrollListeners.add(listener);
6108     }
6109 
6110     /**
6111      * Removes a previously added scroll change listener
6112      */
6113     public void removeOnScrollChangedListener(OnScrollChangedListener listener) {
6114         mScrollListeners.remove(listener);
6115     }
6116 
6117     /**
6118      * @return Corner radius in pixel value for PiP window, which is updated via
6119      *         {@link #mIPipAnimationListener}
6120      */
6121     public int getPipCornerRadius() {
6122         return mPipCornerRadius;
6123     }
6124 
6125     /**
6126      * @return Shadow radius in pixel value for PiP window, which is updated via
6127      *         {@link #mIPipAnimationListener}
6128      */
6129     public int getPipShadowRadius() {
6130         return mPipShadowRadius;
6131     }
6132 
6133     @Override
6134     public boolean scrollLeft() {
6135         if (!showAsGrid()) {
6136             return super.scrollLeft();
6137         }
6138 
6139         int targetPage = getNextPage();
6140         if (targetPage >= 0) {
6141             // Find the next page that is not fully visible.
6142             TaskView taskView = getTaskViewAt(targetPage);
6143             while ((taskView == null || isTaskViewFullyVisible(taskView)) && targetPage - 1 >= 0) {
6144                 taskView = getTaskViewAt(--targetPage);
6145             }
6146             // Target a scroll where targetPage is on left of screen but still fully visible.
6147             int normalTaskEnd = mIsRtl
6148                     ? mLastComputedGridTaskSize.left
6149                     : mLastComputedGridTaskSize.right;
6150             int targetScroll = getScrollForPage(targetPage) + normalTaskEnd - getLastTaskEnd();
6151             // Find a page that is close to targetScroll while not over it.
6152             while (targetPage - 1 >= 0
6153                     && (mIsRtl
6154                     ? getScrollForPage(targetPage - 1) < targetScroll
6155                     : getScrollForPage(targetPage - 1) > targetScroll)) {
6156                 targetPage--;
6157             }
6158             snapToPage(targetPage);
6159             return true;
6160         }
6161 
6162         return mAllowOverScroll;
6163     }
6164 
6165     @Override
6166     public boolean scrollRight() {
6167         if (!showAsGrid()) {
6168             return super.scrollRight();
6169         }
6170 
6171         int targetPage = getNextPage();
6172         if (targetPage < getChildCount()) {
6173             // Find the next page that is not fully visible.
6174             TaskView taskView = getTaskViewAt(targetPage);
6175             while ((taskView != null && isTaskViewFullyVisible(taskView))
6176                     && targetPage + 1 < getChildCount()) {
6177                 taskView = getTaskViewAt(++targetPage);
6178             }
6179             snapToPage(targetPage);
6180             return true;
6181         }
6182         return mAllowOverScroll;
6183     }
6184 
6185     @Override
6186     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
6187         super.onScrollChanged(l, t, oldl, oldt);
6188         dispatchScrollChanged();
6189     }
6190 
6191     /**
6192      * Prepares this RecentsView to scroll properly for an upcoming child view focus request from
6193      * keyboard quick switching
6194      */
6195     public void setKeyboardTaskFocusIndex(int taskIndex) {
6196         mKeyboardTaskFocusIndex = taskIndex;
6197     }
6198 
6199     /** Returns whether this RecentsView will be scrolling to a child view for a focus request */
6200     public boolean isKeyboardTaskFocusPending() {
6201         return mKeyboardTaskFocusIndex != INVALID_PAGE;
6202     }
6203 
6204     private boolean isKeyboardTaskFocusPendingForChild(View child) {
6205         return isKeyboardTaskFocusPending() && mKeyboardTaskFocusIndex == indexOfChild(child);
6206     }
6207 
6208     @Override
6209     protected int getSnapAnimationDuration() {
6210         return isKeyboardTaskFocusPending()
6211                 ? mKeyboardTaskFocusSnapAnimationDuration : super.getSnapAnimationDuration();
6212     }
6213 
6214     @Override
6215     protected void onVelocityValuesUpdated() {
6216         super.onVelocityValuesUpdated();
6217         mKeyboardTaskFocusSnapAnimationDuration =
6218                 getResources().getInteger(R.integer.config_keyboardTaskFocusSnapAnimationDuration);
6219     }
6220 
6221     @Override
6222     protected boolean shouldHandleRequestChildFocus(View child) {
6223         // If we are already scrolling to a task view and we aren't focusing to this child from
6224         // keyboard quick switch, then the focus request has already been handled
6225         return mScroller.isFinished() || isKeyboardTaskFocusPendingForChild(child);
6226     }
6227 
6228     @Override
6229     public void requestChildFocus(View child, View focused) {
6230         if (isKeyboardTaskFocusPendingForChild(child)) {
6231             updateGridProperties();
6232             updateScrollSynchronously();
6233         }
6234         super.requestChildFocus(child, focused);
6235     }
6236 
6237     private void dispatchScrollChanged() {
6238         runActionOnRemoteHandles(remoteTargetHandle ->
6239                 remoteTargetHandle.getTaskViewSimulator().setScroll(getScrollOffset()));
6240         for (int i = mScrollListeners.size() - 1; i >= 0; i--) {
6241             mScrollListeners.get(i).onScrollChanged();
6242         }
6243     }
6244 
6245     private static class PinnedStackAnimationListener<T extends RecentsViewContainer> extends
6246             IPipAnimationListener.Stub {
6247         @Nullable
6248         private T mActivity;
6249         @Nullable
6250         private RecentsView mRecentsView;
6251 
6252         public void setActivityAndRecentsView(@Nullable T activity,
6253                 @Nullable RecentsView recentsView) {
6254             mActivity = activity;
6255             mRecentsView = recentsView;
6256         }
6257 
6258         @Override
6259         public void onPipAnimationStarted() {
6260             MAIN_EXECUTOR.execute(() -> {
6261                 // Needed for activities that auto-enter PiP, which will not trigger a remote
6262                 // animation to be created
6263                 if (mActivity != null) {
6264                     mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
6265                 }
6266             });
6267         }
6268 
6269         @Override
6270         public void onPipResourceDimensionsChanged(int cornerRadius, int shadowRadius) {
6271             if (mRecentsView != null) {
6272                 mRecentsView.mPipCornerRadius = cornerRadius;
6273                 mRecentsView.mPipShadowRadius = shadowRadius;
6274             }
6275         }
6276 
6277         @Override
6278         public void onExpandPip() {
6279             MAIN_EXECUTOR.execute(() -> {
6280                 if (mRecentsView == null
6281                         || mRecentsView.mSizeStrategy.getTaskbarController() == null) {
6282                     return;
6283                 }
6284                 // Hide the task bar when leaving PiP to prevent it from flickering once
6285                 // the app settles in full-screen mode.
6286                 mRecentsView.mSizeStrategy.getTaskbarController().onExpandPip();
6287             });
6288         }
6289     }
6290 
6291     /** Get the color used for foreground scrimming the RecentsView for sharing. */
6292     public static int getForegroundScrimDimColor(Context context) {
6293         return context.getColor(R.color.overview_foreground_scrim_color);
6294     }
6295 
6296     /** Get the RecentsAnimationController */
6297     @Nullable
6298     public RecentsAnimationController getRecentsAnimationController() {
6299         return mRecentsAnimationController;
6300     }
6301 
6302     @Nullable
6303     public SplitInstructionsView getSplitInstructionsView() {
6304         return mSplitSelectStateController.getSplitInstructionsView();
6305     }
6306 
6307     /** Update the current activity locus id to show the enabled state of Overview */
6308     public void updateLocusId() {
6309         String locusId = "Overview";
6310 
6311         if (mOverviewStateEnabled && mContainer.isStarted()) {
6312             locusId += "|ENABLED";
6313         } else {
6314             locusId += "|DISABLED";
6315         }
6316 
6317         final LocusId id = new LocusId(locusId);
6318         // Set locus context is a binder call, don't want it to happen during a transition
6319         UI_HELPER_EXECUTOR.post(() -> mContainer.setLocusContext(id, Bundle.EMPTY));
6320     }
6321 
6322     /**
6323      * Moves the provided task into desktop mode, and invoke {@code successCallback} if succeeded.
6324      */
6325     public void moveTaskToDesktop(TaskContainer taskContainer,
6326             DesktopModeTransitionSource transitionSource,
6327             Runnable successCallback) {
6328         if (!DesktopModeStatus.canEnterDesktopMode(mContext)) {
6329             return;
6330         }
6331         switchToScreenshot(() -> finishRecentsAnimation(/* toRecents= */true, /* shouldPip= */false,
6332                 () -> moveTaskToDesktopInternal(taskContainer, successCallback, transitionSource)));
6333     }
6334 
6335     private void moveTaskToDesktopInternal(TaskContainer taskContainer,
6336             Runnable successCallback, DesktopModeTransitionSource transitionSource) {
6337         if (mDesktopRecentsTransitionController == null) {
6338             return;
6339         }
6340         mDesktopRecentsTransitionController.moveToDesktop(taskContainer.getTask().key.id,
6341                 transitionSource);
6342         successCallback.run();
6343     }
6344 
6345     // Logs when the orientation of Overview changes. We log both real and fake orientation changes.
6346     private void logOrientationChanged() {
6347         // Only log when Overview is showing.
6348         if (mOverviewStateEnabled) {
6349             mContainer.getStatsLogManager()
6350                     .logger()
6351                     .withContainerInfo(
6352                             LauncherAtom.ContainerInfo.newBuilder()
6353                                     .setTaskSwitcherContainer(
6354                                             LauncherAtom.TaskSwitcherContainer.newBuilder()
6355                                                     .setOrientationHandler(
6356                                                             getPagedOrientationHandler()
6357                                                                     .getHandlerTypeForLogging()))
6358                                     .build())
6359                     .log(LAUNCHER_OVERVIEW_ORIENTATION_CHANGED);
6360         }
6361     }
6362 
6363     public interface TaskLaunchListener {
6364         void onTaskLaunched();
6365     }
6366 }
6367