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