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