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.util.Pair
24 import android.view.Gravity
25 import android.view.Surface
26 import android.view.View
27 import android.view.View.MeasureSpec
28 import android.widget.FrameLayout
29 import androidx.core.util.component1
30 import androidx.core.util.component2
31 import com.android.launcher3.DeviceProfile
32 import com.android.launcher3.Flags
33 import com.android.launcher3.R
34 import com.android.launcher3.Utilities
35 import com.android.launcher3.logger.LauncherAtom
36 import com.android.launcher3.touch.SingleAxisSwipeDetector
37 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT
38 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT
39 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
40 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN
41 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds
42 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption
43 import com.android.launcher3.views.BaseDragLayer
44 import com.android.quickstep.views.IconAppChipView
45 
46 class SeascapePagedViewHandler : LandscapePagedViewHandler() {
rotateInsetsnull47     override fun rotateInsets(insets: Rect, outInsets: Rect) {
48         outInsets.set(insets.top, insets.right, insets.bottom, insets.left)
49     }
50 
51     override val secondaryTranslationDirectionFactor: Int = -1
52 
getSplitTranslationDirectionFactornull53     override fun getSplitTranslationDirectionFactor(
54         stagePosition: Int,
55         deviceProfile: DeviceProfile
56     ): Int = if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) -1 else 1
57 
58     override fun getRecentsRtlSetting(resources: Resources): Boolean = Utilities.isRtl(resources)
59 
60     override val degreesRotated: Float = 270f
61 
62     override val rotation: Int = Surface.ROTATION_270
63 
64     override fun adjustFloatingIconStartVelocity(velocity: PointF) =
65         velocity.set(velocity.y, -velocity.x)
66 
67     override fun getTaskMenuX(
68         x: Float,
69         thumbnailView: View,
70         deviceProfile: DeviceProfile,
71         taskInsetMargin: Float,
72         taskViewIcon: View
73     ): Float = x + taskInsetMargin
74 
75     override fun getTaskMenuY(
76         y: Float,
77         thumbnailView: View,
78         stagePosition: Int,
79         taskMenuView: View,
80         taskInsetMargin: Float,
81         taskViewIcon: View
82     ): Float {
83         if (Flags.enableOverviewIconMenu()) {
84             return y
85         }
86         val lp = taskMenuView.layoutParams as BaseDragLayer.LayoutParams
87         val taskMenuWidth = lp.width
88         return if (stagePosition == STAGE_POSITION_UNDEFINED) {
89             y + taskInsetMargin + (thumbnailView.measuredHeight + taskMenuWidth) / 2f
90         } else {
91             y + taskMenuWidth + taskInsetMargin
92         }
93     }
94 
getTaskMenuHeightnull95     override fun getTaskMenuHeight(
96         taskInsetMargin: Float,
97         deviceProfile: DeviceProfile,
98         taskMenuX: Float,
99         taskMenuY: Float
100     ): Int = (deviceProfile.availableWidthPx - taskInsetMargin - taskMenuX).toInt()
101 
102     override fun setSplitTaskSwipeRect(
103         dp: DeviceProfile,
104         outRect: Rect,
105         splitInfo: SplitBounds,
106         desiredStagePosition: Int
107     ) {
108         val topLeftTaskPercent: Float
109         val dividerBarPercent: Float
110         if (splitInfo.appsStackedVertically) {
111             topLeftTaskPercent = splitInfo.topTaskPercent
112             dividerBarPercent = splitInfo.dividerHeightPercent
113         } else {
114             topLeftTaskPercent = splitInfo.leftTaskPercent
115             dividerBarPercent = splitInfo.dividerWidthPercent
116         }
117 
118         // In seascape, the primary thumbnail is counterintuitively placed at the physical bottom of
119         // the screen. This is to preserve consistency when the user rotates: From the user's POV,
120         // the primary should always be on the left.
121         if (desiredStagePosition == STAGE_POSITION_TOP_OR_LEFT) {
122             outRect.top += (outRect.height() * (1 - topLeftTaskPercent)).toInt()
123         } else {
124             outRect.bottom -= (outRect.height() * (topLeftTaskPercent + dividerBarPercent)).toInt()
125         }
126     }
127 
getDwbLayoutTranslationsnull128     override fun getDwbLayoutTranslations(
129         taskViewWidth: Int,
130         taskViewHeight: Int,
131         splitBounds: SplitBounds?,
132         deviceProfile: DeviceProfile,
133         thumbnailViews: Array<View>,
134         desiredTaskId: Int,
135         banner: View
136     ): Pair<Float, Float> {
137         val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams
138         val isRtl = banner.layoutDirection == View.LAYOUT_DIRECTION_RTL
139 
140         val bannerParams = banner.layoutParams as FrameLayout.LayoutParams
141         bannerParams.gravity = Gravity.BOTTOM or if (isRtl) Gravity.END else Gravity.START
142         banner.pivotX = 0f
143         banner.pivotY = 0f
144         banner.rotation = degreesRotated
145 
146         val translationX: Float = (taskViewWidth - banner.height).toFloat()
147         if (splitBounds == null) {
148             // Single, fullscreen case
149             bannerParams.width = taskViewHeight - snapshotParams.topMargin
150             return Pair(translationX, banner.height.toFloat())
151         }
152 
153         // Set correct width and translations
154         val translationY: Float
155         if (desiredTaskId == splitBounds.leftTopTaskId) {
156             bannerParams.width = thumbnailViews[0].measuredHeight
157             val bottomRightTaskPlusDividerPercent =
158                 if (splitBounds.appsStackedVertically) {
159                     1f - splitBounds.topTaskPercent
160                 } else {
161                     1f - splitBounds.leftTaskPercent
162                 }
163             translationY =
164                 banner.height -
165                     (taskViewHeight - snapshotParams.topMargin) * bottomRightTaskPlusDividerPercent
166         } else {
167             bannerParams.width = thumbnailViews[1].measuredHeight
168             translationY = banner.height.toFloat()
169         }
170 
171         return Pair(translationX, translationY)
172     }
173 
getDistanceToBottomOfRectnull174     override fun getDistanceToBottomOfRect(dp: DeviceProfile, rect: Rect): Int =
175         dp.widthPx - rect.right
176 
177     override fun getSplitPositionOptions(dp: DeviceProfile): List<SplitPositionOption> =
178         // Add "right" option which is actually the top
179         listOf(
180             SplitPositionOption(
181                 R.drawable.ic_split_horizontal,
182                 R.string.recent_task_option_split_screen,
183                 STAGE_POSITION_BOTTOM_OR_RIGHT,
184                 STAGE_TYPE_MAIN
185             )
186         )
187 
188     override fun setSplitInstructionsParams(
189         out: View,
190         dp: DeviceProfile,
191         splitInstructionsHeight: Int,
192         splitInstructionsWidth: Int
193     ) {
194         out.pivotX = 0f
195         out.pivotY = splitInstructionsHeight.toFloat()
196         out.rotation = degreesRotated
197         val distanceToEdge =
198             out.resources.getDimensionPixelSize(
199                 R.dimen.split_instructions_bottom_margin_phone_landscape
200             )
201         // Adjust for any insets on the right edge
202         val insetCorrectionX = dp.insets.right
203         // Center the view in case of unbalanced insets on top or bottom of screen
204         val insetCorrectionY = (dp.insets.bottom - dp.insets.top) / 2
205         out.translationX = (splitInstructionsWidth - distanceToEdge + insetCorrectionX).toFloat()
206         out.translationY =
207             (-splitInstructionsHeight + splitInstructionsWidth) / 2f + insetCorrectionY
208         // Setting gravity to RIGHT instead of the lint-recommended END because we always want this
209         // view to be screen-right when phone is in seascape, regardless of the RtL setting.
210         val lp = out.layoutParams as FrameLayout.LayoutParams
211         lp.gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
212         out.layoutParams = lp
213     }
214 
setTaskIconParamsnull215     override fun setTaskIconParams(
216         iconParams: FrameLayout.LayoutParams,
217         taskIconMargin: Int,
218         taskIconHeight: Int,
219         thumbnailTopMargin: Int,
220         isRtl: Boolean
221     ) {
222         iconParams.gravity =
223             if (isRtl) {
224                 Gravity.END or Gravity.CENTER_VERTICAL
225             } else {
226                 Gravity.START or Gravity.CENTER_VERTICAL
227             }
228         iconParams.setMargins(-taskIconHeight - taskIconMargin / 2, thumbnailTopMargin / 2, 0, 0)
229     }
230 
setIconAppChipChildrenParamsnull231     override fun setIconAppChipChildrenParams(
232         iconParams: FrameLayout.LayoutParams,
233         chipChildMarginStart: Int
234     ) {
235         iconParams.setMargins(0, 0, 0, 0)
236         iconParams.marginStart = chipChildMarginStart
237         iconParams.gravity = Gravity.START or Gravity.CENTER_VERTICAL
238     }
239 
setIconAppChipMenuParamsnull240     override fun setIconAppChipMenuParams(
241         iconAppChipView: IconAppChipView,
242         iconMenuParams: FrameLayout.LayoutParams,
243         iconMenuMargin: Int,
244         thumbnailTopMargin: Int
245     ) {
246         val isRtl = iconAppChipView.layoutDirection == View.LAYOUT_DIRECTION_RTL
247         val iconCenter = iconAppChipView.getHeight() / 2f
248 
249         if (isRtl) {
250             iconMenuParams.gravity = Gravity.TOP or Gravity.END
251             iconMenuParams.topMargin = iconMenuMargin
252             iconMenuParams.marginEnd = thumbnailTopMargin
253             // Use half menu height to place the pivot within the X/Y center of icon in the menu.
254             iconAppChipView.pivotX = iconMenuParams.width / 2f
255             iconAppChipView.pivotY = iconMenuParams.width / 2f
256         } else {
257             iconMenuParams.gravity = Gravity.BOTTOM or Gravity.START
258             iconMenuParams.topMargin = 0
259             iconMenuParams.marginEnd = 0
260             iconAppChipView.pivotX = iconCenter
261             iconAppChipView.pivotY = iconCenter - iconMenuMargin
262         }
263         iconMenuParams.marginStart = 0
264         iconMenuParams.bottomMargin = 0
265         iconAppChipView.setSplitTranslationY(0f)
266         iconAppChipView.setRotation(degreesRotated)
267     }
268 
measureGroupedTaskViewThumbnailBoundsnull269     override fun measureGroupedTaskViewThumbnailBounds(
270         primarySnapshot: View,
271         secondarySnapshot: View,
272         parentWidth: Int,
273         parentHeight: Int,
274         splitBoundsConfig: SplitBounds,
275         dp: DeviceProfile,
276         isRtl: Boolean
277     ) {
278         val primaryParams = primarySnapshot.layoutParams as FrameLayout.LayoutParams
279         val secondaryParams = secondarySnapshot.layoutParams as FrameLayout.LayoutParams
280 
281         // Swap the margins that are set in TaskView#setRecentsOrientedState()
282         secondaryParams.topMargin = dp.overviewTaskThumbnailTopMarginPx
283         primaryParams.topMargin = 0
284 
285         // Measure and layout the thumbnails bottom up, since the primary is on the visual left
286         // (portrait bottom) and secondary is on the right (portrait top)
287         val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx
288         val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
289         val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig)
290 
291         val (taskViewFirst, taskViewSecond) =
292             getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight)
293         secondarySnapshot.translationY = 0f
294         primarySnapshot.translationY =
295             (taskViewSecond.y + spaceAboveSnapshot + dividerBar).toFloat()
296         primarySnapshot.measure(
297             MeasureSpec.makeMeasureSpec(taskViewFirst.x, MeasureSpec.EXACTLY),
298             MeasureSpec.makeMeasureSpec(taskViewFirst.y, MeasureSpec.EXACTLY)
299         )
300         secondarySnapshot.measure(
301             MeasureSpec.makeMeasureSpec(taskViewSecond.x, MeasureSpec.EXACTLY),
302             MeasureSpec.makeMeasureSpec(taskViewSecond.y, MeasureSpec.EXACTLY)
303         )
304     }
305 
getGroupedTaskViewSizesnull306     override fun getGroupedTaskViewSizes(
307         dp: DeviceProfile,
308         splitBoundsConfig: SplitBounds,
309         parentWidth: Int,
310         parentHeight: Int
311     ): Pair<Point, Point> {
312         // Measure and layout the thumbnails bottom up, since the primary is on the visual left
313         // (portrait bottom) and secondary is on the right (portrait top)
314         val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx
315         val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
316         val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig)
317 
318         val taskPercent =
319             if (splitBoundsConfig.appsStackedVertically) {
320                 splitBoundsConfig.topTaskPercent
321             } else {
322                 splitBoundsConfig.leftTaskPercent
323             }
324         val firstTaskViewSize = Point(parentWidth, (totalThumbnailHeight * taskPercent).toInt())
325         val secondTaskViewSize =
326             Point(parentWidth, totalThumbnailHeight - firstTaskViewSize.y - dividerBar)
327         return Pair(firstTaskViewSize, secondTaskViewSize)
328     }
329 
330     /* ---------- The following are only used by TaskViewTouchHandler. ---------- */
331     override val upDownSwipeDirection: SingleAxisSwipeDetector.Direction =
332         SingleAxisSwipeDetector.HORIZONTAL
333 
getUpDirectionnull334     override fun getUpDirection(isRtl: Boolean): Int =
335         if (isRtl) SingleAxisSwipeDetector.DIRECTION_POSITIVE
336         else SingleAxisSwipeDetector.DIRECTION_NEGATIVE
337 
338     override fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean =
339         if (isRtl) displacement > 0 else displacement < 0
340 
341     override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) -1 else 1
342     /* -------------------- */
343 
344     override fun getSplitIconsPosition(
345         taskIconHeight: Int,
346         primarySnapshotHeight: Int,
347         totalThumbnailHeight: Int,
348         isRtl: Boolean,
349         overviewTaskMarginPx: Int,
350         dividerSize: Int,
351     ): SplitIconPositions {
352         return if (Flags.enableOverviewIconMenu()) {
353             if (isRtl) {
354                 SplitIconPositions(
355                     topLeftY = totalThumbnailHeight - primarySnapshotHeight,
356                     bottomRightY = 0
357                 )
358             } else {
359                 SplitIconPositions(
360                     topLeftY = 0,
361                     bottomRightY = -(primarySnapshotHeight + dividerSize)
362                 )
363             }
364         } else {
365             // In seascape, the icons are initially placed at the bottom start of the
366             // display (portrait locked). The values defined here are used to translate the icons
367             // from the bottom to the almost-center of the screen using the bottom margin.
368             // The primary snapshot is placed at the bottom, thus we translate the icons using
369             // the size of the primary snapshot minus the icon size for the top-left icon.
370             SplitIconPositions(
371                 topLeftY = primarySnapshotHeight - taskIconHeight,
372                 bottomRightY = primarySnapshotHeight + dividerSize
373             )
374         }
375     }
376 
377     /**
378      * Updates icon view gravity and translation for split tasks
379      *
380      * @param iconView View to be updated
381      * @param translationY the translationY that should be applied
382      * @param isRtl Whether the layout direction is RTL (or false for LTR).
383      */
384     @SuppressLint("RtlHardcoded")
updateSplitIconsPositionnull385     override fun updateSplitIconsPosition(iconView: View, translationY: Int, isRtl: Boolean) {
386         val layoutParams = iconView.layoutParams as FrameLayout.LayoutParams
387 
388         if (Flags.enableOverviewIconMenu()) {
389             val appChipView = iconView as IconAppChipView
390             layoutParams.gravity =
391                 if (isRtl) Gravity.TOP or Gravity.END else Gravity.BOTTOM or Gravity.START
392             appChipView.layoutParams = layoutParams
393             appChipView.setSplitTranslationX(0f)
394             appChipView.setSplitTranslationY(translationY.toFloat())
395         } else {
396             layoutParams.gravity = Gravity.BOTTOM or Gravity.LEFT
397             iconView.translationX = 0f
398             iconView.translationY = 0f
399             layoutParams.bottomMargin = translationY
400             iconView.layoutParams = layoutParams
401         }
402     }
403 
404     @Override
getHandlerTypeForLoggingnull405     override fun getHandlerTypeForLogging(): LauncherAtom.TaskSwitcherContainer.OrientationHandler =
406         LauncherAtom.TaskSwitcherContainer.OrientationHandler.SEASCAPE
407 }
408