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.device.helpers 18 19 import android.graphics.Rect 20 import android.tools.common.PlatformConsts 21 import android.tools.common.Rotation 22 import android.tools.common.datatypes.Region 23 import android.tools.common.traces.surfaceflinger.Display 24 import android.tools.common.traces.wm.DisplayContent 25 import android.tools.device.traces.getCurrentStateDump 26 import android.tools.device.traces.parsers.toAndroidRect 27 import android.util.LruCache 28 import androidx.test.platform.app.InstrumentationRegistry 29 30 object WindowUtils { 31 32 private val displayBoundsCache = LruCache<Rotation, Region>(1) 33 private val instrumentation = InstrumentationRegistry.getInstrumentation() 34 35 /** Helper functions to retrieve system window sizes and positions. */ 36 private val context = instrumentation.context 37 38 private val resources 39 get() = context.getResources() 40 41 /** Get the display bounds */ 42 val displayBounds: Rect 43 get() { 44 val currState = getCurrentStateDump(clearCacheAfterParsing = false) 45 return currState.layerState.physicalDisplay?.layerStackSpace?.toAndroidRect() ?: Rect() 46 } 47 48 /** Gets the current display rotation */ 49 val displayRotation: Rotation 50 get() { 51 val currState = getCurrentStateDump(clearCacheAfterParsing = false) 52 53 return currState.wmState.getRotation(PlatformConsts.DEFAULT_DISPLAY) 54 } 55 56 /** 57 * Get the display bounds when the device is at a specific rotation 58 * 59 * @param requestedRotation Device rotation 60 */ getDisplayBoundsnull61 fun getDisplayBounds(requestedRotation: Rotation): Region { 62 return displayBoundsCache[requestedRotation] 63 ?: let { 64 val displayIsRotated = displayRotation.isRotated() 65 val requestedDisplayIsRotated = requestedRotation.isRotated() 66 67 // if the current orientation changes with the requested rotation, 68 // flip height and width of display bounds. 69 val displayBounds = displayBounds 70 val retval: Region 71 if (displayIsRotated != requestedDisplayIsRotated) { 72 retval = Region.from(0, 0, displayBounds.height(), displayBounds.width()) 73 } else { 74 retval = Region.from(0, 0, displayBounds.width(), displayBounds.height()) 75 } 76 displayBoundsCache.put(requestedRotation, retval) 77 return retval 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.from(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.from(0, displayHeight - navBarHeight, displayWidth, displayHeight) 127 // nav bar is on the right side 128 requestedRotation == Rotation.ROTATION_90 -> 129 Region.from(displayWidth - navBarWidth, 0, displayWidth, displayHeight) 130 // nav bar is on the left side 131 requestedRotation == Rotation.ROTATION_270 -> 132 Region.from(0, 0, navBarWidth, displayHeight) 133 else -> error("Unknown rotation $requestedRotation") 134 } 135 } 136 137 /** 138 * Estimate the navigation bar position at a specific rotation 139 * 140 * @param requestedRotation Device rotation 141 */ estimateNavigationBarPositionnull142 fun estimateNavigationBarPosition(requestedRotation: Rotation): Region { 143 val displayBounds = displayBounds 144 val displayWidth: Int 145 val displayHeight: Int 146 if (!requestedRotation.isRotated()) { 147 displayWidth = displayBounds.width() 148 displayHeight = displayBounds.height() 149 } else { 150 // swap display dimensions in landscape or seascape mode 151 displayWidth = displayBounds.height() 152 displayHeight = displayBounds.width() 153 } 154 val navBarWidth = getDimensionPixelSize("navigation_bar_width") 155 val navBarHeight = 156 getNavigationBarFrameHeight(requestedRotation, isGesturalNavigation = false) 157 158 return when { 159 // nav bar is at the bottom of the screen 160 !requestedRotation.isRotated() || isGesturalNavigationEnabled -> 161 Region.from(0, displayHeight - navBarHeight, displayWidth, displayHeight) 162 // nav bar is on the right side 163 requestedRotation == Rotation.ROTATION_90 -> 164 Region.from(displayWidth - navBarWidth, 0, displayWidth, displayHeight) 165 // nav bar is on the left side 166 requestedRotation == Rotation.ROTATION_270 -> 167 Region.from(0, 0, navBarWidth, displayHeight) 168 else -> error("Unknown rotation $requestedRotation") 169 } 170 } 171 172 /** Checks if the device uses gestural navigation */ 173 val isGesturalNavigationEnabled: Boolean 174 get() { 175 val resourceId = 176 resources.getIdentifier("config_navBarInteractionMode", "integer", "android") 177 return resources.getInteger(resourceId) == 2 /* NAV_BAR_MODE_GESTURAL */ 178 } 179 getDimensionPixelSizenull180 fun getDimensionPixelSize(resourceName: String): Int { 181 val resourceId = resources.getIdentifier(resourceName, "dimen", "android") 182 return resources.getDimensionPixelSize(resourceId) 183 } 184 185 /** Gets the navigation bar frame height */ getNavigationBarFrameHeightnull186 fun getNavigationBarFrameHeight(rotation: Rotation, isGesturalNavigation: Boolean): Int { 187 return if (rotation.isRotated()) { 188 if (isGesturalNavigation) { 189 getDimensionPixelSize("navigation_bar_frame_height") 190 } else { 191 getDimensionPixelSize("navigation_bar_height_landscape") 192 } 193 } else { 194 getDimensionPixelSize("navigation_bar_frame_height") 195 } 196 } 197 198 private val status_bar_height_default: Int 199 get() { 200 val resourceId = 201 resources.getIdentifier("status_bar_height_default", "dimen", "android") 202 return resources.getDimensionPixelSize(resourceId) 203 } 204 205 val quick_qs_offset_height: Int 206 get() { 207 val resourceId = resources.getIdentifier("quick_qs_offset_height", "dimen", "android") 208 return resources.getDimensionPixelSize(resourceId) 209 } 210 211 /** Split screen divider inset height */ 212 val dockedStackDividerInset: Int 213 get() { 214 val resourceId = 215 resources.getIdentifier("docked_stack_divider_insets", "dimen", "android") 216 return resources.getDimensionPixelSize(resourceId) 217 } 218 } 219