xref: /aosp_15_r20/platform_testing/libraries/flicker/utils/src/android/tools/helpers/WindowUtils.kt (revision dd0948b35e70be4c0246aabd6c72554a5eb8b22a)
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.tools.traces.wm.InsetsSource
27 import android.util.LruCache
28 import android.view.WindowInsets
29 import androidx.test.platform.app.InstrumentationRegistry
30 
31 object WindowUtils {
32 
33     private val displayBoundsCache = LruCache<Rotation, Rect>(4)
34     private val instrumentation = InstrumentationRegistry.getInstrumentation()
35 
36     /** Helper functions to retrieve system window sizes and positions. */
<lambda>null37     private val context by lazy { instrumentation.context }
38 
39     private val resources
40         get() = context.resources
41 
42     /** Get the display bounds */
43     val displayBounds: Rect
44         get() {
45             val currState = getCurrentStateDump(clearCacheAfterParsing = false)
46             return currState.layerState.physicalDisplay?.layerStackSpace ?: Rect()
47         }
48 
49     val displayStableBounds: Rect
50         get() {
51             val currState = getCurrentStateDump(clearCacheAfterParsing = false)
52             return currState.wmState.getDefaultDisplay()?.stableBounds ?: Rect()
53         }
54 
55     /** Gets the current display rotation */
56     val displayRotation: Rotation
57         get() {
58             val currState = getCurrentStateDump(clearCacheAfterParsing = false)
59             return currState.wmState.getRotation(PlatformConsts.DEFAULT_DISPLAY)
60         }
61 
62     /**
63      * Get the display bounds when the device is at a specific rotation
64      *
65      * @param requestedRotation Device rotation
66      */
67     @JvmStatic
getDisplayBoundsnull68     fun getDisplayBounds(requestedRotation: Rotation): Rect {
69         return displayBoundsCache[requestedRotation]
70             ?: let {
71                 val displayIsRotated = displayRotation.isRotated()
72                 val requestedDisplayIsRotated = requestedRotation.isRotated()
73 
74                 // if the current orientation changes with the requested rotation,
75                 // flip height and width of display bounds.
76                 val displayBounds = displayBounds
77                 val retval =
78                     if (displayIsRotated != requestedDisplayIsRotated) {
79                         Rect(0, 0, displayBounds.height(), displayBounds.width())
80                     } else {
81                         Rect(0, 0, displayBounds.width(), displayBounds.height())
82                     }
83                 displayBoundsCache.put(requestedRotation, retval)
84                 return retval
85             }
86     }
87 
88     @JvmStatic
getInsetDisplayBoundsnull89     fun getInsetDisplayBounds(requestedRotation: Rotation): Rect {
90         val currState = getCurrentStateDump(clearCacheAfterParsing = false)
91         val display = currState.wmState.getDefaultDisplay() ?: error("Missing physical display")
92 
93         // check device is rotated, and if so, rotate the returned inset bounds
94         val insetDisplayBounds =
95             with(display.displayRect) {
96                 if (displayRotation.isRotated() == requestedRotation.isRotated()) {
97                     Rect(left, top, right, bottom)
98                 } else {
99                     Rect(left, top, bottom, right)
100                 }
101             }
102 
103         // Find visible insets from status bar and navigation bar (equivalent to taskbar)
104         display.insetsSourceProviders.forEach {
105             val insetsSource: InsetsSource = it.source ?: return@forEach
106             val insets: Rect = it.frame ?: return@forEach
107             if (!insetsSource.visible) return@forEach
108 
109             when (insetsSource.type) {
110                 // Returned insets are based on the display bounds in its natural orientation,
111                 // so we calculate the delta between the insets and display bounds when not rotated,
112                 // then apply it to the properly rotated (if necessary) display bounds
113                 WindowInsets.Type.statusBars() -> {
114                     val topDelta = insets.bottom - display.displayRect.top
115                     insetDisplayBounds.top += topDelta
116                 }
117                 WindowInsets.Type.navigationBars() -> {
118                     val botDelta = display.displayRect.bottom - insets.top
119                     insetDisplayBounds.bottom -= botDelta
120                 }
121             }
122         }
123 
124         return insetDisplayBounds
125     }
126 
127     /** Gets the status bar height with a specific display cutout. */
getExpectedStatusBarHeightnull128     private fun getExpectedStatusBarHeight(displayContent: DisplayContent): Int {
129         val cutout = displayContent.cutout
130         val defaultSize = status_bar_height_default
131         val safeInsetTop = cutout?.insets?.top ?: 0
132         val waterfallInsetTop = cutout?.waterfallInsets?.top ?: 0
133         // The status bar height should be:
134         // Max(top cutout size, (status bar default height + waterfall top size))
135         return safeInsetTop.coerceAtLeast(defaultSize + waterfallInsetTop)
136     }
137 
138     /**
139      * Gets the expected status bar position for a specific display
140      *
141      * @param display the main display
142      */
143     @JvmStatic
getExpectedStatusBarPositionnull144     fun getExpectedStatusBarPosition(display: DisplayContent): Region {
145         val height = getExpectedStatusBarHeight(display)
146         return Region(0, 0, display.displayRect.width(), height)
147     }
148 
149     /**
150      * Gets the expected navigation bar position for a specific display
151      *
152      * @param display the main display
153      */
154     @JvmStatic
getNavigationBarPositionnull155     fun getNavigationBarPosition(display: Display): Region {
156         return getNavigationBarPosition(display, isGesturalNavigationEnabled)
157     }
158 
159     /**
160      * Gets the expected navigation bar position for a specific display
161      *
162      * @param display the main display
163      * @param isGesturalNavigation whether gestural navigation is enabled
164      */
165     @JvmStatic
getNavigationBarPositionnull166     fun getNavigationBarPosition(display: Display, isGesturalNavigation: Boolean): Region {
167         val navBarWidth = getDimensionPixelSize("navigation_bar_width")
168         val displayHeight = display.layerStackSpace.height()
169         val displayWidth = display.layerStackSpace.width()
170         val requestedRotation = display.transform.getRotation()
171         val navBarHeight = getNavigationBarFrameHeight(requestedRotation, isGesturalNavigation)
172 
173         return when {
174             // nav bar is at the bottom of the screen
175             !requestedRotation.isRotated() || isGesturalNavigation ->
176                 Region(0, displayHeight - navBarHeight, displayWidth, displayHeight)
177             // nav bar is on the right side
178             requestedRotation == Rotation.ROTATION_90 ->
179                 Region(displayWidth - navBarWidth, 0, displayWidth, displayHeight)
180             // nav bar is on the left side
181             requestedRotation == Rotation.ROTATION_270 -> Region(0, 0, navBarWidth, displayHeight)
182             else -> error("Unknown rotation $requestedRotation")
183         }
184     }
185 
186     /**
187      * Estimate the navigation bar position at a specific rotation
188      *
189      * @param requestedRotation Device rotation
190      */
191     @JvmStatic
estimateNavigationBarPositionnull192     fun estimateNavigationBarPosition(requestedRotation: Rotation): Region {
193         val displayBounds = displayBounds
194         val displayWidth: Int
195         val displayHeight: Int
196         if (!requestedRotation.isRotated()) {
197             displayWidth = displayBounds.width()
198             displayHeight = displayBounds.height()
199         } else {
200             // swap display dimensions in landscape or seascape mode
201             displayWidth = displayBounds.height()
202             displayHeight = displayBounds.width()
203         }
204         val navBarWidth = getDimensionPixelSize("navigation_bar_width")
205         val navBarHeight =
206             getNavigationBarFrameHeight(requestedRotation, isGesturalNavigation = false)
207 
208         return when {
209             // nav bar is at the bottom of the screen
210             !requestedRotation.isRotated() || isGesturalNavigationEnabled ->
211                 Region(0, displayHeight - navBarHeight, displayWidth, displayHeight)
212             // nav bar is on the right side
213             requestedRotation == Rotation.ROTATION_90 ->
214                 Region(displayWidth - navBarWidth, 0, displayWidth, displayHeight)
215             // nav bar is on the left side
216             requestedRotation == Rotation.ROTATION_270 -> Region(0, 0, navBarWidth, displayHeight)
217             else -> error("Unknown rotation $requestedRotation")
218         }
219     }
220 
221     /** Checks if the device uses gestural navigation */
222     val isGesturalNavigationEnabled: Boolean
223         get() {
224             val resourceId =
225                 resources.getIdentifier("config_navBarInteractionMode", "integer", "android")
226             return resources.getInteger(resourceId) == 2
227         }
228 
229     @JvmStatic
getDimensionPixelSizenull230     fun getDimensionPixelSize(resourceName: String): Int {
231         val resourceId = resources.getIdentifier(resourceName, "dimen", "android")
232         return resources.getDimensionPixelSize(resourceId)
233     }
234 
235     /** Gets the navigation bar frame height */
236     @JvmStatic
getNavigationBarFrameHeightnull237     fun getNavigationBarFrameHeight(rotation: Rotation, isGesturalNavigation: Boolean): Int {
238         return if (rotation.isRotated()) {
239             if (isGesturalNavigation) {
240                 getDimensionPixelSize("navigation_bar_frame_height")
241             } else {
242                 getDimensionPixelSize("navigation_bar_height_landscape")
243             }
244         } else {
245             getDimensionPixelSize("navigation_bar_frame_height")
246         }
247     }
248 
249     private val status_bar_height_default: Int
250         get() {
251             val resourceId =
252                 resources.getIdentifier("status_bar_height_default", "dimen", "android")
253             return resources.getDimensionPixelSize(resourceId)
254         }
255 
256     val quick_qs_offset_height: Int
257         get() {
258             val resourceId = resources.getIdentifier("quick_qs_offset_height", "dimen", "android")
259             return resources.getDimensionPixelSize(resourceId)
260         }
261 
262     /** Split screen divider inset height */
263     val dockedStackDividerInset: Int
264         get() {
265             val resourceId =
266                 resources.getIdentifier("docked_stack_divider_insets", "dimen", "android")
267             return resources.getDimensionPixelSize(resourceId)
268         }
269 }
270