1 /*
2  * Copyright (C) 2024 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.orientation
17 
18 import android.annotation.SuppressLint
19 import android.content.res.Resources
20 import android.graphics.Point
21 import android.graphics.PointF
22 import android.graphics.Rect
23 import android.graphics.RectF
24 import android.graphics.drawable.ShapeDrawable
25 import android.util.FloatProperty
26 import android.util.Pair
27 import android.view.Gravity
28 import android.view.MotionEvent
29 import android.view.Surface
30 import android.view.VelocityTracker
31 import android.view.View
32 import android.view.View.MeasureSpec
33 import android.view.ViewGroup
34 import android.view.accessibility.AccessibilityEvent
35 import android.widget.FrameLayout
36 import android.widget.LinearLayout
37 import androidx.annotation.VisibleForTesting
38 import androidx.core.util.component1
39 import androidx.core.util.component2
40 import androidx.core.view.updateLayoutParams
41 import com.android.launcher3.DeviceProfile
42 import com.android.launcher3.Flags
43 import com.android.launcher3.LauncherAnimUtils
44 import com.android.launcher3.R
45 import com.android.launcher3.Utilities
46 import com.android.launcher3.logger.LauncherAtom.TaskSwitcherContainer
47 import com.android.launcher3.touch.PagedOrientationHandler.ChildBounds
48 import com.android.launcher3.touch.PagedOrientationHandler.Float2DAction
49 import com.android.launcher3.touch.PagedOrientationHandler.Int2DAction
50 import com.android.launcher3.touch.SingleAxisSwipeDetector
51 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT
52 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT
53 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
54 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN
55 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds
56 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption
57 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
58 import com.android.launcher3.views.BaseDragLayer
59 import com.android.quickstep.views.IconAppChipView
60 import kotlin.math.max
61 
62 open class LandscapePagedViewHandler : RecentsPagedOrientationHandler {
getPrimaryValuenull63     override fun <T> getPrimaryValue(x: T, y: T): T = y
64 
65     override fun <T> getSecondaryValue(x: T, y: T): T = x
66 
67     override fun getPrimaryValue(x: Int, y: Int): Int = y
68 
69     override fun getSecondaryValue(x: Int, y: Int): Int = x
70 
71     override fun getPrimaryValue(x: Float, y: Float): Float = y
72 
73     override fun getSecondaryValue(x: Float, y: Float): Float = x
74 
75     override val isLayoutNaturalToLauncher: Boolean = false
76 
77     override fun adjustFloatingIconStartVelocity(velocity: PointF) {
78         val oldX = velocity.x
79         val oldY = velocity.y
80         velocity.set(-oldY, oldX)
81     }
82 
fixBoundsForHomeAnimStartRectnull83     override fun fixBoundsForHomeAnimStartRect(outStartRect: RectF, deviceProfile: DeviceProfile) {
84         // We don't need to check the "top" value here because the startRect is in the orientation
85         // of the app, not of the fixed portrait launcher.
86         if (outStartRect.left > deviceProfile.heightPx) {
87             outStartRect.offsetTo(0f, outStartRect.top)
88         } else if (outStartRect.left < -deviceProfile.heightPx) {
89             outStartRect.offsetTo(0f, outStartRect.top)
90         }
91     }
92 
setPrimarynull93     override fun <T> setPrimary(target: T, action: Int2DAction<T>, param: Int) =
94         action.call(target, 0, param)
95 
96     override fun <T> setPrimary(target: T, action: Float2DAction<T>, param: Float) =
97         action.call(target, 0f, param)
98 
99     override fun <T> setSecondary(target: T, action: Float2DAction<T>, param: Float) =
100         action.call(target, param, 0f)
101 
102     override fun <T> set(
103         target: T,
104         action: Int2DAction<T>,
105         primaryParam: Int,
106         secondaryParam: Int
107     ) = action.call(target, secondaryParam, primaryParam)
108 
109     override fun getPrimaryDirection(event: MotionEvent, pointerIndex: Int): Float =
110         event.getY(pointerIndex)
111 
112     override fun getPrimaryVelocity(velocityTracker: VelocityTracker, pointerId: Int): Float =
113         velocityTracker.getYVelocity(pointerId)
114 
115     override fun getMeasuredSize(view: View): Int = view.measuredHeight
116 
117     override fun getPrimarySize(view: View): Int = view.height
118 
119     override fun getPrimarySize(rect: RectF): Float = rect.height()
120 
121     override fun getStart(rect: RectF): Float = rect.top
122 
123     override fun getEnd(rect: RectF): Float = rect.bottom
124 
125     override fun rotateInsets(insets: Rect, outInsets: Rect) {
126         outInsets.set(insets.bottom, insets.left, insets.top, insets.right)
127     }
128 
getClearAllSidePaddingnull129     override fun getClearAllSidePadding(view: View, isRtl: Boolean): Int =
130         if (isRtl) view.paddingBottom / 2 else -view.paddingTop / 2
131 
132     override fun getSecondaryDimension(view: View): Int = view.width
133 
134     override val primaryViewTranslate: FloatProperty<View> = LauncherAnimUtils.VIEW_TRANSLATE_Y
135 
136     override val secondaryViewTranslate: FloatProperty<View> = LauncherAnimUtils.VIEW_TRANSLATE_X
137 
138     override fun getPrimaryScroll(view: View): Int = view.scrollY
139 
140     override fun getPrimaryScale(view: View): Float = view.scaleY
141 
142     override fun setMaxScroll(event: AccessibilityEvent, maxScroll: Int) {
143         event.maxScrollY = maxScroll
144     }
145 
getRecentsRtlSettingnull146     override fun getRecentsRtlSetting(resources: Resources): Boolean = !Utilities.isRtl(resources)
147 
148     override val degreesRotated: Float = 90f
149 
150     override val rotation: Int = Surface.ROTATION_90
151 
152     override fun setPrimaryScale(view: View, scale: Float) {
153         view.scaleY = scale
154     }
155 
setSecondaryScalenull156     override fun setSecondaryScale(view: View, scale: Float) {
157         view.scaleX = scale
158     }
159 
getChildStartnull160     override fun getChildStart(view: View): Int = view.top
161 
162     override fun getCenterForPage(view: View, insets: Rect): Int =
163         (view.paddingLeft + view.measuredWidth + insets.left - insets.right - view.paddingRight) / 2
164 
165     override fun getScrollOffsetStart(view: View, insets: Rect): Int = insets.top + view.paddingTop
166 
167     override fun getScrollOffsetEnd(view: View, insets: Rect): Int =
168         view.height - view.paddingBottom - insets.bottom
169 
170     override val secondaryTranslationDirectionFactor: Int = 1
171 
172     override fun getSplitTranslationDirectionFactor(
173         stagePosition: Int,
174         deviceProfile: DeviceProfile
175     ): Int = if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) -1 else 1
176 
177     override fun getTaskMenuX(
178         x: Float,
179         thumbnailView: View,
180         deviceProfile: DeviceProfile,
181         taskInsetMargin: Float,
182         taskViewIcon: View
183     ): Float = thumbnailView.measuredWidth + x - taskInsetMargin
184 
185     override fun getTaskMenuY(
186         y: Float,
187         thumbnailView: View,
188         stagePosition: Int,
189         taskMenuView: View,
190         taskInsetMargin: Float,
191         taskViewIcon: View
192     ): Float {
193         val layoutParams = taskMenuView.layoutParams as BaseDragLayer.LayoutParams
194         var taskMenuY = y + taskInsetMargin
195 
196         if (stagePosition == STAGE_POSITION_UNDEFINED) {
197             taskMenuY += (thumbnailView.measuredHeight - layoutParams.width) / 2f
198         }
199 
200         return taskMenuY
201     }
202 
getTaskMenuWidthnull203     override fun getTaskMenuWidth(
204         thumbnailView: View,
205         deviceProfile: DeviceProfile,
206         @StagePosition stagePosition: Int
207     ): Int =
208         when {
209             Flags.enableOverviewIconMenu() ->
210                 thumbnailView.resources.getDimensionPixelSize(
211                     R.dimen.task_thumbnail_icon_menu_expanded_width
212                 )
213             stagePosition == STAGE_POSITION_UNDEFINED -> thumbnailView.measuredWidth
214             else -> thumbnailView.measuredHeight
215         }
216 
getTaskMenuHeightnull217     override fun getTaskMenuHeight(
218         taskInsetMargin: Float,
219         deviceProfile: DeviceProfile,
220         taskMenuX: Float,
221         taskMenuY: Float
222     ): Int = (taskMenuX - taskInsetMargin).toInt()
223 
224     override fun setTaskOptionsMenuLayoutOrientation(
225         deviceProfile: DeviceProfile,
226         taskMenuLayout: LinearLayout,
227         dividerSpacing: Int,
228         dividerDrawable: ShapeDrawable
229     ) {
230         taskMenuLayout.orientation = LinearLayout.VERTICAL
231         dividerDrawable.intrinsicHeight = dividerSpacing
232         taskMenuLayout.dividerDrawable = dividerDrawable
233     }
234 
setLayoutParamsForTaskMenuOptionItemnull235     override fun setLayoutParamsForTaskMenuOptionItem(
236         lp: LinearLayout.LayoutParams,
237         viewGroup: LinearLayout,
238         deviceProfile: DeviceProfile
239     ) {
240         // Phone fake landscape
241         viewGroup.orientation = LinearLayout.HORIZONTAL
242         lp.width = ViewGroup.LayoutParams.MATCH_PARENT
243         lp.height = ViewGroup.LayoutParams.WRAP_CONTENT
244     }
245 
updateDwbBannerLayoutnull246     override fun updateDwbBannerLayout(
247         taskViewWidth: Int,
248         taskViewHeight: Int,
249         isGroupedTaskView: Boolean,
250         deviceProfile: DeviceProfile,
251         snapshotViewWidth: Int,
252         snapshotViewHeight: Int,
253         banner: View
254     ) {
255         banner.pivotX = 0f
256         banner.pivotY = 0f
257         banner.rotation = degreesRotated
258         banner.updateLayoutParams<FrameLayout.LayoutParams> {
259             gravity = Gravity.TOP or if (banner.isLayoutRtl) Gravity.END else Gravity.START
260             width =
261                 if (isGroupedTaskView) {
262                     snapshotViewHeight
263                 } else {
264                     taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx
265                 }
266         }
267     }
268 
getDwbBannerTranslationsnull269     override fun getDwbBannerTranslations(
270         taskViewWidth: Int,
271         taskViewHeight: Int,
272         splitBounds: SplitBounds?,
273         deviceProfile: DeviceProfile,
274         thumbnailViews: Array<View>,
275         desiredTaskId: Int,
276         banner: View
277     ): Pair<Float, Float> {
278         val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams
279         val translationX = banner.height.toFloat()
280         val translationY: Float
281         if (splitBounds == null) {
282             translationY = snapshotParams.topMargin.toFloat()
283         } else {
284             if (desiredTaskId == splitBounds.leftTopTaskId) {
285                 translationY = snapshotParams.topMargin.toFloat()
286             } else {
287                 val topLeftTaskPlusDividerPercent =
288                     if (splitBounds.appsStackedVertically) {
289                         splitBounds.topTaskPercent + splitBounds.dividerHeightPercent
290                     } else {
291                         splitBounds.leftTaskPercent + splitBounds.dividerWidthPercent
292                     }
293                 translationY =
294                     snapshotParams.topMargin +
295                         (taskViewHeight - snapshotParams.topMargin) * topLeftTaskPlusDividerPercent
296             }
297         }
298         return Pair(translationX, translationY)
299     }
300 
301     /* ---------- The following are only used by TaskViewTouchHandler. ---------- */
302     override val upDownSwipeDirection: SingleAxisSwipeDetector.Direction =
303         SingleAxisSwipeDetector.HORIZONTAL
304 
getUpDirectionnull305     override fun getUpDirection(isRtl: Boolean): Int =
306         if (isRtl) SingleAxisSwipeDetector.DIRECTION_NEGATIVE
307         else SingleAxisSwipeDetector.DIRECTION_POSITIVE
308 
309     override fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean =
310         if (isRtl) displacement < 0 else displacement > 0
311 
312     override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) 1 else -1
313 
314     /* -------------------- */
315 
316     override fun getChildBounds(
317         child: View,
318         childStart: Int,
319         pageCenter: Int,
320         layoutChild: Boolean
321     ): ChildBounds {
322         val childHeight = child.measuredHeight
323         val childWidth = child.measuredWidth
324         val childBottom = childStart + childHeight
325         val childLeft = pageCenter - childWidth / 2
326         if (layoutChild) {
327             child.layout(childLeft, childStart, childLeft + childWidth, childBottom)
328         }
329         return ChildBounds(childHeight, childWidth, childBottom, childLeft)
330     }
331 
getDistanceToBottomOfRectnull332     override fun getDistanceToBottomOfRect(dp: DeviceProfile, rect: Rect): Int = rect.left
333 
334     override fun getSplitPositionOptions(dp: DeviceProfile): List<SplitPositionOption> =
335         // Add "left" side of phone which is actually the top
336         listOf(
337             SplitPositionOption(
338                 R.drawable.ic_split_horizontal,
339                 R.string.recent_task_option_split_screen,
340                 STAGE_POSITION_TOP_OR_LEFT,
341                 STAGE_TYPE_MAIN
342             )
343         )
344 
345     override fun getInitialSplitPlaceholderBounds(
346         placeholderHeight: Int,
347         placeholderInset: Int,
348         dp: DeviceProfile,
349         @StagePosition stagePosition: Int,
350         out: Rect
351     ) {
352         // In fake land/seascape, the placeholder always needs to go to the "top" of the device,
353         // which is the same bounds as 0 rotation.
354         val width = dp.widthPx
355         val insetSizeAdjustment = getPlaceholderSizeAdjustment(dp)
356         out.set(0, 0, width, placeholderHeight + insetSizeAdjustment)
357         out.inset(placeholderInset, 0)
358 
359         // Adjust the top to account for content off screen. This will help to animate the view in
360         // with rounded corners.
361         val screenWidth = dp.widthPx
362         val screenHeight = dp.heightPx
363         val totalHeight =
364             (1.0f * screenHeight / 2 * (screenWidth - 2 * placeholderInset) / screenWidth).toInt()
365         out.top -= totalHeight - placeholderHeight
366     }
367 
updateSplitIconParamsnull368     override fun updateSplitIconParams(
369         out: View,
370         onScreenRectCenterX: Float,
371         onScreenRectCenterY: Float,
372         fullscreenScaleX: Float,
373         fullscreenScaleY: Float,
374         drawableWidth: Int,
375         drawableHeight: Int,
376         dp: DeviceProfile,
377         @StagePosition stagePosition: Int
378     ) {
379         val insetAdjustment = getPlaceholderSizeAdjustment(dp) / 2f
380         out.x = (onScreenRectCenterX / fullscreenScaleX - 1.0f * drawableWidth / 2)
381         out.y =
382             ((onScreenRectCenterY + insetAdjustment) / fullscreenScaleY - 1.0f * drawableHeight / 2)
383     }
384 
385     /**
386      * The split placeholder comes with a default inset to buffer the icon from the top of the
387      * screen. But if the device already has a large inset (from cutouts etc), use that instead.
388      */
getPlaceholderSizeAdjustmentnull389     private fun getPlaceholderSizeAdjustment(dp: DeviceProfile?): Int =
390         max((dp!!.insets.top - dp.splitPlaceholderInset).toDouble(), 0.0).toInt()
391 
392     override fun setSplitInstructionsParams(
393         out: View,
394         dp: DeviceProfile,
395         splitInstructionsHeight: Int,
396         splitInstructionsWidth: Int
397     ) {
398         out.pivotX = 0f
399         out.pivotY = splitInstructionsHeight.toFloat()
400         out.rotation = degreesRotated
401         val distanceToEdge =
402             out.resources.getDimensionPixelSize(
403                 R.dimen.split_instructions_bottom_margin_phone_landscape
404             )
405         // Adjust for any insets on the left edge
406         val insetCorrectionX = dp.insets.left
407         // Center the view in case of unbalanced insets on top or bottom of screen
408         val insetCorrectionY = (dp.insets.bottom - dp.insets.top) / 2
409         out.translationX = (distanceToEdge - insetCorrectionX).toFloat()
410         out.translationY =
411             (-splitInstructionsHeight - splitInstructionsWidth) / 2f + insetCorrectionY
412         // Setting gravity to LEFT instead of the lint-recommended START because we always want this
413         // view to be screen-left when phone is in landscape, regardless of the RtL setting.
414         val lp = out.layoutParams as FrameLayout.LayoutParams
415         lp.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
416         out.layoutParams = lp
417     }
418 
getFinalSplitPlaceholderBoundsnull419     override fun getFinalSplitPlaceholderBounds(
420         splitDividerSize: Int,
421         dp: DeviceProfile,
422         @StagePosition stagePosition: Int,
423         out1: Rect,
424         out2: Rect
425     ) {
426         // In fake land/seascape, the window bounds are always top and bottom half
427         val screenHeight = dp.heightPx
428         val screenWidth = dp.widthPx
429         out1.set(0, 0, screenWidth, screenHeight / 2 - splitDividerSize)
430         out2.set(0, screenHeight / 2 + splitDividerSize, screenWidth, screenHeight)
431     }
432 
setSplitTaskSwipeRectnull433     override fun setSplitTaskSwipeRect(
434         dp: DeviceProfile,
435         outRect: Rect,
436         splitInfo: SplitBounds,
437         desiredStagePosition: Int
438     ) {
439         val topLeftTaskPercent: Float
440         val dividerBarPercent: Float
441         if (splitInfo.appsStackedVertically) {
442             topLeftTaskPercent = splitInfo.topTaskPercent
443             dividerBarPercent = splitInfo.dividerHeightPercent
444         } else {
445             topLeftTaskPercent = splitInfo.leftTaskPercent
446             dividerBarPercent = splitInfo.dividerWidthPercent
447         }
448 
449         if (desiredStagePosition == STAGE_POSITION_TOP_OR_LEFT) {
450             outRect.bottom = outRect.top + (outRect.height() * topLeftTaskPercent).toInt()
451         } else {
452             outRect.top += (outRect.height() * (topLeftTaskPercent + dividerBarPercent)).toInt()
453         }
454     }
455 
456     /**
457      * @param inSplitSelection Whether user currently has a task from this task group staged for
458      * split screen. Currently this state is not reachable in fake landscape.
459      */
measureGroupedTaskViewThumbnailBoundsnull460     override fun measureGroupedTaskViewThumbnailBounds(
461         primarySnapshot: View,
462         secondarySnapshot: View,
463         parentWidth: Int,
464         parentHeight: Int,
465         splitBoundsConfig: SplitBounds,
466         dp: DeviceProfile,
467         isRtl: Boolean,
468         inSplitSelection: Boolean
469     ) {
470         val primaryParams = primarySnapshot.layoutParams as FrameLayout.LayoutParams
471         val secondaryParams = secondarySnapshot.layoutParams as FrameLayout.LayoutParams
472 
473         // Swap the margins that are set in TaskView#setRecentsOrientedState()
474         secondaryParams.topMargin = dp.overviewTaskThumbnailTopMarginPx
475         primaryParams.topMargin = 0
476 
477         // Measure and layout the thumbnails bottom up, since the primary is on the visual left
478         // (portrait bottom) and secondary is on the right (portrait top)
479         val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx
480         val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
481         val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig)
482 
483         val (taskViewFirst, taskViewSecond) =
484             getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight)
485 
486         primarySnapshot.translationY = spaceAboveSnapshot.toFloat()
487         primarySnapshot.measure(
488             MeasureSpec.makeMeasureSpec(taskViewFirst.x, MeasureSpec.EXACTLY),
489             MeasureSpec.makeMeasureSpec(taskViewFirst.y, MeasureSpec.EXACTLY)
490         )
491         val translationY = taskViewFirst.y + spaceAboveSnapshot + dividerBar
492         secondarySnapshot.translationY = (translationY - spaceAboveSnapshot).toFloat()
493         secondarySnapshot.measure(
494             MeasureSpec.makeMeasureSpec(taskViewSecond.x, MeasureSpec.EXACTLY),
495             MeasureSpec.makeMeasureSpec(taskViewSecond.y, MeasureSpec.EXACTLY)
496         )
497     }
498 
getGroupedTaskViewSizesnull499     override fun getGroupedTaskViewSizes(
500         dp: DeviceProfile,
501         splitBoundsConfig: SplitBounds,
502         parentWidth: Int,
503         parentHeight: Int
504     ): Pair<Point, Point> {
505         val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx
506         val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
507         val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig)
508 
509         val taskPercent =
510             if (splitBoundsConfig.appsStackedVertically) {
511                 splitBoundsConfig.topTaskPercent
512             } else {
513                 splitBoundsConfig.leftTaskPercent
514             }
515         val firstTaskViewSize = Point(parentWidth, (totalThumbnailHeight * taskPercent).toInt())
516         val secondTaskViewSize =
517             Point(parentWidth, totalThumbnailHeight - firstTaskViewSize.y - dividerBar)
518         return Pair(firstTaskViewSize, secondTaskViewSize)
519     }
520 
setTaskIconParamsnull521     override fun setTaskIconParams(
522         iconParams: FrameLayout.LayoutParams,
523         taskIconMargin: Int,
524         taskIconHeight: Int,
525         thumbnailTopMargin: Int,
526         isRtl: Boolean
527     ) {
528         iconParams.gravity =
529             if (isRtl) {
530                 Gravity.START or Gravity.CENTER_VERTICAL
531             } else {
532                 Gravity.END or Gravity.CENTER_VERTICAL
533             }
534         iconParams.rightMargin = -taskIconHeight - taskIconMargin / 2
535         iconParams.leftMargin = 0
536         iconParams.topMargin = thumbnailTopMargin / 2
537         iconParams.bottomMargin = 0
538     }
539 
setIconAppChipChildrenParamsnull540     override fun setIconAppChipChildrenParams(
541         iconParams: FrameLayout.LayoutParams,
542         chipChildMarginStart: Int
543     ) {
544         iconParams.gravity = Gravity.START or Gravity.CENTER_VERTICAL
545         iconParams.marginStart = chipChildMarginStart
546         iconParams.topMargin = 0
547     }
548 
setIconAppChipMenuParamsnull549     override fun setIconAppChipMenuParams(
550         iconAppChipView: IconAppChipView,
551         iconMenuParams: FrameLayout.LayoutParams,
552         iconMenuMargin: Int,
553         thumbnailTopMargin: Int
554     ) {
555         val isRtl = iconAppChipView.layoutDirection == View.LAYOUT_DIRECTION_RTL
556 
557         if (isRtl) {
558             iconMenuParams.gravity = Gravity.START or Gravity.BOTTOM
559             iconMenuParams.marginStart = iconMenuMargin
560             iconMenuParams.bottomMargin = iconMenuMargin
561             iconAppChipView.pivotX = iconMenuParams.width - iconMenuParams.height / 2f
562             iconAppChipView.pivotY = iconMenuParams.height / 2f
563         } else {
564             iconMenuParams.gravity = Gravity.END or Gravity.TOP
565             iconMenuParams.marginStart = 0
566             iconMenuParams.bottomMargin = 0
567             iconAppChipView.pivotX = iconMenuParams.width / 2f
568             iconAppChipView.pivotY = iconMenuParams.width / 2f
569         }
570 
571         iconMenuParams.topMargin = iconMenuMargin
572         iconMenuParams.marginEnd = iconMenuMargin
573         iconAppChipView.setSplitTranslationY(0f)
574         iconAppChipView.setRotation(degreesRotated)
575     }
576 
577     /**
578      * @param inSplitSelection Whether user currently has a task from this task group staged for
579      * split screen. Currently this state is not reachable in fake landscape.
580      */
setSplitIconParamsnull581     override fun setSplitIconParams(
582         primaryIconView: View,
583         secondaryIconView: View,
584         taskIconHeight: Int,
585         primarySnapshotWidth: Int,
586         primarySnapshotHeight: Int,
587         groupedTaskViewHeight: Int,
588         groupedTaskViewWidth: Int,
589         isRtl: Boolean,
590         deviceProfile: DeviceProfile,
591         splitConfig: SplitBounds,
592         inSplitSelection: Boolean
593     ) {
594         val spaceAboveSnapshot = deviceProfile.overviewTaskThumbnailTopMarginPx
595         val totalThumbnailHeight = groupedTaskViewHeight - spaceAboveSnapshot
596         val dividerBar: Int = getDividerBarSize(totalThumbnailHeight, splitConfig)
597 
598         val (topLeftY, bottomRightY) =
599             getSplitIconsPosition(
600                 taskIconHeight,
601                 primarySnapshotHeight,
602                 totalThumbnailHeight,
603                 isRtl,
604                 deviceProfile.overviewTaskMarginPx,
605                 dividerBar
606             )
607 
608         updateSplitIconsPosition(primaryIconView, topLeftY, isRtl)
609         updateSplitIconsPosition(secondaryIconView, bottomRightY, isRtl)
610     }
611 
getDefaultSplitPositionnull612     override fun getDefaultSplitPosition(deviceProfile: DeviceProfile): Int {
613         throw IllegalStateException("Default position not available in fake landscape")
614     }
615 
getSplitSelectTaskOffsetnull616     override fun <T> getSplitSelectTaskOffset(
617         primary: FloatProperty<T>,
618         secondary: FloatProperty<T>,
619         deviceProfile: DeviceProfile
620     ): Pair<FloatProperty<T>, FloatProperty<T>> = Pair(primary, secondary)
621 
622     override fun getFloatingTaskOffscreenTranslationTarget(
623         floatingTask: View,
624         onScreenRect: RectF,
625         @StagePosition stagePosition: Int,
626         dp: DeviceProfile
627     ): Float = floatingTask.translationY - onScreenRect.height()
628 
629     override fun setFloatingTaskPrimaryTranslation(
630         floatingTask: View,
631         translation: Float,
632         dp: DeviceProfile
633     ) {
634         floatingTask.translationY = translation
635     }
636 
getFloatingTaskPrimaryTranslationnull637     override fun getFloatingTaskPrimaryTranslation(floatingTask: View, dp: DeviceProfile): Float =
638         floatingTask.translationY
639 
640     override fun getHandlerTypeForLogging(): TaskSwitcherContainer.OrientationHandler =
641         TaskSwitcherContainer.OrientationHandler.LANDSCAPE
642 
643     /**
644      * Retrieves split icons position
645      *
646      * @param taskIconHeight The height of the task icon.
647      * @param primarySnapshotHeight The height for the primary snapshot (i.e., top-left snapshot).
648      * @param totalThumbnailHeight The total height for the group task view.
649      * @param isRtl Whether the layout direction is RTL (or false for LTR).
650      * @param overviewTaskMarginPx The space under the focused task icon provided by Device Profile.
651      * @param dividerSize The size of the divider for the group task view.
652      * @return The top-left and right-bottom positions for the icon views.
653      */
654     @VisibleForTesting
655     open fun getSplitIconsPosition(
656         taskIconHeight: Int,
657         primarySnapshotHeight: Int,
658         totalThumbnailHeight: Int,
659         isRtl: Boolean,
660         overviewTaskMarginPx: Int,
661         dividerSize: Int,
662     ): SplitIconPositions {
663         return if (Flags.enableOverviewIconMenu()) {
664             if (isRtl) {
665                 SplitIconPositions(0, -(totalThumbnailHeight - primarySnapshotHeight))
666             } else {
667                 SplitIconPositions(0, primarySnapshotHeight + dividerSize)
668             }
669         } else {
670             val topLeftY = primarySnapshotHeight + overviewTaskMarginPx
671             SplitIconPositions(
672                 topLeftY = topLeftY,
673                 bottomRightY = topLeftY + dividerSize + taskIconHeight
674             )
675         }
676     }
677 
678     /**
679      * Updates icon view gravity and translation for split tasks
680      *
681      * @param iconView View to be updated
682      * @param translationY the translationY that should be applied
683      * @param isRtl Whether the layout direction is RTL (or false for LTR).
684      */
685     @SuppressLint("RtlHardcoded")
686     @VisibleForTesting
updateSplitIconsPositionnull687     open fun updateSplitIconsPosition(iconView: View, translationY: Int, isRtl: Boolean) {
688         val layoutParams = iconView.layoutParams as FrameLayout.LayoutParams
689 
690         if (Flags.enableOverviewIconMenu()) {
691             val appChipView = iconView as IconAppChipView
692             layoutParams.gravity =
693                 if (isRtl) Gravity.BOTTOM or Gravity.START else Gravity.TOP or Gravity.END
694             appChipView.layoutParams = layoutParams
695             appChipView.setSplitTranslationX(0f)
696             appChipView.setSplitTranslationY(translationY.toFloat())
697         } else {
698             layoutParams.gravity = Gravity.TOP or Gravity.RIGHT
699             layoutParams.topMargin = translationY
700             iconView.translationX = 0f
701             iconView.translationY = 0f
702             iconView.layoutParams = layoutParams
703         }
704     }
705 
706     /**
707      * It calculates the divider's size in the group task view.
708      *
709      * @param totalThumbnailHeight The total height for the group task view
710      * @param splitConfig Contains information about sizes and proportions for split task.
711      * @return The divider size for the group task view.
712      */
getDividerBarSizenull713     protected fun getDividerBarSize(totalThumbnailHeight: Int, splitConfig: SplitBounds): Int {
714         return Math.round(
715             totalThumbnailHeight *
716                 if (splitConfig.appsStackedVertically) splitConfig.dividerHeightPercent
717                 else splitConfig.dividerWidthPercent
718         )
719     }
720 
721     /**
722      * Data structure to keep the y position to be used for the split task icon views translation.
723      *
724      * @param topLeftY The y-axis position for the task view position on the Top or Left side.
725      * @param bottomRightY The y-axis position for the task view position on the Bottom or Right
726      *   side.
727      */
728     data class SplitIconPositions(val topLeftY: Int, val bottomRightY: Int)
729 }
730