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