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