• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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 package com.android.quickstep.views
17 
18 import android.animation.Animator
19 import android.animation.AnimatorListenerAdapter
20 import android.animation.AnimatorSet
21 import android.animation.ObjectAnimator
22 import android.annotation.IdRes
23 import android.app.ActivityOptions
24 import android.content.Context
25 import android.graphics.Canvas
26 import android.graphics.PointF
27 import android.graphics.Rect
28 import android.graphics.drawable.Drawable
29 import android.os.Bundle
30 import android.util.AttributeSet
31 import android.util.FloatProperty
32 import android.util.Log
33 import android.view.Display
34 import android.view.MotionEvent
35 import android.view.View
36 import android.view.View.OnClickListener
37 import android.view.ViewGroup
38 import android.view.ViewStub
39 import android.view.accessibility.AccessibilityNodeInfo
40 import android.widget.FrameLayout
41 import android.widget.Toast
42 import androidx.annotation.IntDef
43 import androidx.annotation.VisibleForTesting
44 import androidx.core.view.updateLayoutParams
45 import com.android.app.animation.Interpolators
46 import com.android.app.tracing.traceSection
47 import com.android.launcher3.AbstractFloatingView
48 import com.android.launcher3.Flags.enableCursorHoverStates
49 import com.android.launcher3.Flags.enableDesktopExplodedView
50 import com.android.launcher3.Flags.enableGridOnlyOverview
51 import com.android.launcher3.Flags.enableHoverOfChildElementsInTaskview
52 import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
53 import com.android.launcher3.Flags.enableOverviewIconMenu
54 import com.android.launcher3.Flags.enableRefactorTaskThumbnail
55 import com.android.launcher3.Flags.enableSeparateExternalDisplayTasks
56 import com.android.launcher3.R
57 import com.android.launcher3.Utilities
58 import com.android.launcher3.anim.AnimatedFloat
59 import com.android.launcher3.logging.StatsLogManager.LauncherEvent
60 import com.android.launcher3.model.data.ItemInfo
61 import com.android.launcher3.model.data.TaskViewItemInfo
62 import com.android.launcher3.testing.TestLogging
63 import com.android.launcher3.testing.shared.TestProtocol
64 import com.android.launcher3.util.CancellableTask
65 import com.android.launcher3.util.Executors
66 import com.android.launcher3.util.KFloatProperty
67 import com.android.launcher3.util.MultiPropertyDelegate
68 import com.android.launcher3.util.MultiPropertyFactory
69 import com.android.launcher3.util.MultiValueAlpha
70 import com.android.launcher3.util.RunnableList
71 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
72 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
73 import com.android.launcher3.util.TraceHelper
74 import com.android.launcher3.util.TransformingTouchDelegate
75 import com.android.launcher3.util.ViewPool
76 import com.android.launcher3.util.coroutines.DispatcherProvider
77 import com.android.launcher3.util.rects.set
78 import com.android.quickstep.FullscreenDrawParams
79 import com.android.quickstep.RecentsModel
80 import com.android.quickstep.RemoteAnimationTargets
81 import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle
82 import com.android.quickstep.TaskOverlayFactory
83 import com.android.quickstep.TaskViewUtils
84 import com.android.quickstep.orientation.RecentsPagedOrientationHandler
85 import com.android.quickstep.recents.di.RecentsDependencies
86 import com.android.quickstep.recents.di.get
87 import com.android.quickstep.recents.di.inject
88 import com.android.quickstep.recents.domain.usecase.ThumbnailPosition
89 import com.android.quickstep.recents.ui.viewmodel.TaskData
90 import com.android.quickstep.recents.ui.viewmodel.TaskTileUiState
91 import com.android.quickstep.recents.ui.viewmodel.TaskViewModel
92 import com.android.quickstep.util.ActiveGestureErrorDetector
93 import com.android.quickstep.util.ActiveGestureLog
94 import com.android.quickstep.util.BorderAnimator
95 import com.android.quickstep.util.BorderAnimator.Companion.createSimpleBorderAnimator
96 import com.android.quickstep.util.RecentsOrientedState
97 import com.android.quickstep.util.TaskCornerRadius
98 import com.android.quickstep.util.TaskRemovedDuringLaunchListener
99 import com.android.quickstep.util.isExternalDisplay
100 import com.android.quickstep.util.safeDisplayId
101 import com.android.quickstep.views.IconAppChipView.AppChipStatus
102 import com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL
103 import com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED
104 import com.android.quickstep.views.RecentsView.UNBOUND_TASK_VIEW_ID
105 import com.android.systemui.shared.recents.model.Task
106 import com.android.systemui.shared.recents.model.ThumbnailData
107 import com.android.systemui.shared.system.ActivityManagerWrapper
108 import kotlinx.coroutines.CoroutineScope
109 import kotlinx.coroutines.Job
110 import kotlinx.coroutines.cancel
111 import kotlinx.coroutines.flow.collectLatest
112 import kotlinx.coroutines.launch
113 
114 /** A task in the Recents view. */
115 open class TaskView
116 @JvmOverloads
117 constructor(
118     context: Context,
119     attrs: AttributeSet? = null,
120     defStyleAttr: Int = 0,
121     defStyleRes: Int = 0,
122     focusBorderAnimator: BorderAnimator? = null,
123     hoverBorderAnimator: BorderAnimator? = null,
124     val type: TaskViewType = TaskViewType.SINGLE,
125     protected val thumbnailFullscreenParams: FullscreenDrawParams = FullscreenDrawParams(context),
126 ) : FrameLayout(context, attrs), ViewPool.Reusable {
127     /**
128      * Used in conjunction with [onTaskListVisibilityChanged], providing more granularity on which
129      * components of this task require an update
130      */
131     @Retention(AnnotationRetention.SOURCE)
132     @IntDef(FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL, FLAG_UPDATE_CORNER_RADIUS)
133     annotation class TaskDataChanges
134 
135     val taskIds: IntArray
136         /** Returns a copy of integer array containing taskIds of all tasks in the TaskView. */
137         get() = taskContainers.map { it.task.key.id }.toIntArray()
138 
139     val taskIdSet: Set<Int>
140         /** Returns a copy of integer array containing taskIds of all tasks in the TaskView. */
141         get() = taskContainers.map { it.task.key.id }.toSet()
142 
143     val snapshotViews: Array<View>
144         get() = taskContainers.map { it.snapshotView }.toTypedArray()
145 
146     val isGridTask: Boolean
147         /** Returns whether the task is part of overview grid and not being focused. */
148         get() = container.deviceProfile.isTablet && !isLargeTile
149 
150     val isRunningTask: Boolean
151         get() = this === recentsView?.runningTaskView
152 
153     private val isSelectedTask: Boolean
154         get() = this === recentsView?.selectedTaskView
155 
156     open val displayId: Int
157         get() = taskContainers.firstOrNull()?.task.safeDisplayId
158 
159     val isExternalDisplay: Boolean
160         get() = displayId.isExternalDisplay
161 
162     val isLargeTile: Boolean
163         get() =
164             this == recentsView?.focusedTaskView ||
165                 (enableLargeDesktopWindowingTile() && type == TaskViewType.DESKTOP) ||
166                 (enableSeparateExternalDisplayTasks() && isExternalDisplay)
167 
168     val recentsView: RecentsView<*, *>?
169         get() = parent as? RecentsView<*, *>
170 
171     val pagedOrientationHandler: RecentsPagedOrientationHandler
172         get() = orientedState.orientationHandler
173 
174     val firstTaskContainer: TaskContainer?
175         get() = taskContainers.firstOrNull()
176 
177     val firstTask: Task?
178         /** Returns the first task bound to this TaskView. */
179         get() = firstTaskContainer?.task
180 
181     val firstItemInfo: ItemInfo?
182         get() = firstTaskContainer?.itemInfo
183 
184     /**
185      * A [TaskViewItemInfo] of this TaskView. The [firstTaskContainer] will be used to get some
186      * specific information like user, title etc of the Task. However, these task specific
187      * information will be skipped if the TaskView has no [taskContainers]. Note, please use
188      * [TaskContainer.itemInfo] for [TaskViewItemInfo] on a specific [TaskContainer].
189      */
190     val itemInfo: TaskViewItemInfo
191         get() = TaskViewItemInfo(this, firstTaskContainer)
192 
193     protected val container: RecentsViewContainer =
194         RecentsViewContainer.containerFromContext(context)
195     protected val lastTouchDownPosition = PointF()
196 
197     // Derived view properties
198     protected val persistentScale: Float
199         /**
200          * Returns multiplication of scale that is persistent (e.g. fullscreen and grid), and does
201          * not change according to a temporary state.
202          */
203         get() = Utilities.mapRange(gridProgress, nonGridScale, 1f)
204 
205     protected val persistentTranslationX: Float
206         /**
207          * Returns addition of translationX that is persistent (e.g. fullscreen and grid), and does
208          * not change according to a temporary state (e.g. task offset).
209          */
210         get() = (getNonGridTrans(nonGridTranslationX) + getGridTrans(this.gridTranslationX))
211 
212     val persistentTranslationY: Float
213         /**
214          * Returns addition of translationY that is persistent (e.g. fullscreen and grid), and does
215          * not change according to a temporary state (e.g. task offset).
216          */
217         get() = boxTranslationY + getGridTrans(gridTranslationY)
218 
219     protected val primarySplitTranslationProperty: FloatProperty<TaskView>
220         get() =
221             pagedOrientationHandler.getPrimaryValue(
222                 SPLIT_SELECT_TRANSLATION_X,
223                 SPLIT_SELECT_TRANSLATION_Y,
224             )
225 
226     protected val secondarySplitTranslationProperty: FloatProperty<TaskView>
227         get() =
228             pagedOrientationHandler.getSecondaryValue(
229                 SPLIT_SELECT_TRANSLATION_X,
230                 SPLIT_SELECT_TRANSLATION_Y,
231             )
232 
233     val primaryDismissTranslationProperty: FloatProperty<TaskView>
234         get() =
235             pagedOrientationHandler.getPrimaryValue(DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y)
236 
237     val secondaryDismissTranslationProperty: FloatProperty<TaskView>
238         get() =
239             pagedOrientationHandler.getSecondaryValue(DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y)
240 
241     protected val primaryTaskOffsetTranslationProperty: FloatProperty<TaskView>
242         get() =
243             pagedOrientationHandler.getPrimaryValue(
244                 TASK_OFFSET_TRANSLATION_X,
245                 TASK_OFFSET_TRANSLATION_Y,
246             )
247 
248     protected val secondaryTaskOffsetTranslationProperty: FloatProperty<TaskView>
249         get() =
250             pagedOrientationHandler.getSecondaryValue(
251                 TASK_OFFSET_TRANSLATION_X,
252                 TASK_OFFSET_TRANSLATION_Y,
253             )
254 
255     protected val taskResistanceTranslationProperty: FloatProperty<TaskView>
256         get() =
257             pagedOrientationHandler.getSecondaryValue(
258                 TASK_RESISTANCE_TRANSLATION_X,
259                 TASK_RESISTANCE_TRANSLATION_Y,
260             )
261 
262     private val tempCoordinates = FloatArray(2)
263     private val focusBorderAnimator: BorderAnimator? =
264         focusBorderAnimator
265             ?: createSimpleBorderAnimator(
266                 TaskCornerRadius.get(context).toInt(),
267                 context.resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_border_width),
268                 this::getThumbnailBounds,
269                 this,
270                 context
271                     .obtainStyledAttributes(attrs, R.styleable.TaskView, defStyleAttr, defStyleRes)
272                     .getColor(
273                         R.styleable.TaskView_focusBorderColor,
274                         BorderAnimator.DEFAULT_BORDER_COLOR,
275                     ),
276             )
277 
278     private val hoverBorderAnimator: BorderAnimator? =
279         hoverBorderAnimator
280             ?: if (enableCursorHoverStates())
281                 createSimpleBorderAnimator(
282                     TaskCornerRadius.get(context).toInt(),
283                     context.resources.getDimensionPixelSize(R.dimen.task_hover_border_width),
284                     this::getThumbnailBounds,
285                     this,
286                     context
287                         .obtainStyledAttributes(
288                             attrs,
289                             R.styleable.TaskView,
290                             defStyleAttr,
291                             defStyleRes,
292                         )
293                         .getColor(
294                             R.styleable.TaskView_hoverBorderColor,
295                             BorderAnimator.DEFAULT_BORDER_COLOR,
296                         ),
297                 )
298             else null
299 
300     private val rootViewDisplayId: Int
301         get() = rootView.display?.displayId ?: Display.DEFAULT_DISPLAY
302 
303     /** Returns a list of all TaskContainers in the TaskView. */
304     lateinit var taskContainers: List<TaskContainer>
305         protected set
306 
307     lateinit var orientedState: RecentsOrientedState
308 
309     var taskViewId = UNBOUND_TASK_VIEW_ID
310     var isEndQuickSwitchCuj = false
311     var sysUiStatusNavFlags: Int = 0
312         get() =
313             if (enableRefactorTaskThumbnail()) field
314             else firstTaskContainer?.thumbnailViewDeprecated?.sysUiStatusNavFlags ?: 0
315         private set
316 
317     // Various animation progress variables.
318     // progress: 0 = show icon and no insets; 1 = don't show icon and show full insets.
319     protected var fullscreenProgress = 0f
320         set(value) {
321             if (value == field && enableOverviewIconMenu()) return
322             field = Utilities.boundToRange(value, 0f, 1f)
323             onFullscreenProgressChanged(field)
324         }
325 
326     // gridProgress 0 = carousel; 1 = 2 row grid.
327     protected var gridProgress = 0f
328         set(value) {
329             field = value
330             onGridProgressChanged()
331         }
332 
333     /**
334      * The modalness of this view is how it should be displayed when it is shown on its own in the
335      * modal state of overview. 0 being in context with other tasks, 1 being shown on its own.
336      */
337     protected var modalness = 0f
338         set(value) {
339             if (field == value) {
340                 return
341             }
342             field = value
343             onModalnessUpdated(field)
344         }
345 
346     var modalPivot: PointF? = null
347         set(value) {
348             field = value
349             updatePivots()
350         }
351 
352     var splitSplashAlpha = 0f
353         set(value) {
354             field = value
355             applyThumbnailSplashAlpha()
356         }
357 
358     protected var taskThumbnailSplashAlpha = 0f
359         set(value) {
360             field = value
361             applyThumbnailSplashAlpha()
362         }
363 
364     protected var nonGridScale = 1f
365         set(value) {
366             field = value
367             applyScale()
368         }
369 
370     private var dismissScale = 1f
371         set(value) {
372             field = value
373             applyScale()
374         }
375 
376     var modalScale = 1f
377         set(value) {
378             field = value
379             applyScale()
380         }
381 
382     private var dismissTranslationX = 0f
383         set(value) {
384             field = value
385             applyTranslationX()
386         }
387 
388     private var dismissTranslationY = 0f
389         set(value) {
390             field = value
391             applyTranslationY()
392         }
393 
394     private var taskOffsetTranslationX = 0f
395         set(value) {
396             field = value
397             applyTranslationX()
398         }
399 
400     private var taskOffsetTranslationY = 0f
401         set(value) {
402             field = value
403             applyTranslationY()
404         }
405 
406     private var taskResistanceTranslationX = 0f
407         set(value) {
408             field = value
409             applyTranslationX()
410         }
411 
412     private var taskResistanceTranslationY = 0f
413         set(value) {
414             field = value
415             applyTranslationY()
416         }
417 
418     // The following translation variables should only be used in the same orientation as Launcher.
419     private var boxTranslationY = 0f
420         set(value) {
421             field = value
422             applyTranslationY()
423         }
424 
425     // The following grid translations scales with mGridProgress.
426     protected var gridTranslationX = 0f
427         set(value) {
428             field = value
429             applyTranslationX()
430         }
431 
432     var gridTranslationY = 0f
433         protected set(value) {
434             field = value
435             applyTranslationY()
436         }
437 
438     // The following grid translation is used to animate closing the gap between grid and clear all.
439     private var gridEndTranslationX = 0f
440         set(value) {
441             field = value
442             applyTranslationX()
443         }
444 
445     // Applied as a complement to gridTranslation, for adjusting the carousel overview and quick
446     // switch.
447     protected var nonGridTranslationX = 0f
448         set(value) {
449             field = value
450             applyTranslationX()
451         }
452 
453     // Used when in SplitScreenSelectState
454     private var splitSelectTranslationY = 0f
455         set(value) {
456             field = value
457             applyTranslationY()
458         }
459 
460     private var splitSelectTranslationX = 0f
461         set(value) {
462             field = value
463             applyTranslationX()
464         }
465 
466     private val taskViewAlpha = MultiValueAlpha(this, Alpha.entries.size)
467     protected var stableAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.Stable)
468     var attachAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.Attach)
469     var splitAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.Split)
470     private var modalAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.Modal)
471 
472     protected var shouldShowScreenshot = false
473         get() = !isRunningTask || field
474         private set
475 
476     /** Enable or disable showing border on hover and focus change */
477     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
478     var borderEnabled = false
479         set(value) {
480             if (field == value) {
481                 return
482             }
483             field = value
484             // Set the animation correctly in case it misses the hover/focus event during state
485             // transition
486             hoverBorderAnimator?.setBorderVisibility(visible = field && isHovered, animated = true)
487             focusBorderAnimator?.setBorderVisibility(visible = field && isFocused, animated = true)
488         }
489 
490     /**
491      * Used to cache the hover border state so we don't repeatedly call the border animator with
492      * every hover event when the user hasn't crossed the threshold of the [thumbnailBounds].
493      */
494     private var hoverBorderVisible = false
495         set(value) {
496             if (field == value) {
497                 return
498             }
499             field = value
500             Log.d(
501                 TAG,
502                 "${taskIds.contentToString()} - setting border animator visibility to: $field",
503             )
504             hoverBorderAnimator?.setBorderVisibility(visible = field, animated = true)
505         }
506 
507     // Used to cache thumbnail bounds to avoid recalculating on every hover move.
508     private var thumbnailBounds = Rect()
509 
510     // Progress variable indicating if the TaskView is in a settled state:
511     // 0 = The TaskView is in a transitioning state e.g. during gesture, in quickswitch carousel,
512     // becoming focus task etc.
513     // 1 = The TaskView is settled and no longer transitioning
514     private var settledProgress = 1f
515         set(value) {
516             if (value == field && enableOverviewIconMenu()) return
517             field = value
518             onSettledProgressUpdated(field)
519         }
520 
521     private val settledProgressPropertyFactory =
522         MultiPropertyFactory(
523             this,
524             SETTLED_PROGRESS,
525             SettledProgress.entries.size,
526             { x: Float, y: Float -> x * y },
527             1f,
528         )
529     private var settledProgressFullscreen by
530         MultiPropertyDelegate(settledProgressPropertyFactory, SettledProgress.Fullscreen)
531     private var settledProgressGesture by
532         MultiPropertyDelegate(settledProgressPropertyFactory, SettledProgress.Gesture)
533     private var settledProgressDismiss by
534         MultiPropertyDelegate(settledProgressPropertyFactory, SettledProgress.Dismiss)
535 
536     private var viewModel: TaskViewModel? = null
537     private val dispatcherProvider: DispatcherProvider by RecentsDependencies.inject()
538     private val coroutineScope: CoroutineScope by RecentsDependencies.inject()
539     private val coroutineJobs = mutableListOf<Job>()
540 
541     /**
542      * Returns an animator of [settledProgressDismiss] that transition in with a built-in
543      * interpolator.
544      */
545     fun getDismissIconFadeInAnimator(): ObjectAnimator =
546         ObjectAnimator.ofFloat(this, SETTLED_PROGRESS_DISMISS, 1f).apply {
547             duration = FADE_IN_ICON_DURATION
548             interpolator = FADE_IN_ICON_INTERPOLATOR
549         }
550 
551     /**
552      * Returns an animator of [settledProgressDismiss] that transition out with a built-in
553      * interpolator. [AnimatedFloat] is used to apply another level of interpolation, on top of
554      * interpolator set to the [Animator] by the caller.
555      */
556     fun getDismissIconFadeOutAnimator(): ObjectAnimator =
557         AnimatedFloat { v ->
558                 settledProgressDismiss = SETTLED_PROGRESS_FAST_OUT_INTERPOLATOR.getInterpolation(v)
559             }
560             .animateToValue(1f, 0f)
561 
562     private var iconFadeInOnGestureCompleteAnimator: ObjectAnimator? = null
563     // The current background requests to load the task thumbnail and icon
564     private val pendingThumbnailLoadRequests = mutableListOf<CancellableTask<*>>()
565     private val pendingIconLoadRequests = mutableListOf<CancellableTask<*>>()
566     private var isClickableAsLiveTile = true
567 
568     init {
569         setOnClickListener { _ -> onClick() }
570 
571         setWillNotDraw(!enableCursorHoverStates())
572     }
573 
574     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
575     public override fun onFocusChanged(
576         gainFocus: Boolean,
577         direction: Int,
578         previouslyFocusedRect: Rect?,
579     ) {
580         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
581         if (borderEnabled) {
582             focusBorderAnimator?.setBorderVisibility(gainFocus, /* animated= */ true)
583         }
584     }
585 
586     override fun onHoverEvent(event: MotionEvent): Boolean {
587         if (borderEnabled) {
588             when (event.action) {
589                 MotionEvent.ACTION_HOVER_ENTER -> {
590                     hoverBorderVisible =
591                         if (enableHoverOfChildElementsInTaskview()) {
592                             getThumbnailBounds(thumbnailBounds)
593                             event.isWithinThumbnailBounds()
594                         } else {
595                             true
596                         }
597                 }
598                 MotionEvent.ACTION_HOVER_MOVE ->
599                     if (enableHoverOfChildElementsInTaskview())
600                         hoverBorderVisible = event.isWithinThumbnailBounds()
601                 MotionEvent.ACTION_HOVER_EXIT -> hoverBorderVisible = false
602                 else -> {}
603             }
604         }
605         return super.onHoverEvent(event)
606     }
607 
608     override fun onInterceptHoverEvent(event: MotionEvent): Boolean =
609         if (enableHoverOfChildElementsInTaskview()) super.onInterceptHoverEvent(event)
610         else if (enableCursorHoverStates()) true else super.onInterceptHoverEvent(event)
611 
612     override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
613         val recentsView = recentsView ?: return false
614         val splitSelectStateController = recentsView.splitSelectController
615         // Disable taps for split selection animation unless we have a task not being selected
616         if (
617             splitSelectStateController.isSplitSelectActive &&
618                 taskContainers.none { it.task.key.id != splitSelectStateController.initialTaskId }
619         ) {
620             return false
621         }
622         if (ev.action == MotionEvent.ACTION_DOWN) {
623             with(lastTouchDownPosition) {
624                 x = ev.x
625                 y = ev.y
626             }
627         }
628         return super.dispatchTouchEvent(ev)
629     }
630 
631     override fun draw(canvas: Canvas) {
632         // Draw border first so any child views outside of the thumbnail bounds are drawn above it.
633         focusBorderAnimator?.drawBorder(canvas)
634         hoverBorderAnimator?.drawBorder(canvas)
635         super.draw(canvas)
636     }
637 
638     override fun setLayoutDirection(layoutDirection: Int) {
639         super.setLayoutDirection(layoutDirection)
640         if (enableOverviewIconMenu()) {
641             val deviceLayoutDirection = resources.configuration.layoutDirection
642             taskContainers.forEach {
643                 (it.iconView as IconAppChipView).layoutDirection = deviceLayoutDirection
644             }
645         }
646     }
647 
648     override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
649         super.onLayout(changed, left, top, right, bottom)
650         updatePivots()
651         systemGestureExclusionRects =
652             SYSTEM_GESTURE_EXCLUSION_RECT.onEach {
653                 it.right = width
654                 it.bottom = height
655             }
656         if (enableHoverOfChildElementsInTaskview()) {
657             getThumbnailBounds(thumbnailBounds)
658         }
659     }
660 
661     private fun updatePivots() {
662         val modalPivot = modalPivot
663         if (modalPivot != null) {
664             pivotX = modalPivot.x
665             pivotY = modalPivot.y
666         } else {
667             val thumbnailTopMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx
668             if (container.deviceProfile.isTablet) {
669                 pivotX =
670                     (if (layoutDirection == LAYOUT_DIRECTION_RTL) 0 else right - left).toFloat()
671                 pivotY = thumbnailTopMargin.toFloat()
672             } else {
673                 pivotX = (right - left) * 0.5f
674                 pivotY = thumbnailTopMargin + (height - thumbnailTopMargin) * 0.5f
675             }
676         }
677     }
678 
679     override fun onRecycle() {
680         resetPersistentViewTransforms()
681 
682         viewModel = null
683         attachAlpha = 1f
684         splitAlpha = 1f
685         splitSplashAlpha = 0f
686         modalAlpha = 1f
687         modalScale = 1f
688         modalPivot = null
689         taskThumbnailSplashAlpha = 0f
690         // Clear any references to the thumbnail (it will be re-read either from the cache or the
691         // system on next bind)
692         if (!enableRefactorTaskThumbnail()) {
693             taskContainers.forEach { it.thumbnailViewDeprecated.setThumbnail(it.task, null) }
694         }
695         setOverlayEnabled(false)
696         onTaskListVisibilityChanged(false)
697         borderEnabled = false
698         hoverBorderVisible = false
699         taskViewId = UNBOUND_TASK_VIEW_ID
700         // TODO(b/390583187): Clean the components UI State when TaskView is recycled.
701         taskContainers.forEach { it.destroy() }
702     }
703 
704     // TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
705     override fun hasOverlappingRendering() = false
706 
707     override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
708         super.onInitializeAccessibilityNodeInfo(info)
709         with(info) {
710             // Only make actions available if the app icon menu is visible to the user.
711             // When modalness is >0, the user is in select mode and the icon menu is hidden.
712             // When split selection is active, they should only be able to select the app and not
713             // take any other action.
714             val shouldPopulateAccessibilityMenu =
715                 modalness == 0f && recentsView?.isSplitSelectionActive == false
716             if (shouldPopulateAccessibilityMenu) {
717                 taskContainers.forEach {
718                     TraceHelper.allowIpcs("TV.a11yInfo") {
719                         TaskOverlayFactory.getEnabledShortcuts(this@TaskView, it).forEach { shortcut
720                             ->
721                             addAction(shortcut.createAccessibilityAction(context))
722                         }
723                     }
724                 }
725 
726                 // Add DWB accessibility action at the end of the list
727                 taskContainers.forEach {
728                     it.digitalWellBeingToast?.getDWBAccessibilityAction()?.let(::addAction)
729                 }
730             }
731 
732             recentsView?.let {
733                 collectionItemInfo =
734                     AccessibilityNodeInfo.CollectionItemInfo(
735                         0,
736                         1,
737                         it.getAccessibilityChildren().indexOf(this@TaskView),
738                         1,
739                         false,
740                     )
741             }
742         }
743     }
744 
745     override fun performAccessibilityAction(action: Int, arguments: Bundle?): Boolean {
746         // TODO(b/343708271): Add support for multiple tasks per action.
747         taskContainers.forEach {
748             if (it.digitalWellBeingToast?.handleAccessibilityAction(action) == true) {
749                 return true
750             }
751 
752             TaskOverlayFactory.getEnabledShortcuts(this, it).forEach { shortcut ->
753                 if (shortcut.hasHandlerForAction(action)) {
754                     shortcut.onClick(this)
755                     return true
756                 }
757             }
758         }
759 
760         return super.performAccessibilityAction(action, arguments)
761     }
762 
763     override fun onFinishInflate() {
764         super.onFinishInflate()
765         inflateViewStubs()
766     }
767 
768     protected open fun inflateViewStubs() {
769         findViewById<ViewStub>(R.id.snapshot)
770             ?.apply {
771                 layoutResource =
772                     if (enableRefactorTaskThumbnail()) R.layout.task_thumbnail
773                     else R.layout.task_thumbnail_deprecated
774             }
775             ?.inflate()
776         findViewById<ViewStub>(R.id.icon)
777             ?.apply {
778                 layoutResource =
779                     if (enableOverviewIconMenu()) R.layout.icon_app_chip_view
780                     else R.layout.icon_view
781             }
782             ?.inflate()
783     }
784 
785     override fun onAttachedToWindow() =
786         traceSection("TaskView.onAttachedToWindow") {
787             super.onAttachedToWindow()
788             if (enableRefactorTaskThumbnail()) {
789                 // The TaskView lifecycle is starts the ViewModel during onBind, and cleans it in
790                 // onRecycle. So it should be initialized at this point. TaskView Lifecycle:
791                 // `bind` -> `onBind` ->  onAttachedToWindow() -> onDetachFromWindow -> onRecycle
792                 coroutineJobs +=
793                     coroutineScope.launch(dispatcherProvider.main) {
794                         viewModel!!.state.collectLatest(::updateTaskViewState)
795                     }
796             }
797         }
798 
799     private fun updateTaskViewState(state: TaskTileUiState) =
800         traceSection("TaskView.updateTaskViewState") {
801             sysUiStatusNavFlags = state.sysUiStatusNavFlags
802 
803             // Updating containers
804             val mapOfTasks = state.tasks.associateBy { it.taskId }
805             taskContainers.forEach { container ->
806                 val taskId = container.task.key.id
807                 val containerState = mapOfTasks[taskId]
808                 val shouldHaveHeader = (type == TaskViewType.DESKTOP) && enableDesktopExplodedView()
809                 container.setState(
810                     state = containerState,
811                     liveTile = state.isLiveTile,
812                     hasHeader = shouldHaveHeader,
813                     clickCloseListener =
814                         if (shouldHaveHeader) {
815                             {
816                                 // Update the layout UI to remove this task from the layout grid,
817                                 // and remove the task from ActivityManager afterwards.
818                                 recentsView?.dismissTask(
819                                     taskId,
820                                     /* animate= */ true,
821                                     /* removeTask= */ true,
822                                 )
823                             }
824                         } else {
825                             null
826                         },
827                 )
828                 updateThumbnailValidity(container)
829                 val thumbnailPosition =
830                     updateThumbnailMatrix(
831                         container = container,
832                         width = container.thumbnailView.width,
833                         height = container.thumbnailView.height,
834                     )
835                 container.setOverlayEnabled(state.taskOverlayEnabled, thumbnailPosition)
836                 if (state.isCentralTask) {
837                     this.container.actionsView.let {
838                         it.updateDisabledFlags(
839                             DISABLED_ROTATED,
840                             thumbnailPosition?.isRotated ?: false,
841                         )
842                         it.updateDisabledFlags(
843                             DISABLED_NO_THUMBNAIL,
844                             state.tasks.any { taskData ->
845                                 (taskData as? TaskData.Data)?.thumbnailData?.thumbnail == null
846                             },
847                         )
848                     }
849                 }
850 
851                 if (enableOverviewIconMenu()) {
852                     setIconState(container, containerState)
853                 }
854             }
855         }
856 
857     private fun updateThumbnailValidity(container: TaskContainer) {
858         container.isThumbnailValid =
859             viewModel?.isThumbnailValid(
860                 thumbnail = container.thumbnailData,
861                 width = container.thumbnailView.width,
862                 height = container.thumbnailView.height,
863             ) ?: return
864         applyThumbnailSplashAlpha()
865     }
866 
867     /**
868      * Updates the thumbnail's transformation matrix and rotation state within a TaskContainer.
869      *
870      * This function is called to reposition the thumbnail in the following scenarios:
871      * - When the TTV's size changes (onSizeChanged), and it's displaying a SnapshotSplash.
872      * - When drawing a snapshot (drawSnapshot).
873      *
874      * @param container The TaskContainer holding the thumbnail to be updated.
875      * @param width The desired width of the thumbnail's container.
876      * @param height The desired height of the thumbnail's container.
877      */
878     private fun updateThumbnailMatrix(
879         container: TaskContainer,
880         width: Int,
881         height: Int,
882     ): ThumbnailPosition? =
883         traceSection("TaskView.updateThumbnailMatrix") {
884             val thumbnailPosition =
885                 viewModel?.getThumbnailPosition(container.thumbnailData, width, height, isLayoutRtl)
886                     ?: return null
887             container.updateThumbnailMatrix(thumbnailPosition.matrix)
888             return thumbnailPosition
889         }
890 
891     override fun onDetachedFromWindow() =
892         traceSection("TaskView.onDetachedFromWindow") {
893             super.onDetachedFromWindow()
894             if (enableRefactorTaskThumbnail()) {
895                 // The jobs are being cancelled in the background thread. So we make a copy of the
896                 // list to prevent cleaning a new job that might be added to this list during
897                 // onAttach or another moment in the lifecycle.
898                 val coroutineJobsToCancel = coroutineJobs.toList()
899                 coroutineJobs.clear()
900                 coroutineScope.launch(dispatcherProvider.background) {
901                     traceSection("TaskView.onDetachedFromWindow.cancellingJobs") {
902                         coroutineJobsToCancel.forEach {
903                             it.cancel("TaskView detaching from window")
904                         }
905                     }
906                 }
907             }
908         }
909 
910     /** Updates this task view to the given {@param task}. */
911     open fun bind(
912         task: Task,
913         orientedState: RecentsOrientedState,
914         taskOverlayFactory: TaskOverlayFactory,
915     ) {
916         cancelPendingLoadTasks()
917         this.orientedState = orientedState // Needed for dependencies
918         taskContainers =
919             listOf(
920                 createTaskContainer(
921                     task,
922                     R.id.snapshot,
923                     R.id.icon,
924                     R.id.show_windows,
925                     R.id.digital_wellbeing_toast,
926                     STAGE_POSITION_UNDEFINED,
927                     taskOverlayFactory,
928                 )
929             )
930         onBind(orientedState)
931     }
932 
933     protected open fun onBind(orientedState: RecentsOrientedState) =
934         traceSection("TaskView.onBind") {
935             traceSection("TaskView.onBind.createViewModel") {
936                 if (enableRefactorTaskThumbnail()) {
937                     val scopeId = context
938                     Log.d(TAG, "onBind $scopeId ${orientedState.containerInterface}")
939                     viewModel =
940                         TaskViewModel(
941                                 taskViewType = type,
942                                 recentsViewData = RecentsDependencies.get(scopeId),
943                                 getTaskUseCase = RecentsDependencies.get(scopeId),
944                                 getSysUiStatusNavFlagsUseCase = RecentsDependencies.get(scopeId),
945                                 isThumbnailValidUseCase = RecentsDependencies.get(scopeId),
946                                 getThumbnailPositionUseCase = RecentsDependencies.get(scopeId),
947                                 dispatcherProvider = RecentsDependencies.get(scopeId),
948                             )
949                             .apply { bind(*taskIds) }
950                 }
951             }
952 
953             taskContainers.forEach { container ->
954                 container.bind()
955                 if (enableRefactorTaskThumbnail()) {
956                     container.thumbnailView.cornerRadius =
957                         thumbnailFullscreenParams.currentCornerRadius
958                     container.thumbnailView.doOnSizeChange { width, height ->
959                         updateThumbnailValidity(container)
960                         val thumbnailPosition = updateThumbnailMatrix(container, width, height)
961                         container.refreshOverlay(thumbnailPosition)
962                     }
963                 }
964             }
965             setOrientationState(orientedState)
966         }
967 
968     private fun applyThumbnailSplashAlpha() {
969         val alpha = getSplashAlphaProgress()
970         taskContainers.forEach { it.updateThumbnailSplashProgress(alpha) }
971     }
972 
973     private fun getSplashAlphaProgress(): Float =
974         when {
975             !enableRefactorTaskThumbnail() -> taskThumbnailSplashAlpha
976             splitSplashAlpha > 0f -> splitSplashAlpha
977             shouldShowSplash() -> taskThumbnailSplashAlpha
978             else -> 0f
979         }
980 
981     internal fun shouldShowSplash(): Boolean = taskContainers.any { !it.isThumbnailValid }
982 
983     protected fun createTaskContainer(
984         task: Task,
985         @IdRes thumbnailViewId: Int,
986         @IdRes iconViewId: Int,
987         @IdRes showWindowViewId: Int,
988         @IdRes digitalWellbeingBannerId: Int,
989         @StagePosition stagePosition: Int,
990         taskOverlayFactory: TaskOverlayFactory,
991     ): TaskContainer =
992         traceSection("TaskView.createTaskContainer") {
993             val iconView = findViewById<View>(iconViewId) as TaskViewIcon
994             return TaskContainer(
995                 this,
996                 task,
997                 findViewById(thumbnailViewId),
998                 iconView,
999                 TransformingTouchDelegate(iconView.asView()),
1000                 stagePosition,
1001                 findViewById(digitalWellbeingBannerId)!!,
1002                 findViewById(showWindowViewId)!!,
1003                 taskOverlayFactory,
1004             )
1005         }
1006 
1007     fun containsMultipleTasks() = taskContainers.size > 1
1008 
1009     /**
1010      * Returns the TaskContainer corresponding to a given taskId, or null if the TaskView does not
1011      * contain a Task with that ID.
1012      */
1013     fun getTaskContainerById(taskId: Int) = taskContainers.firstOrNull { it.task.key.id == taskId }
1014 
1015     /** Check if given `taskId` is tracked in this view */
1016     fun containsTaskId(taskId: Int) = getTaskContainerById(taskId) != null
1017 
1018     open fun setOrientationState(orientationState: RecentsOrientedState) =
1019         traceSection("TaskView.setOrientationState") {
1020             this.orientedState = orientationState
1021             taskContainers.forEach { it.iconView.setIconOrientation(orientationState, isGridTask) }
1022             setThumbnailOrientation(orientationState)
1023         }
1024 
1025     protected open fun setThumbnailOrientation(orientationState: RecentsOrientedState) {
1026         taskContainers.forEach {
1027             it.overlay.updateOrientationState(orientationState)
1028             it.digitalWellBeingToast?.initialize()
1029         }
1030     }
1031 
1032     /**
1033      * Updates TaskView scaling and translation required to support variable width if enabled, while
1034      * ensuring TaskView fits into screen in fullscreen.
1035      */
1036     open fun updateTaskSize(lastComputedTaskSize: Rect, lastComputedGridTaskSize: Rect) {
1037         val thumbnailPadding = container.deviceProfile.overviewTaskThumbnailTopMarginPx
1038         val taskWidth = lastComputedTaskSize.width()
1039         val taskHeight = lastComputedTaskSize.height()
1040         val nonGridScale: Float
1041         val boxTranslationY: Float
1042         val expectedWidth: Int
1043         val expectedHeight: Int
1044         if (container.deviceProfile.isTablet) {
1045             val boxWidth: Int
1046             val boxHeight: Int
1047 
1048             // Focused task and Desktop tasks should use focusTaskRatio that is associated
1049             // with the original orientation of the focused task.
1050             if (isLargeTile) {
1051                 boxWidth = taskWidth
1052                 boxHeight = taskHeight
1053             } else {
1054                 // Otherwise task is in grid, and should use lastComputedGridTaskSize.
1055                 boxWidth = lastComputedGridTaskSize.width()
1056                 boxHeight = lastComputedGridTaskSize.height()
1057             }
1058 
1059             // Bound width/height to the box size.
1060             expectedWidth = boxWidth
1061             expectedHeight = boxHeight + thumbnailPadding
1062 
1063             // Scale to to fit task Rect.
1064             nonGridScale = taskWidth / boxWidth.toFloat()
1065 
1066             // Align to top of task Rect.
1067             boxTranslationY = (expectedHeight - thumbnailPadding - taskHeight) / 2.0f
1068         } else {
1069             nonGridScale = 1f
1070             boxTranslationY = 0f
1071             expectedWidth = taskWidth
1072             expectedHeight = taskHeight + thumbnailPadding
1073         }
1074         this.nonGridScale = nonGridScale
1075         this.boxTranslationY = boxTranslationY
1076         updateLayoutParams<ViewGroup.LayoutParams> {
1077             width = expectedWidth
1078             height = expectedHeight
1079         }
1080         updateThumbnailSize()
1081     }
1082 
1083     protected open fun updateThumbnailSize() {
1084         // TODO(b/271468547), we should default to setting translations only on the snapshot instead
1085         //  of a hybrid of both margins and translations
1086         firstTaskContainer?.snapshotView?.updateLayoutParams<LayoutParams> {
1087             topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx
1088         }
1089         taskContainers.forEach { it.digitalWellBeingToast?.setupLayout() }
1090     }
1091 
1092     /** Returns the thumbnail's bounds, optionally relative to the screen. */
1093     @JvmOverloads
1094     open fun getThumbnailBounds(bounds: Rect, relativeToDragLayer: Boolean = false) {
1095         bounds.setEmpty()
1096         taskContainers.forEach {
1097             val thumbnailBounds = Rect()
1098             if (relativeToDragLayer) {
1099                 container.dragLayer.getDescendantRectRelativeToSelf(
1100                     it.snapshotView,
1101                     thumbnailBounds,
1102                 )
1103             } else {
1104                 thumbnailBounds.set(it.snapshotView)
1105             }
1106             bounds.union(thumbnailBounds)
1107         }
1108     }
1109 
1110     /**
1111      * See [TaskDataChanges]
1112      *
1113      * @param visible If this task view will be visible to the user in overview or hidden
1114      */
1115     fun onTaskListVisibilityChanged(visible: Boolean) {
1116         onTaskListVisibilityChanged(visible, FLAG_UPDATE_ALL)
1117     }
1118 
1119     /**
1120      * See [TaskDataChanges]
1121      *
1122      * @param visible If this task view will be visible to the user in overview or hidden
1123      */
1124     open fun onTaskListVisibilityChanged(visible: Boolean, @TaskDataChanges changes: Int) {
1125         cancelPendingLoadTasks()
1126         val recentsModel = RecentsModel.INSTANCE.get(context)
1127         // These calls are no-ops if the data is already loaded, try and load the high
1128         // resolution thumbnail if the state permits
1129         if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL) && !enableRefactorTaskThumbnail()) {
1130             taskContainers.forEach {
1131                 if (visible) {
1132                     recentsModel.thumbnailCache
1133                         .getThumbnailInBackground(it.task) { thumbnailData ->
1134                             it.task.thumbnail = thumbnailData
1135                             it.thumbnailViewDeprecated.setThumbnail(it.task, thumbnailData)
1136                         }
1137                         ?.also { request -> pendingThumbnailLoadRequests.add(request) }
1138                 } else {
1139                     it.thumbnailViewDeprecated.setThumbnail(null, null)
1140                     // Reset the task thumbnail reference as well (it will be fetched from the
1141                     // cache or reloaded next time we need it)
1142                     it.task.thumbnail = null
1143                 }
1144             }
1145         }
1146         if (needsUpdate(changes, FLAG_UPDATE_ICON) && !enableOverviewIconMenu()) {
1147             taskContainers.forEach {
1148                 if (visible) {
1149                     recentsModel.iconCache
1150                         .getIconInBackground(it.task) { icon, contentDescription, title ->
1151                             it.task.icon = icon
1152                             it.task.titleDescription = contentDescription
1153                             it.task.title = title
1154                             onIconLoaded(it)
1155                         }
1156                         ?.also { request -> pendingIconLoadRequests.add(request) }
1157                 } else {
1158                     onIconUnloaded(it)
1159                 }
1160             }
1161         }
1162         if (needsUpdate(changes, FLAG_UPDATE_CORNER_RADIUS)) {
1163             thumbnailFullscreenParams.updateCornerRadius(context)
1164         }
1165     }
1166 
1167     protected open fun needsUpdate(@TaskDataChanges dataChange: Int, @TaskDataChanges flag: Int) =
1168         (dataChange and flag) == flag
1169 
1170     protected open fun cancelPendingLoadTasks() =
1171         traceSection("TaskView.cancelPendingLoadTasks") {
1172             pendingThumbnailLoadRequests.forEach { it.cancel() }
1173             pendingThumbnailLoadRequests.clear()
1174             pendingIconLoadRequests.forEach { it.cancel() }
1175             pendingIconLoadRequests.clear()
1176         }
1177 
1178     protected open fun setIconState(container: TaskContainer, state: TaskData?) =
1179         traceSection("TaskView.setIconState") {
1180             if (enableOverviewIconMenu()) {
1181                 if (state is TaskData.Data) {
1182                     setIcon(container.iconView, state.icon)
1183                     container.iconView.setText(state.title)
1184                     container.digitalWellBeingToast?.initialize()
1185                 } else {
1186                     setIcon(container.iconView, null)
1187                     container.iconView.setText(null)
1188                 }
1189             }
1190         }
1191 
1192     protected open fun onIconLoaded(taskContainer: TaskContainer) {
1193         setIcon(taskContainer.iconView, taskContainer.task.icon)
1194         if (enableOverviewIconMenu()) {
1195             taskContainer.iconView.setText(taskContainer.task.title)
1196         }
1197         taskContainer.digitalWellBeingToast?.initialize()
1198     }
1199 
1200     protected open fun onIconUnloaded(taskContainer: TaskContainer) {
1201         setIcon(taskContainer.iconView, null)
1202         if (enableOverviewIconMenu()) {
1203             taskContainer.iconView.setText(null)
1204         }
1205     }
1206 
1207     protected fun setIcon(iconView: TaskViewIcon, icon: Drawable?) {
1208         with(iconView) {
1209             if (icon != null) {
1210                 setDrawable(icon)
1211                 setOnClickListener {
1212                     if (!confirmSecondSplitSelectApp()) {
1213                         showTaskMenu(this)
1214                     }
1215                 }
1216                 setOnLongClickListener {
1217                     requestDisallowInterceptTouchEvent(true)
1218                     showTaskMenu(this)
1219                 }
1220             } else {
1221                 setDrawable(null)
1222                 setOnClickListener(null)
1223                 setOnLongClickListener(null)
1224             }
1225         }
1226     }
1227 
1228     @JvmOverloads
1229     open fun setShouldShowScreenshot(
1230         shouldShowScreenshot: Boolean,
1231         thumbnailDatas: Map<Int, ThumbnailData?>? = null,
1232     ) {
1233         if (this.shouldShowScreenshot == shouldShowScreenshot) return
1234         this.shouldShowScreenshot = shouldShowScreenshot
1235         if (enableRefactorTaskThumbnail()) {
1236             return
1237         }
1238 
1239         taskContainers.forEach {
1240             val thumbnailData = thumbnailDatas?.get(it.task.key.id)
1241             if (thumbnailData != null) {
1242                 it.thumbnailViewDeprecated.setThumbnail(it.task, thumbnailData)
1243             } else {
1244                 it.thumbnailViewDeprecated.refresh()
1245             }
1246         }
1247     }
1248 
1249     private fun onClick() {
1250         if (confirmSecondSplitSelectApp()) {
1251             Log.d("b/310064698", "${taskIds.contentToString()} - onClick - split select is active")
1252             return
1253         }
1254         val callbackList =
1255             launchWithAnimation()?.apply {
1256                 add {
1257                     Log.d("b/310064698", "${taskIds.contentToString()} - onClick - launchCompleted")
1258                 }
1259             }
1260         Log.d("b/310064698", "${taskIds.contentToString()} - onClick - callbackList: $callbackList")
1261         container.statsLogManager
1262             .logger()
1263             .withItemInfo(itemInfo)
1264             .log(LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP)
1265     }
1266 
1267     /** Launch of the current task (both live and inactive tasks) with an animation. */
1268     fun launchWithAnimation(): RunnableList? {
1269         return if (isRunningTask && recentsView?.remoteTargetHandles != null) {
1270             launchAsLiveTile(recentsView?.remoteTargetHandles!!)
1271         } else {
1272             launchAsStaticTile()
1273         }
1274     }
1275 
1276     private fun launchAsLiveTile(remoteTargetHandles: Array<RemoteTargetHandle>): RunnableList? {
1277         val recentsView = recentsView ?: return null
1278         if (!isClickableAsLiveTile) {
1279             Log.e(
1280                 TAG,
1281                 "launchAsLiveTile - TaskView is not clickable as a live tile; returning to home: ${taskIds.contentToString()}",
1282             )
1283             return null
1284         }
1285         isClickableAsLiveTile = false
1286         val targets =
1287             if (remoteTargetHandles.isNotEmpty()) {
1288                 if (remoteTargetHandles.size == 1) {
1289                     remoteTargetHandles[0].transformParams.targetSet
1290                 } else {
1291                     val apps =
1292                         remoteTargetHandles.flatMap {
1293                             it.transformParams.targetSet.apps.asIterable()
1294                         }
1295                     val wallpapers =
1296                         remoteTargetHandles.flatMap {
1297                             it.transformParams.targetSet.wallpapers.asIterable()
1298                         }
1299                     RemoteAnimationTargets(
1300                         apps.toTypedArray(),
1301                         wallpapers.toTypedArray(),
1302                         remoteTargetHandles[0].transformParams.targetSet.nonApps,
1303                         remoteTargetHandles[0].transformParams.targetSet.targetMode,
1304                     )
1305                 }
1306             } else {
1307                 null
1308             }
1309         if (targets == null) {
1310             // If the recents animation is cancelled somehow between the parent if block and
1311             // here, try to launch the task as a non live tile task.
1312             val runnableList = launchAsStaticTile()
1313             if (runnableList == null) {
1314                 Log.e(
1315                     TAG,
1316                     "launchAsLiveTile - Recents animation cancelled and cannot launch task as non-live tile; returning to home: ${taskIds.contentToString()}",
1317                 )
1318             }
1319             isClickableAsLiveTile = true
1320             return runnableList
1321         }
1322         TestLogging.recordEvent(
1323             TestProtocol.SEQUENCE_MAIN,
1324             "composeRecentsLaunchAnimator",
1325             taskIds.contentToString(),
1326         )
1327         val runnableList = RunnableList()
1328         with(AnimatorSet()) {
1329             TaskViewUtils.composeRecentsLaunchAnimator(
1330                 this,
1331                 this@TaskView,
1332                 targets.apps,
1333                 targets.wallpapers,
1334                 targets.nonApps,
1335                 true, /* launcherClosing */
1336                 recentsView.stateManager,
1337                 recentsView,
1338                 recentsView.depthController,
1339                 /* transitionInfo= */ null,
1340             )
1341             addListener(
1342                 object : AnimatorListenerAdapter() {
1343                     override fun onAnimationEnd(animator: Animator) {
1344                         if (taskContainers.any { it.task.key.displayId != rootViewDisplayId }) {
1345                             launchAsStaticTile()
1346                         }
1347                         isClickableAsLiveTile = true
1348                         runEndCallback()
1349                     }
1350 
1351                     override fun onAnimationCancel(animation: Animator) {
1352                         runEndCallback()
1353                     }
1354 
1355                     private fun runEndCallback() {
1356                         runnableList.executeAllAndDestroy()
1357                     }
1358                 }
1359             )
1360             start()
1361         }
1362         Log.d(TAG, "launchAsLiveTile - composeRecentsLaunchAnimator: ${taskIds.contentToString()}")
1363         recentsView.onTaskLaunchedInLiveTileMode()
1364         return runnableList
1365     }
1366 
1367     /**
1368      * Starts the task associated with this view and animates the startup.
1369      *
1370      * @return CompletionStage to indicate the animation completion or null if the launch failed.
1371      */
1372     open fun launchAsStaticTile(): RunnableList? {
1373         val firstTaskContainer = firstTaskContainer ?: return null
1374         TestLogging.recordEvent(
1375             TestProtocol.SEQUENCE_MAIN,
1376             "startActivityFromRecentsAsync",
1377             taskIds.contentToString(),
1378         )
1379         val opts =
1380             container.getActivityLaunchOptions(this, null).apply {
1381                 options.launchDisplayId = displayId
1382             }
1383         if (
1384             ActivityManagerWrapper.getInstance()
1385                 .startActivityFromRecents(firstTaskContainer.task.key, opts.options)
1386         ) {
1387             Log.d(
1388                 TAG,
1389                 "launchAsStaticTile - startActivityFromRecents: ${taskIds.contentToString()}",
1390             )
1391             ActiveGestureLog.INSTANCE.trackEvent(
1392                 ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED
1393             )
1394             val recentsView = recentsView ?: return null
1395             if (
1396                 recentsView.runningTaskViewId != -1 &&
1397                     recentsView.mRecentsAnimationController != null
1398             ) {
1399                 recentsView.onTaskLaunchedInLiveTileMode()
1400 
1401                 // Return a fresh callback in the live tile case, so that it's not accidentally
1402                 // triggered by QuickstepTransitionManager.AppLaunchAnimationRunner.
1403                 return RunnableList().also { recentsView.addSideTaskLaunchCallback(it) }
1404             }
1405             // If the recents transition is running (ie. in live tile mode), then the start
1406             // of a new task will merge into the existing transition and it currently will
1407             // not be run independently, so we need to rely on the onTaskAppeared() call
1408             // for the new task to trigger the side launch callback to flush this runnable
1409             // list (which is usually flushed when the app launch animation finishes)
1410             recentsView.addSideTaskLaunchCallback(opts.onEndCallback)
1411             return opts.onEndCallback
1412         } else {
1413             notifyTaskLaunchFailed("launchAsStaticTile")
1414             return null
1415         }
1416     }
1417 
1418     /** Starts the task associated with this view without any animation */
1419     @JvmOverloads
1420     open fun launchWithoutAnimation(
1421         isQuickSwitch: Boolean = false,
1422         callback: (launched: Boolean) -> Unit,
1423     ) {
1424         val firstTaskContainer = firstTaskContainer ?: return
1425         TestLogging.recordEvent(
1426             TestProtocol.SEQUENCE_MAIN,
1427             "startActivityFromRecentsAsync",
1428             taskIds.contentToString(),
1429         )
1430         val failureListener = TaskRemovedDuringLaunchListener(context.applicationContext)
1431         if (isQuickSwitch) {
1432             // We only listen for failures to launch in quickswitch because the during this
1433             // gesture launcher is in the background state, vs other launches which are in
1434             // the actual overview state
1435             failureListener.register(container, firstTaskContainer.task.key.id) {
1436                 notifyTaskLaunchFailed("launchWithoutAnimation")
1437                 recentsView?.let {
1438                     // Disable animations for now, as it is an edge case and the app usually
1439                     // covers launcher and also any state transition animation also gets
1440                     // clobbered by QuickstepTransitionManager.createWallpaperOpenAnimations
1441                     // when launcher shows again
1442                     it.startHome(false /* animated */)
1443                     // LauncherTaskbarUIController depends on the launcher state when
1444                     // checking whether to handle resume, but that can come in before
1445                     // startHome() changes the state, so force-refresh here to ensure the
1446                     // taskbar is updated
1447                     it.mSizeStrategy.taskbarController?.refreshResumedState()
1448                 }
1449             }
1450         }
1451         // Indicate success once the system has indicated that the transition has started
1452         val opts =
1453             ActivityOptions.makeCustomTaskAnimation(
1454                     context,
1455                     0,
1456                     0,
1457                     Executors.MAIN_EXECUTOR.handler,
1458                     { callback(true) },
1459                 ) {
1460                     failureListener.onTransitionFinished()
1461                 }
1462                 .apply {
1463                     launchDisplayId = display?.displayId ?: Display.DEFAULT_DISPLAY
1464                     if (isQuickSwitch) {
1465                         setFreezeRecentTasksReordering()
1466                     }
1467                     // TODO(b/331754864): Update this to use TV.shouldShowSplash
1468                     disableStartingWindow = firstTaskContainer.shouldShowSplashView
1469                 }
1470         Executors.UI_HELPER_EXECUTOR.execute {
1471             if (
1472                 !ActivityManagerWrapper.getInstance()
1473                     .startActivityFromRecents(firstTaskContainer.task.key, opts)
1474             ) {
1475                 // If the call to start activity failed, then post the result immediately,
1476                 // otherwise, wait for the animation start callback from the activity options
1477                 // above
1478                 Executors.MAIN_EXECUTOR.post {
1479                     notifyTaskLaunchFailed("launchTask")
1480                     callback(false)
1481                 }
1482             }
1483             Log.d(
1484                 TAG,
1485                 "launchWithoutAnimation - startActivityFromRecents: ${taskIds.contentToString()}",
1486             )
1487         }
1488     }
1489 
1490     private fun notifyTaskLaunchFailed(launchMethod: String) {
1491         val sb =
1492             StringBuilder("$launchMethod - Failed to launch task: ${taskIds.contentToString()}\n")
1493         taskContainers.forEach {
1494             sb.append("(task=${it.task.key.baseIntent} userId=${it.task.key.userId})\n")
1495         }
1496         Log.w(TAG, sb.toString())
1497         Toast.makeText(context, R.string.activity_not_available, Toast.LENGTH_SHORT).show()
1498     }
1499 
1500     /**
1501      * Returns `true` if user is already in split select mode and this tap was to choose the second
1502      * app. `false` otherwise
1503      */
1504     protected open fun confirmSecondSplitSelectApp(): Boolean {
1505         val index = getLastSelectedChildTaskIndex()
1506         if (index >= taskContainers.size) {
1507             return false
1508         }
1509         val container = taskContainers[index]
1510         val recentsView = recentsView ?: return false
1511         return recentsView.confirmSplitSelect(
1512             this,
1513             container.task,
1514             container.iconView.drawable,
1515             container.snapshotView,
1516             container.thumbnail,
1517             /* intent */ null,
1518             /* user */ null,
1519             container.itemInfo,
1520         )
1521     }
1522 
1523     /**
1524      * Returns the task index of the last selected child task (0 or 1). If we contain multiple tasks
1525      * and this TaskView is used as part of split selection, the selected child task index will be
1526      * that of the remaining task.
1527      */
1528     protected open fun getLastSelectedChildTaskIndex() = 0
1529 
1530     private fun showTaskMenu(iconView: TaskViewIcon): Boolean {
1531         val recentsView = recentsView ?: return false
1532         if (!recentsView.canLaunchFullscreenTask()) {
1533             // Don't show menu when selecting second split screen app
1534             return true
1535         }
1536         if (!container.deviceProfile.isTablet && !recentsView.isClearAllHidden) {
1537             recentsView.snapToPage(recentsView.indexOfChild(this))
1538             return false
1539         }
1540         val menuContainer = taskContainers.firstOrNull { it.iconView === iconView } ?: return false
1541         container.statsLogManager
1542             .logger()
1543             .withItemInfo(menuContainer.itemInfo)
1544             .log(LauncherEvent.LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS)
1545         return showTaskMenuWithContainer(menuContainer)
1546     }
1547 
1548     private fun closeTaskMenu(): Boolean {
1549         val floatingView: AbstractFloatingView? =
1550             AbstractFloatingView.getTopOpenViewWithType(
1551                 container,
1552                 AbstractFloatingView.TYPE_TASK_MENU,
1553             )
1554         if (floatingView?.isOpen == true) {
1555             floatingView.close(true)
1556             return true
1557         } else {
1558             return false
1559         }
1560     }
1561 
1562     private fun showTaskMenuWithContainer(menuContainer: TaskContainer): Boolean {
1563         val recentsView = recentsView ?: return false
1564         if (enableHoverOfChildElementsInTaskview()) {
1565             // Disable hover on all TaskView's whilst menu is showing.
1566             recentsView.setTaskBorderEnabled(false)
1567         }
1568         return if (enableOverviewIconMenu() && menuContainer.iconView is IconAppChipView) {
1569             if (menuContainer.iconView.status == AppChipStatus.Expanded) {
1570                 closeTaskMenu()
1571             } else {
1572                 menuContainer.iconView.revealAnim(/* isRevealing= */ true)
1573                 TaskMenuView.showForTask(menuContainer) {
1574                     val isAnimated = !recentsView.isSplitSelectionActive
1575                     menuContainer.iconView.revealAnim(/* isRevealing= */ false, isAnimated)
1576                     if (enableHoverOfChildElementsInTaskview()) {
1577                         recentsView.setTaskBorderEnabled(true)
1578                     }
1579                 }
1580             }
1581         } else if (container.deviceProfile.isTablet) {
1582             val alignedOptionIndex =
1583                 if (
1584                     recentsView.isOnGridBottomRow(menuContainer.taskView) &&
1585                         container.deviceProfile.isLandscape
1586                 ) {
1587                     if (enableGridOnlyOverview()) {
1588                         // With no focused task, there is less available space below the tasks, so
1589                         // align the arrow to the third option in the menu.
1590                         2
1591                     } else {
1592                         // Bottom row of landscape grid aligns arrow to second option to avoid
1593                         // clipping
1594                         1
1595                     }
1596                 } else {
1597                     0
1598                 }
1599             TaskMenuViewWithArrow.showForTask(menuContainer, alignedOptionIndex) {
1600                 if (enableHoverOfChildElementsInTaskview()) {
1601                     recentsView.setTaskBorderEnabled(true)
1602                 }
1603             }
1604         } else {
1605             TaskMenuView.showForTask(menuContainer) {
1606                 if (enableHoverOfChildElementsInTaskview()) {
1607                     recentsView.setTaskBorderEnabled(true)
1608                 }
1609             }
1610         }
1611     }
1612 
1613     /**
1614      * Whether the taskview should take the touch event from parent. Events passed to children that
1615      * might require special handling.
1616      */
1617     open fun offerTouchToChildren(event: MotionEvent): Boolean {
1618         taskContainers.forEach {
1619             if (event.action == MotionEvent.ACTION_DOWN) {
1620                 computeAndSetIconTouchDelegate(it.iconView, tempCoordinates, it.iconTouchDelegate)
1621                 if (it.iconTouchDelegate.onTouchEvent(event)) {
1622                     return true
1623                 }
1624             }
1625         }
1626         return false
1627     }
1628 
1629     private fun computeAndSetIconTouchDelegate(
1630         view: TaskViewIcon,
1631         tempCenterCoordinates: FloatArray,
1632         transformingTouchDelegate: TransformingTouchDelegate,
1633     ) {
1634         val viewHalfWidth = view.width / 2f
1635         val viewHalfHeight = view.height / 2f
1636         Utilities.getDescendantCoordRelativeToAncestor(
1637             view.asView(),
1638             container.dragLayer,
1639             tempCenterCoordinates.apply {
1640                 this[0] = viewHalfWidth
1641                 this[1] = viewHalfHeight
1642             },
1643             false,
1644         )
1645         transformingTouchDelegate.setBounds(
1646             (tempCenterCoordinates[0] - viewHalfWidth).toInt(),
1647             (tempCenterCoordinates[1] - viewHalfHeight).toInt(),
1648             (tempCenterCoordinates[0] + viewHalfWidth).toInt(),
1649             (tempCenterCoordinates[1] + viewHalfHeight).toInt(),
1650         )
1651     }
1652 
1653     /** Sets up an on-click listener and the visibility for show_windows icon on top of the task. */
1654     open fun setUpShowAllInstancesListener() {
1655         taskContainers.forEach {
1656             it.showWindowsView?.let { showWindowsView ->
1657                 updateFilterCallback(
1658                     showWindowsView,
1659                     getFilterUpdateCallback(it.task.key.packageName),
1660                 )
1661             }
1662         }
1663     }
1664 
1665     /**
1666      * Returns a callback that updates the state of the filter and the recents overview
1667      *
1668      * @param taskPackageName package name of the task to filter by
1669      */
1670     private fun getFilterUpdateCallback(taskPackageName: String?) =
1671         if (recentsView?.filterState?.shouldShowFilterUI(taskPackageName) == true)
1672             OnClickListener { recentsView?.setAndApplyFilter(taskPackageName) }
1673         else null
1674 
1675     /**
1676      * Sets the correct visibility and callback on the provided filterView based on whether the
1677      * callback is null or not
1678      */
1679     private fun updateFilterCallback(filterView: View, callback: OnClickListener?) {
1680         // Filtering changes alpha instead of the visibility since visibility
1681         // can be altered separately through RecentsView#resetFromSplitSelectionState()
1682         with(filterView) {
1683             alpha = if (callback == null) 0f else 1f
1684             setOnClickListener(callback)
1685         }
1686     }
1687 
1688     /**
1689      * Called to animate a smooth transition when going directly from an app into Overview (and vice
1690      * versa). Icons fade in, and DWB banners slide in with a "shift up" animation.
1691      */
1692     private fun onSettledProgressUpdated(settledProgress: Float) {
1693         taskContainers.forEach {
1694             it.iconView.setContentAlpha(settledProgress)
1695             it.digitalWellBeingToast?.bannerOffsetPercentage = 1f - settledProgress
1696         }
1697     }
1698 
1699     fun startIconFadeInOnGestureComplete() {
1700         iconFadeInOnGestureCompleteAnimator?.cancel()
1701         iconFadeInOnGestureCompleteAnimator =
1702             ObjectAnimator.ofFloat(this, SETTLED_PROGRESS_GESTURE, 1f).apply {
1703                 duration = FADE_IN_ICON_DURATION
1704                 interpolator = Interpolators.LINEAR
1705                 addListener(
1706                     object : AnimatorListenerAdapter() {
1707                         override fun onAnimationEnd(animation: Animator) {
1708                             iconFadeInOnGestureCompleteAnimator = null
1709                         }
1710                     }
1711                 )
1712                 start()
1713             }
1714     }
1715 
1716     fun setIconVisibleForGesture(isVisible: Boolean) {
1717         iconFadeInOnGestureCompleteAnimator?.cancel()
1718         settledProgressGesture = if (isVisible) 1f else 0f
1719     }
1720 
1721     /** Set a color tint on the snapshot and supporting views. */
1722     open fun setColorTint(amount: Float, tintColor: Int) {
1723         taskContainers.forEach {
1724             if (enableRefactorTaskThumbnail()) {
1725                 it.updateTintAmount(amount)
1726             } else {
1727                 it.thumbnailViewDeprecated.dimAlpha = amount
1728             }
1729             it.iconView.setIconColorTint(tintColor, amount)
1730             it.digitalWellBeingToast?.setColorTint(tintColor, amount)
1731         }
1732     }
1733 
1734     /**
1735      * Sets visibility for the thumbnail and associated elements (DWB banners and action chips).
1736      * IconView is unaffected.
1737      *
1738      * @param taskId is only used when setting visibility to a non-[View.VISIBLE] value
1739      */
1740     open fun setThumbnailVisibility(visibility: Int, taskId: Int) {
1741         taskContainers.forEach {
1742             if (visibility == VISIBLE || it.task.key.id == taskId) {
1743                 it.snapshotView.visibility = visibility
1744                 it.digitalWellBeingToast?.visibility = visibility
1745                 it.showWindowsView?.visibility = visibility
1746                 it.overlay.setVisibility(visibility)
1747             }
1748         }
1749     }
1750 
1751     open fun setOverlayEnabled(overlayEnabled: Boolean) {
1752         if (!enableRefactorTaskThumbnail()) {
1753             taskContainers.forEach { it.setOverlayEnabled(overlayEnabled) }
1754         }
1755     }
1756 
1757     protected open fun refreshTaskThumbnailSplash() {
1758         if (!enableRefactorTaskThumbnail()) {
1759             taskContainers.forEach { it.thumbnailViewDeprecated.refreshSplashView() }
1760         }
1761     }
1762 
1763     protected fun getScrollAdjustment(gridEnabled: Boolean) =
1764         if (gridEnabled) gridTranslationX else nonGridTranslationX
1765 
1766     fun getOffsetAdjustment(gridEnabled: Boolean) = getScrollAdjustment(gridEnabled)
1767 
1768     fun getSizeAdjustment(fullscreenEnabled: Boolean) = if (fullscreenEnabled) nonGridScale else 1f
1769 
1770     private fun applyScale() {
1771         val scale = persistentScale * dismissScale * Utilities.mapRange(modalness, 1f, modalScale)
1772         scaleX = scale
1773         scaleY = scale
1774         updateFullscreenParams()
1775     }
1776 
1777     private fun applyTranslationX() {
1778         translationX =
1779             dismissTranslationX +
1780                 taskOffsetTranslationX +
1781                 taskResistanceTranslationX +
1782                 splitSelectTranslationX +
1783                 gridEndTranslationX +
1784                 persistentTranslationX
1785     }
1786 
1787     private fun applyTranslationY() {
1788         translationY =
1789             dismissTranslationY +
1790                 taskOffsetTranslationY +
1791                 taskResistanceTranslationY +
1792                 splitSelectTranslationY +
1793                 persistentTranslationY
1794     }
1795 
1796     private fun onGridProgressChanged() {
1797         applyTranslationX()
1798         applyTranslationY()
1799         applyScale()
1800     }
1801 
1802     protected open fun onFullscreenProgressChanged(fullscreenProgress: Float) {
1803         taskContainers.forEach {
1804             if (!enableOverviewIconMenu()) {
1805                 it.iconView.setVisibility(if (fullscreenProgress < 1) VISIBLE else INVISIBLE)
1806             }
1807             it.overlay.setFullscreenProgress(fullscreenProgress)
1808         }
1809         settledProgressFullscreen =
1810             SETTLED_PROGRESS_FAST_OUT_INTERPOLATOR.getInterpolation(1 - fullscreenProgress)
1811         updateFullscreenParams()
1812     }
1813 
1814     protected open fun updateFullscreenParams() {
1815         updateFullscreenParams(thumbnailFullscreenParams)
1816         taskContainers.forEach {
1817             if (enableRefactorTaskThumbnail()) {
1818                 it.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius
1819             } else {
1820                 it.thumbnailViewDeprecated.setFullscreenParams(thumbnailFullscreenParams)
1821             }
1822             it.overlay.setFullscreenParams(thumbnailFullscreenParams)
1823         }
1824     }
1825 
1826     protected fun updateFullscreenParams(fullscreenParams: FullscreenDrawParams) {
1827         recentsView?.let { fullscreenParams.setProgress(fullscreenProgress, it.scaleX, scaleX) }
1828     }
1829 
1830     private fun onModalnessUpdated(modalness: Float) {
1831         isClickable = modalness == 0f
1832         taskContainers.forEach {
1833             it.iconView.setModalAlpha(1f - modalness)
1834             it.digitalWellBeingToast?.bannerOffsetPercentage = modalness
1835         }
1836         if (enableGridOnlyOverview()) {
1837             modalAlpha = if (isSelectedTask) 1f else (1f - modalness)
1838             applyScale()
1839         }
1840     }
1841 
1842     fun resetPersistentViewTransforms() {
1843         nonGridTranslationX = 0f
1844         gridTranslationX = 0f
1845         gridTranslationY = 0f
1846         boxTranslationY = 0f
1847         taskContainers.forEach {
1848             it.snapshotView.translationX = 0f
1849             it.snapshotView.translationY = 0f
1850         }
1851         resetViewTransforms()
1852     }
1853 
1854     fun resetViewTransforms() {
1855         // fullscreenTranslation and accumulatedTranslation should not be reset, as
1856         // resetViewTransforms is called during QuickSwitch scrolling.
1857         dismissTranslationX = 0f
1858         taskOffsetTranslationX = 0f
1859         taskResistanceTranslationX = 0f
1860         splitSelectTranslationX = 0f
1861         gridEndTranslationX = 0f
1862         dismissTranslationY = 0f
1863         taskOffsetTranslationY = 0f
1864         taskResistanceTranslationY = 0f
1865         if (recentsView?.isSplitSelectionActive != true) {
1866             splitSelectTranslationY = 0f
1867         }
1868         dismissScale = 1f
1869         translationZ = 0f
1870         setIconVisibleForGesture(true)
1871         settledProgressDismiss = 1f
1872         setColorTint(0f, 0)
1873     }
1874 
1875     private fun getGridTrans(endTranslation: Float) =
1876         Utilities.mapRange(gridProgress, 0f, endTranslation)
1877 
1878     private fun getNonGridTrans(endTranslation: Float) =
1879         endTranslation - getGridTrans(endTranslation)
1880 
1881     private fun MotionEvent.isWithinThumbnailBounds(): Boolean {
1882         return thumbnailBounds.contains(x.toInt(), y.toInt())
1883     }
1884 
1885     override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {
1886         (if (isLayoutRtl) taskContainers.reversed() else taskContainers).forEach {
1887             it.addChildForAccessibility(outChildren)
1888         }
1889     }
1890 
1891     companion object {
1892         private const val TAG = "TaskView"
1893 
1894         private enum class Alpha {
1895             Stable,
1896             Attach,
1897             Split,
1898             Modal,
1899         }
1900 
1901         private enum class SettledProgress {
1902             Fullscreen,
1903             Gesture,
1904             Dismiss,
1905         }
1906 
1907         const val FLAG_UPDATE_ICON = 1
1908         const val FLAG_UPDATE_THUMBNAIL = FLAG_UPDATE_ICON shl 1
1909         const val FLAG_UPDATE_CORNER_RADIUS = FLAG_UPDATE_THUMBNAIL shl 1
1910         const val FLAG_UPDATE_ALL =
1911             (FLAG_UPDATE_ICON or FLAG_UPDATE_THUMBNAIL or FLAG_UPDATE_CORNER_RADIUS)
1912 
1913         /** The maximum amount that a task view can be scrimmed, dimmed or tinted. */
1914         const val MAX_PAGE_SCRIM_ALPHA = 0.4f
1915         const val FADE_IN_ICON_DURATION: Long = 120
1916         private const val DIM_ANIM_DURATION: Long = 700
1917         private const val SETTLE_TRANSITION_THRESHOLD =
1918             FADE_IN_ICON_DURATION.toFloat() / DIM_ANIM_DURATION
1919         val SETTLED_PROGRESS_FAST_OUT_INTERPOLATOR =
1920             Interpolators.clampToProgress(
1921                 Interpolators.FAST_OUT_SLOW_IN,
1922                 1f - SETTLE_TRANSITION_THRESHOLD,
1923                 1f,
1924             )!!
1925         private val FADE_IN_ICON_INTERPOLATOR = Interpolators.LINEAR
1926         private val SYSTEM_GESTURE_EXCLUSION_RECT = listOf(Rect())
1927 
1928         private val SETTLED_PROGRESS: FloatProperty<TaskView> =
1929             KFloatProperty(TaskView::settledProgress)
1930 
1931         private val SETTLED_PROGRESS_GESTURE: FloatProperty<TaskView> =
1932             KFloatProperty(TaskView::settledProgressGesture)
1933 
1934         private val SETTLED_PROGRESS_DISMISS: FloatProperty<TaskView> =
1935             KFloatProperty(TaskView::settledProgressDismiss)
1936 
1937         private val SPLIT_SELECT_TRANSLATION_X: FloatProperty<TaskView> =
1938             KFloatProperty(TaskView::splitSelectTranslationX)
1939 
1940         private val SPLIT_SELECT_TRANSLATION_Y: FloatProperty<TaskView> =
1941             KFloatProperty(TaskView::splitSelectTranslationY)
1942 
1943         private val DISMISS_TRANSLATION_X: FloatProperty<TaskView> =
1944             KFloatProperty(TaskView::dismissTranslationX)
1945 
1946         private val DISMISS_TRANSLATION_Y: FloatProperty<TaskView> =
1947             KFloatProperty(TaskView::dismissTranslationY)
1948 
1949         private val TASK_OFFSET_TRANSLATION_X: FloatProperty<TaskView> =
1950             KFloatProperty(TaskView::taskOffsetTranslationX)
1951 
1952         private val TASK_OFFSET_TRANSLATION_Y: FloatProperty<TaskView> =
1953             KFloatProperty(TaskView::taskOffsetTranslationY)
1954 
1955         private val TASK_RESISTANCE_TRANSLATION_X: FloatProperty<TaskView> =
1956             KFloatProperty(TaskView::taskResistanceTranslationX)
1957 
1958         private val TASK_RESISTANCE_TRANSLATION_Y: FloatProperty<TaskView> =
1959             KFloatProperty(TaskView::taskResistanceTranslationY)
1960 
1961         @JvmField
1962         val GRID_END_TRANSLATION_X: FloatProperty<TaskView> =
1963             KFloatProperty(TaskView::gridEndTranslationX)
1964 
1965         @JvmField
1966         val DISMISS_SCALE: FloatProperty<TaskView> = KFloatProperty(TaskView::dismissScale)
1967 
1968         @JvmField val SPLIT_ALPHA: FloatProperty<TaskView> = KFloatProperty(TaskView::splitAlpha)
1969     }
1970 }
1971