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