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