1 /* <lambda>null2 * 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 package com.android.launcher3 17 18 import android.content.Context 19 import android.content.res.Configuration 20 import android.graphics.Point 21 import android.graphics.Rect 22 import android.util.DisplayMetrics 23 import android.view.Surface 24 import androidx.test.core.app.ApplicationProvider 25 import com.android.launcher3.testing.shared.ResourceUtils 26 import com.android.launcher3.util.DisplayController 27 import com.android.launcher3.util.NavigationMode 28 import com.android.launcher3.util.WindowBounds 29 import com.android.launcher3.util.window.CachedDisplayInfo 30 import com.android.launcher3.util.window.WindowManagerProxy 31 import java.io.BufferedReader 32 import java.io.File 33 import java.io.PrintWriter 34 import java.io.StringWriter 35 import kotlin.math.max 36 import kotlin.math.min 37 import org.junit.After 38 import org.junit.Before 39 import org.mockito.ArgumentMatchers 40 import org.mockito.Mockito.mock 41 import org.mockito.Mockito.`when` as whenever 42 43 /** 44 * This is an abstract class for DeviceProfile tests that create an InvariantDeviceProfile based on 45 * a real device spec. 46 * 47 * For an implementation that mocks InvariantDeviceProfile, use [FakeInvariantDeviceProfileTest] 48 */ 49 abstract class AbstractDeviceProfileTest { 50 protected var context: Context? = null 51 protected open val runningContext: Context = ApplicationProvider.getApplicationContext() 52 private var displayController: DisplayController = mock(DisplayController::class.java) 53 private var windowManagerProxy: WindowManagerProxy = mock(WindowManagerProxy::class.java) 54 private lateinit var originalDisplayController: DisplayController 55 private lateinit var originalWindowManagerProxy: WindowManagerProxy 56 57 @Before 58 open fun setUp() { 59 val appContext: Context = ApplicationProvider.getApplicationContext() 60 originalWindowManagerProxy = WindowManagerProxy.INSTANCE.get(appContext) 61 originalDisplayController = DisplayController.INSTANCE.get(appContext) 62 WindowManagerProxy.INSTANCE.initializeForTesting(windowManagerProxy) 63 DisplayController.INSTANCE.initializeForTesting(displayController) 64 } 65 66 @After 67 open fun tearDown() { 68 WindowManagerProxy.INSTANCE.initializeForTesting(originalWindowManagerProxy) 69 DisplayController.INSTANCE.initializeForTesting(originalDisplayController) 70 } 71 72 class DeviceSpec( 73 val naturalSize: Pair<Int, Int>, 74 var densityDpi: Int, 75 val statusBarNaturalPx: Int, 76 val statusBarRotatedPx: Int, 77 val gesturePx: Int, 78 val cutoutPx: Int 79 ) 80 81 open val deviceSpecs = 82 mapOf( 83 "phone" to 84 DeviceSpec( 85 Pair(1080, 2400), 86 densityDpi = 420, 87 statusBarNaturalPx = 118, 88 statusBarRotatedPx = 74, 89 gesturePx = 63, 90 cutoutPx = 118 91 ), 92 "tablet" to 93 DeviceSpec( 94 Pair(2560, 1600), 95 densityDpi = 320, 96 statusBarNaturalPx = 104, 97 statusBarRotatedPx = 104, 98 gesturePx = 0, 99 cutoutPx = 0 100 ), 101 "twopanel-phone" to 102 DeviceSpec( 103 Pair(1080, 2092), 104 densityDpi = 420, 105 statusBarNaturalPx = 133, 106 statusBarRotatedPx = 110, 107 gesturePx = 63, 108 cutoutPx = 133 109 ), 110 "twopanel-tablet" to 111 DeviceSpec( 112 Pair(2208, 1840), 113 densityDpi = 420, 114 statusBarNaturalPx = 110, 115 statusBarRotatedPx = 133, 116 gesturePx = 0, 117 cutoutPx = 0 118 ) 119 ) 120 121 protected fun initializeVarsForPhone( 122 deviceSpec: DeviceSpec, 123 isGestureMode: Boolean = true, 124 isVerticalBar: Boolean = false 125 ) { 126 val (naturalX, naturalY) = deviceSpec.naturalSize 127 val windowsBounds = phoneWindowsBounds(deviceSpec, isGestureMode, naturalX, naturalY) 128 val displayInfo = 129 CachedDisplayInfo(Point(naturalX, naturalY), Surface.ROTATION_0, Rect(0, 0, 0, 0)) 130 val perDisplayBoundsCache = mapOf(displayInfo to windowsBounds) 131 132 initializeCommonVars( 133 perDisplayBoundsCache, 134 displayInfo, 135 rotation = if (isVerticalBar) Surface.ROTATION_90 else Surface.ROTATION_0, 136 isGestureMode, 137 densityDpi = deviceSpec.densityDpi 138 ) 139 } 140 141 protected fun initializeVarsForTablet( 142 deviceSpec: DeviceSpec, 143 isLandscape: Boolean = false, 144 isGestureMode: Boolean = true 145 ) { 146 val (naturalX, naturalY) = deviceSpec.naturalSize 147 val windowsBounds = tabletWindowsBounds(deviceSpec, naturalX, naturalY) 148 val displayInfo = 149 CachedDisplayInfo(Point(naturalX, naturalY), Surface.ROTATION_0, Rect(0, 0, 0, 0)) 150 val perDisplayBoundsCache = mapOf(displayInfo to windowsBounds) 151 152 initializeCommonVars( 153 perDisplayBoundsCache, 154 displayInfo, 155 rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90, 156 isGestureMode, 157 densityDpi = deviceSpec.densityDpi 158 ) 159 } 160 161 protected fun initializeVarsForTwoPanel( 162 deviceSpecUnfolded: DeviceSpec, 163 deviceSpecFolded: DeviceSpec, 164 isLandscape: Boolean = false, 165 isGestureMode: Boolean = true, 166 isFolded: Boolean = false 167 ) { 168 val (unfoldedNaturalX, unfoldedNaturalY) = deviceSpecUnfolded.naturalSize 169 val unfoldedWindowsBounds = 170 tabletWindowsBounds(deviceSpecUnfolded, unfoldedNaturalX, unfoldedNaturalY) 171 val unfoldedDisplayInfo = 172 CachedDisplayInfo( 173 Point(unfoldedNaturalX, unfoldedNaturalY), 174 Surface.ROTATION_0, 175 Rect(0, 0, 0, 0) 176 ) 177 178 val (foldedNaturalX, foldedNaturalY) = deviceSpecFolded.naturalSize 179 val foldedWindowsBounds = 180 phoneWindowsBounds(deviceSpecFolded, isGestureMode, foldedNaturalX, foldedNaturalY) 181 val foldedDisplayInfo = 182 CachedDisplayInfo( 183 Point(foldedNaturalX, foldedNaturalY), 184 Surface.ROTATION_0, 185 Rect(0, 0, 0, 0) 186 ) 187 188 val perDisplayBoundsCache = 189 mapOf( 190 unfoldedDisplayInfo to unfoldedWindowsBounds, 191 foldedDisplayInfo to foldedWindowsBounds 192 ) 193 194 if (isFolded) { 195 initializeCommonVars( 196 perDisplayBoundsCache = perDisplayBoundsCache, 197 displayInfo = foldedDisplayInfo, 198 rotation = if (isLandscape) Surface.ROTATION_90 else Surface.ROTATION_0, 199 isGestureMode = isGestureMode, 200 densityDpi = deviceSpecFolded.densityDpi 201 ) 202 } else { 203 initializeCommonVars( 204 perDisplayBoundsCache = perDisplayBoundsCache, 205 displayInfo = unfoldedDisplayInfo, 206 rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90, 207 isGestureMode = isGestureMode, 208 densityDpi = deviceSpecUnfolded.densityDpi 209 ) 210 } 211 } 212 213 private fun phoneWindowsBounds( 214 deviceSpec: DeviceSpec, 215 isGestureMode: Boolean, 216 naturalX: Int, 217 naturalY: Int 218 ): List<WindowBounds> { 219 val buttonsNavHeight = Utilities.dpToPx(48f, deviceSpec.densityDpi) 220 221 val rotation0Insets = 222 Rect( 223 0, 224 max(deviceSpec.statusBarNaturalPx, deviceSpec.cutoutPx), 225 0, 226 if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight 227 ) 228 val rotation90Insets = 229 Rect( 230 deviceSpec.cutoutPx, 231 deviceSpec.statusBarRotatedPx, 232 if (isGestureMode) 0 else buttonsNavHeight, 233 if (isGestureMode) deviceSpec.gesturePx else 0 234 ) 235 val rotation180Insets = 236 Rect( 237 0, 238 deviceSpec.statusBarNaturalPx, 239 0, 240 max( 241 if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight, 242 deviceSpec.cutoutPx 243 ) 244 ) 245 val rotation270Insets = 246 Rect( 247 if (isGestureMode) 0 else buttonsNavHeight, 248 deviceSpec.statusBarRotatedPx, 249 deviceSpec.cutoutPx, 250 if (isGestureMode) deviceSpec.gesturePx else 0 251 ) 252 253 return listOf( 254 WindowBounds(Rect(0, 0, naturalX, naturalY), rotation0Insets, Surface.ROTATION_0), 255 WindowBounds(Rect(0, 0, naturalY, naturalX), rotation90Insets, Surface.ROTATION_90), 256 WindowBounds(Rect(0, 0, naturalX, naturalY), rotation180Insets, Surface.ROTATION_180), 257 WindowBounds(Rect(0, 0, naturalY, naturalX), rotation270Insets, Surface.ROTATION_270) 258 ) 259 } 260 261 private fun tabletWindowsBounds( 262 deviceSpec: DeviceSpec, 263 naturalX: Int, 264 naturalY: Int 265 ): List<WindowBounds> { 266 val naturalInsets = Rect(0, deviceSpec.statusBarNaturalPx, 0, 0) 267 val rotatedInsets = Rect(0, deviceSpec.statusBarRotatedPx, 0, 0) 268 269 return listOf( 270 WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_0), 271 WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_90), 272 WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_180), 273 WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_270) 274 ) 275 } 276 277 private fun initializeCommonVars( 278 perDisplayBoundsCache: Map<CachedDisplayInfo, List<WindowBounds>>, 279 displayInfo: CachedDisplayInfo, 280 rotation: Int, 281 isGestureMode: Boolean = true, 282 densityDpi: Int 283 ) { 284 val windowsBounds = perDisplayBoundsCache[displayInfo]!! 285 val realBounds = windowsBounds[rotation] 286 whenever(windowManagerProxy.getDisplayInfo(ArgumentMatchers.any())).thenReturn(displayInfo) 287 whenever(windowManagerProxy.getRealBounds(ArgumentMatchers.any(), ArgumentMatchers.any())) 288 .thenReturn(realBounds) 289 whenever(windowManagerProxy.getRotation(ArgumentMatchers.any())).thenReturn(rotation) 290 whenever(windowManagerProxy.getNavigationMode(ArgumentMatchers.any())) 291 .thenReturn( 292 if (isGestureMode) NavigationMode.NO_BUTTON else NavigationMode.THREE_BUTTONS 293 ) 294 295 val density = densityDpi / DisplayMetrics.DENSITY_DEFAULT.toFloat() 296 val config = 297 Configuration(runningContext.resources.configuration).apply { 298 this.densityDpi = densityDpi 299 screenWidthDp = (realBounds.bounds.width() / density).toInt() 300 screenHeightDp = (realBounds.bounds.height() / density).toInt() 301 smallestScreenWidthDp = min(screenWidthDp, screenHeightDp) 302 } 303 context = runningContext.createConfigurationContext(config) 304 305 val info = DisplayController.Info(context, windowManagerProxy, perDisplayBoundsCache) 306 whenever(displayController.info).thenReturn(info) 307 whenever(displayController.isTransientTaskbar).thenReturn(isGestureMode) 308 } 309 310 /** Create a new dump of DeviceProfile, saves to a file in the device and returns it */ 311 protected fun dump(context: Context, dp: DeviceProfile, fileName: String): String { 312 val stringWriter = StringWriter() 313 PrintWriter(stringWriter).use { dp.dump(context, "", it) } 314 return stringWriter.toString().also { content -> writeToDevice(context, fileName, content) } 315 } 316 317 /** Read a file from assets/ and return it as a string */ 318 protected fun readDumpFromAssets(context: Context, fileName: String): String = 319 context.assets.open("dumpTests/$fileName").bufferedReader().use(BufferedReader::readText) 320 321 private fun writeToDevice(context: Context, fileName: String, content: String) { 322 File(context.getDir("dumpTests", Context.MODE_PRIVATE), fileName).writeText(content) 323 } 324 325 protected fun Float.dpToPx(): Float { 326 return ResourceUtils.pxFromDp(this, context!!.resources.displayMetrics).toFloat() 327 } 328 329 protected fun Int.dpToPx(): Int { 330 return ResourceUtils.pxFromDp(this.toFloat(), context!!.resources.displayMetrics) 331 } 332 } 333