• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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     /** Gets the default display ID. */
63     val defaultDisplayId: Int
64         get() =
65             getCurrentStateDump(clearCacheAfterParsing = false)
66                 .wmState
67                 .getDefaultDisplay()
68                 ?.displayId ?: error("Missing physical display")
69 
70     /**
71      * Get the display bounds when the device is at a specific rotation
72      *
73      * @param requestedRotation Device rotation
74      */
75     @JvmStatic
getDisplayBoundsnull76     fun getDisplayBounds(requestedRotation: Rotation): Rect {
77         return displayBoundsCache[requestedRotation]
78             ?: let {
79                 val displayIsRotated = displayRotation.isRotated()
80                 val requestedDisplayIsRotated = requestedRotation.isRotated()
81 
82                 // if the current orientation changes with the requested rotation,
83                 // flip height and width of display bounds.
84                 val displayBounds = displayBounds
85                 val retval =
86                     if (displayIsRotated != requestedDisplayIsRotated) {
87                         Rect(0, 0, displayBounds.height(), displayBounds.width())
88                     } else {
89                         Rect(0, 0, displayBounds.width(), displayBounds.height())
90                     }
91                 displayBoundsCache.put(requestedRotation, retval)
92                 return retval
93             }
94     }
95 
96     @JvmStatic
getInsetDisplayBoundsnull97     fun getInsetDisplayBounds(requestedRotation: Rotation): Rect {
98         val currState = getCurrentStateDump(clearCacheAfterParsing = false)
99         val display = currState.wmState.getDefaultDisplay() ?: error("Missing physical display")
100 
101         // check device is rotated, and if so, rotate the returned inset bounds
102         val insetDisplayBounds =
103             with(display.displayRect) {
104                 if (displayRotation.isRotated() == requestedRotation.isRotated()) {
105                     Rect(left, top, right, bottom)
106                 } else {
107                     Rect(left, top, bottom, right)
108                 }
109             }
110 
111         // Find visible insets from status bar and navigation bar (equivalent to taskbar)
112         display.insetsSourceProviders.forEach {
113             val insetsSource: InsetsSource = it.source ?: return@forEach
114             val insets: Rect = it.frame ?: return@forEach
115             if (!insetsSource.visible) return@forEach
116 
117             when (insetsSource.type) {
118                 // Returned insets are based on the display bounds in its natural orientation,
119                 // so we calculate the delta between the insets and display bounds when not rotated,
120                 // then apply it to the properly rotated (if necessary) display bounds
121                 WindowInsets.Type.statusBars() -> {
122                     val topDelta = insets.bottom - display.displayRect.top
123                     insetDisplayBounds.top += topDelta
124                 }
125                 WindowInsets.Type.navigationBars() -> {
126                     val botDelta = display.displayRect.bottom - insets.top
127                     insetDisplayBounds.bottom -= botDelta
128                 }
129             }
130         }
131 
132         return insetDisplayBounds
133     }
134 
135     /** Gets the status bar height with a specific display cutout. */
getExpectedStatusBarHeightnull136     private fun getExpectedStatusBarHeight(displayContent: DisplayContent): Int {
137         val cutout = displayContent.cutout
138         val defaultSize = status_bar_height_default
139         val safeInsetTop = cutout?.insets?.top ?: 0
140         val waterfallInsetTop = cutout?.waterfallInsets?.top ?: 0
141         // The status bar height should be:
142         // Max(top cutout size, (status bar default height + waterfall top size))
143         return safeInsetTop.coerceAtLeast(defaultSize + waterfallInsetTop)
144     }
145 
146     /**
147      * Gets the expected status bar position for a specific display
148      *
149      * @param display the main display
150      */
151     @JvmStatic
getExpectedStatusBarPositionnull152     fun getExpectedStatusBarPosition(display: DisplayContent): Region {
153         val height = getExpectedStatusBarHeight(display)
154         return Region(0, 0, display.displayRect.width(), height)
155     }
156 
157     /**
158      * Gets the expected navigation bar position for a specific display
159      *
160      * @param display the main display
161      */
162     @JvmStatic
getNavigationBarPositionnull163     fun getNavigationBarPosition(display: Display): Region {
164         return getNavigationBarPosition(display, isGesturalNavigationEnabled)
165     }
166 
167     /**
168      * Gets the expected navigation bar position for a specific display
169      *
170      * @param display the main display
171      * @param isGesturalNavigation whether gestural navigation is enabled
172      */
173     @JvmStatic
getNavigationBarPositionnull174     fun getNavigationBarPosition(display: Display, isGesturalNavigation: Boolean): Region {
175         val navBarWidth = getDimensionPixelSize("navigation_bar_width")
176         val displayHeight = display.layerStackSpace.height()
177         val displayWidth = display.layerStackSpace.width()
178         val requestedRotation = display.transform.getRotation()
179         val navBarHeight = getNavigationBarFrameHeight(requestedRotation, isGesturalNavigation)
180 
181         return when {
182             // nav bar is at the bottom of the screen
183             !requestedRotation.isRotated() || isGesturalNavigation ->
184                 Region(0, displayHeight - navBarHeight, displayWidth, displayHeight)
185             // nav bar is on the right side
186             requestedRotation == Rotation.ROTATION_90 ->
187                 Region(displayWidth - navBarWidth, 0, displayWidth, displayHeight)
188             // nav bar is on the left side
189             requestedRotation == Rotation.ROTATION_270 -> Region(0, 0, navBarWidth, displayHeight)
190             else -> error("Unknown rotation $requestedRotation")
191         }
192     }
193 
194     /**
195      * Estimate the navigation bar position at a specific rotation
196      *
197      * @param requestedRotation Device rotation
198      */
199     @JvmStatic
estimateNavigationBarPositionnull200     fun estimateNavigationBarPosition(requestedRotation: Rotation): Region {
201         val displayBounds = displayBounds
202         val displayWidth: Int
203         val displayHeight: Int
204         if (!requestedRotation.isRotated()) {
205             displayWidth = displayBounds.width()
206             displayHeight = displayBounds.height()
207         } else {
208             // swap display dimensions in landscape or seascape mode
209             displayWidth = displayBounds.height()
210             displayHeight = displayBounds.width()
211         }
212         val navBarWidth = getDimensionPixelSize("navigation_bar_width")
213         val navBarHeight =
214             getNavigationBarFrameHeight(requestedRotation, isGesturalNavigation = false)
215 
216         return when {
217             // nav bar is at the bottom of the screen
218             !requestedRotation.isRotated() || isGesturalNavigationEnabled ->
219                 Region(0, displayHeight - navBarHeight, displayWidth, displayHeight)
220             // nav bar is on the right side
221             requestedRotation == Rotation.ROTATION_90 ->
222                 Region(displayWidth - navBarWidth, 0, displayWidth, displayHeight)
223             // nav bar is on the left side
224             requestedRotation == Rotation.ROTATION_270 -> Region(0, 0, navBarWidth, displayHeight)
225             else -> error("Unknown rotation $requestedRotation")
226         }
227     }
228 
229     /** Checks if the device uses gestural navigation */
230     val isGesturalNavigationEnabled: Boolean
231         get() {
232             val resourceId =
233                 resources.getIdentifier("config_navBarInteractionMode", "integer", "android")
234             return resources.getInteger(resourceId) == 2
235         }
236 
237     @JvmStatic
getDimensionPixelSizenull238     fun getDimensionPixelSize(resourceName: String): Int {
239         val resourceId = resources.getIdentifier(resourceName, "dimen", "android")
240         return resources.getDimensionPixelSize(resourceId)
241     }
242 
243     /** Gets the navigation bar frame height */
244     @JvmStatic
getNavigationBarFrameHeightnull245     fun getNavigationBarFrameHeight(rotation: Rotation, isGesturalNavigation: Boolean): Int {
246         return if (rotation.isRotated()) {
247             if (isGesturalNavigation) {
248                 getDimensionPixelSize("navigation_bar_frame_height")
249             } else {
250                 getDimensionPixelSize("navigation_bar_height_landscape")
251             }
252         } else {
253             getDimensionPixelSize("navigation_bar_frame_height")
254         }
255     }
256 
257     private val status_bar_height_default: Int
258         get() {
259             val resourceId =
260                 resources.getIdentifier("status_bar_height_default", "dimen", "android")
261             return resources.getDimensionPixelSize(resourceId)
262         }
263 
264     val quick_qs_offset_height: Int
265         get() {
266             val resourceId = resources.getIdentifier("quick_qs_offset_height", "dimen", "android")
267             return resources.getDimensionPixelSize(resourceId)
268         }
269 
270     /** Split screen divider inset height */
271     val dockedStackDividerInset: Int
272         get() {
273             val resourceId =
274                 resources.getIdentifier("docked_stack_divider_insets", "dimen", "android")
275             return resources.getDimensionPixelSize(resourceId)
276         }
277 }
278