1 /*
2  * Copyright (C) 2023 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 android.tools.helpers
18 
19 import android.graphics.Rect
20 import android.graphics.Region
21 import android.tools.PlatformConsts
22 import android.tools.Rotation
23 import android.tools.traces.getCurrentStateDump
24 import android.tools.traces.surfaceflinger.Display
25 import android.tools.traces.wm.DisplayContent
26 import android.util.LruCache
27 import androidx.test.platform.app.InstrumentationRegistry
28 
29 object WindowUtils {
30 
31     private val displayBoundsCache = LruCache<Rotation, Rect>(4)
32     private val instrumentation = InstrumentationRegistry.getInstrumentation()
33 
34     /** Helper functions to retrieve system window sizes and positions. */
35     private val context = instrumentation.context
36 
37     private val resources
38         get() = context.getResources()
39 
40     /** Get the display bounds */
41     val displayBounds: Rect
42         get() {
43             val currState = getCurrentStateDump(clearCacheAfterParsing = false)
44             return currState.layerState.physicalDisplay?.layerStackSpace ?: Rect()
45         }
46 
47     /** Gets the current display rotation */
48     val displayRotation: Rotation
49         get() {
50             val currState = getCurrentStateDump(clearCacheAfterParsing = false)
51 
52             return currState.wmState.getRotation(PlatformConsts.DEFAULT_DISPLAY)
53         }
54 
55     /**
56      * Get the display bounds when the device is at a specific rotation
57      *
58      * @param requestedRotation Device rotation
59      */
getDisplayBoundsnull60     fun getDisplayBounds(requestedRotation: Rotation): Rect {
61         return displayBoundsCache[requestedRotation]
62             ?: let {
63                 val displayIsRotated = displayRotation.isRotated()
64                 val requestedDisplayIsRotated = requestedRotation.isRotated()
65 
66                 // if the current orientation changes with the requested rotation,
67                 // flip height and width of display bounds.
68                 val displayBounds = displayBounds
69                 val retval =
70                     if (displayIsRotated != requestedDisplayIsRotated) {
71                         Rect(0, 0, displayBounds.height(), displayBounds.width())
72                     } else {
73                         Rect(0, 0, displayBounds.width(), displayBounds.height())
74                     }
75                 displayBoundsCache.put(requestedRotation, retval)
76                 return retval
77             }
78     }
79 
80     /** Gets the status bar height with a specific display cutout. */
getExpectedStatusBarHeightnull81     private fun getExpectedStatusBarHeight(displayContent: DisplayContent): Int {
82         val cutout = displayContent.cutout
83         val defaultSize = status_bar_height_default
84         val safeInsetTop = cutout?.insets?.top ?: 0
85         val waterfallInsetTop = cutout?.waterfallInsets?.top ?: 0
86         // The status bar height should be:
87         // Max(top cutout size, (status bar default height + waterfall top size))
88         return safeInsetTop.coerceAtLeast(defaultSize + waterfallInsetTop)
89     }
90 
91     /**
92      * Gets the expected status bar position for a specific display
93      *
94      * @param display the main display
95      */
getExpectedStatusBarPositionnull96     fun getExpectedStatusBarPosition(display: DisplayContent): Region {
97         val height = getExpectedStatusBarHeight(display)
98         return Region(0, 0, display.displayRect.width(), height)
99     }
100 
101     /**
102      * Gets the expected navigation bar position for a specific display
103      *
104      * @param display the main display
105      */
getNavigationBarPositionnull106     fun getNavigationBarPosition(display: Display): Region {
107         return getNavigationBarPosition(display, isGesturalNavigationEnabled)
108     }
109 
110     /**
111      * Gets the expected navigation bar position for a specific display
112      *
113      * @param display the main display
114      * @param isGesturalNavigation whether gestural navigation is enabled
115      */
getNavigationBarPositionnull116     fun getNavigationBarPosition(display: Display, isGesturalNavigation: Boolean): Region {
117         val navBarWidth = getDimensionPixelSize("navigation_bar_width")
118         val displayHeight = display.layerStackSpace.height()
119         val displayWidth = display.layerStackSpace.width()
120         val requestedRotation = display.transform.getRotation()
121         val navBarHeight = getNavigationBarFrameHeight(requestedRotation, isGesturalNavigation)
122 
123         return when {
124             // nav bar is at the bottom of the screen
125             !requestedRotation.isRotated() || isGesturalNavigation ->
126                 Region(0, displayHeight - navBarHeight, displayWidth, displayHeight)
127             // nav bar is on the right side
128             requestedRotation == Rotation.ROTATION_90 ->
129                 Region(displayWidth - navBarWidth, 0, displayWidth, displayHeight)
130             // nav bar is on the left side
131             requestedRotation == Rotation.ROTATION_270 -> Region(0, 0, navBarWidth, displayHeight)
132             else -> error("Unknown rotation $requestedRotation")
133         }
134     }
135 
136     /**
137      * Estimate the navigation bar position at a specific rotation
138      *
139      * @param requestedRotation Device rotation
140      */
estimateNavigationBarPositionnull141     fun estimateNavigationBarPosition(requestedRotation: Rotation): Region {
142         val displayBounds = displayBounds
143         val displayWidth: Int
144         val displayHeight: Int
145         if (!requestedRotation.isRotated()) {
146             displayWidth = displayBounds.width()
147             displayHeight = displayBounds.height()
148         } else {
149             // swap display dimensions in landscape or seascape mode
150             displayWidth = displayBounds.height()
151             displayHeight = displayBounds.width()
152         }
153         val navBarWidth = getDimensionPixelSize("navigation_bar_width")
154         val navBarHeight =
155             getNavigationBarFrameHeight(requestedRotation, isGesturalNavigation = false)
156 
157         return when {
158             // nav bar is at the bottom of the screen
159             !requestedRotation.isRotated() || isGesturalNavigationEnabled ->
160                 Region(0, displayHeight - navBarHeight, displayWidth, displayHeight)
161             // nav bar is on the right side
162             requestedRotation == Rotation.ROTATION_90 ->
163                 Region(displayWidth - navBarWidth, 0, displayWidth, displayHeight)
164             // nav bar is on the left side
165             requestedRotation == Rotation.ROTATION_270 -> Region(0, 0, navBarWidth, displayHeight)
166             else -> error("Unknown rotation $requestedRotation")
167         }
168     }
169 
170     /** Checks if the device uses gestural navigation */
171     val isGesturalNavigationEnabled: Boolean
172         get() {
173             val resourceId =
174                 resources.getIdentifier("config_navBarInteractionMode", "integer", "android")
175             return resources.getInteger(resourceId) == 2
176         }
177 
getDimensionPixelSizenull178     fun getDimensionPixelSize(resourceName: String): Int {
179         val resourceId = resources.getIdentifier(resourceName, "dimen", "android")
180         return resources.getDimensionPixelSize(resourceId)
181     }
182 
183     /** Gets the navigation bar frame height */
getNavigationBarFrameHeightnull184     fun getNavigationBarFrameHeight(rotation: Rotation, isGesturalNavigation: Boolean): Int {
185         return if (rotation.isRotated()) {
186             if (isGesturalNavigation) {
187                 getDimensionPixelSize("navigation_bar_frame_height")
188             } else {
189                 getDimensionPixelSize("navigation_bar_height_landscape")
190             }
191         } else {
192             getDimensionPixelSize("navigation_bar_frame_height")
193         }
194     }
195 
196     private val status_bar_height_default: Int
197         get() {
198             val resourceId =
199                 resources.getIdentifier("status_bar_height_default", "dimen", "android")
200             return resources.getDimensionPixelSize(resourceId)
201         }
202 
203     val quick_qs_offset_height: Int
204         get() {
205             val resourceId = resources.getIdentifier("quick_qs_offset_height", "dimen", "android")
206             return resources.getDimensionPixelSize(resourceId)
207         }
208 
209     /** Split screen divider inset height */
210     val dockedStackDividerInset: Int
211         get() {
212             val resourceId =
213                 resources.getIdentifier("docked_stack_divider_insets", "dimen", "android")
214             return resources.getDimensionPixelSize(resourceId)
215         }
216 }
217