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