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