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
17 package com.android.wm.shell.desktopmode
18
19 import android.app.TaskInfo
20 import android.content.res.Resources
21 import android.graphics.Point
22 import android.graphics.Rect
23 import android.view.Gravity
24 import com.android.internal.annotations.VisibleForTesting
25 import com.android.wm.shell.R
26 import com.android.wm.shell.desktopmode.DesktopTaskPosition.BottomLeft
27 import com.android.wm.shell.desktopmode.DesktopTaskPosition.BottomRight
28 import com.android.wm.shell.desktopmode.DesktopTaskPosition.Center
29 import com.android.wm.shell.desktopmode.DesktopTaskPosition.TopLeft
30 import com.android.wm.shell.desktopmode.DesktopTaskPosition.TopRight
31
32 /** The position of a task window in desktop mode. */
33 sealed class DesktopTaskPosition {
34 data object Center : DesktopTaskPosition() {
35 private const val WINDOW_HEIGHT_PROPORTION = 0.375
36
getTopLeftCoordinatesnull37 override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
38 val x = (frame.width() - window.width()) / 2
39 // Position with more margin at the bottom.
40 val y = (frame.height() - window.height()) * WINDOW_HEIGHT_PROPORTION + frame.top
41 return Point(x, y.toInt())
42 }
43
nextnull44 override fun next(): DesktopTaskPosition = BottomRight
45 }
46
47 data object BottomRight : DesktopTaskPosition() {
48 override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point =
49 Point(frame.right - window.width(), frame.bottom - window.height())
50
51 override fun next(): DesktopTaskPosition = TopLeft
52 }
53
54 data object TopLeft : DesktopTaskPosition() {
getTopLeftCoordinatesnull55 override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point =
56 Point(frame.left, frame.top)
57
58 override fun next(): DesktopTaskPosition = BottomLeft
59 }
60
61 data object BottomLeft : DesktopTaskPosition() {
62 override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point =
63 Point(frame.left, frame.bottom - window.height())
64
65 override fun next(): DesktopTaskPosition = TopRight
66 }
67
68 data object TopRight : DesktopTaskPosition() {
getTopLeftCoordinatesnull69 override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point =
70 Point(frame.right - window.width(), frame.top)
71
72 override fun next(): DesktopTaskPosition = Center
73 }
74
75 /**
76 * Returns the top left coordinates for the window to be placed in the given DesktopTaskPosition
77 * in the frame.
78 */
79 abstract fun getTopLeftCoordinates(frame: Rect, window: Rect): Point
80
81 abstract fun next(): DesktopTaskPosition
82 }
83
84 /**
85 * If the app has specified horizontal or vertical gravity layout, don't change the task position
86 * for cascading effect.
87 */
88 fun canChangeTaskPosition(taskInfo: TaskInfo): Boolean {
89 taskInfo.topActivityInfo?.windowLayout?.let {
90 val horizontalGravityApplied = it.gravity.and(Gravity.HORIZONTAL_GRAVITY_MASK)
91 val verticalGravityApplied = it.gravity.and(Gravity.VERTICAL_GRAVITY_MASK)
92 return horizontalGravityApplied == 0 && verticalGravityApplied == 0
93 }
94 return true
95 }
96
97 /** Returns the current DesktopTaskPosition for a given window in the frame. */
98 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
Rectnull99 fun Rect.getDesktopTaskPosition(bounds: Rect): DesktopTaskPosition {
100 return when {
101 top == bounds.top && left == bounds.left && bottom != bounds.bottom -> TopLeft
102 top == bounds.top && right == bounds.right && bottom != bounds.bottom -> TopRight
103 bottom == bounds.bottom && left == bounds.left && top != bounds.top -> BottomLeft
104 bottom == bounds.bottom && right == bounds.right && top != bounds.top -> BottomRight
105 else -> Center
106 }
107 }
108
cascadeWindownull109 internal fun cascadeWindow(res: Resources, frame: Rect, prev: Rect, dest: Rect) {
110 val candidateBounds = Rect(dest)
111 val lastPos = frame.getDesktopTaskPosition(prev)
112 var destCoord = Center.getTopLeftCoordinates(frame, candidateBounds)
113 candidateBounds.offsetTo(destCoord.x, destCoord.y)
114 // If the default center position is not free or if last focused window is not at the
115 // center, get the next cascading window position.
116 if (!prevBoundsMovedAboveThreshold(res, prev, candidateBounds) || Center != lastPos) {
117 val nextCascadingPos = lastPos.next()
118 destCoord = nextCascadingPos.getTopLeftCoordinates(frame, dest)
119 }
120 dest.offsetTo(destCoord.x, destCoord.y)
121 }
122
prevBoundsMovedAboveThresholdnull123 internal fun prevBoundsMovedAboveThreshold(res: Resources, prev: Rect, newBounds: Rect): Boolean {
124 // This is the required minimum dp for a task to be touchable.
125 val moveThresholdPx =
126 res.getDimensionPixelSize(R.dimen.freeform_required_visible_empty_space_in_header)
127 val leftFar = newBounds.left - prev.left > moveThresholdPx
128 val topFar = newBounds.top - prev.top > moveThresholdPx
129 val rightFar = prev.right - newBounds.right > moveThresholdPx
130 val bottomFar = prev.bottom - newBounds.bottom > moveThresholdPx
131
132 return leftFar || topFar || rightFar || bottomFar
133 }
134