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.platform.test.flag.junit.SetFlagsRule 23 import android.platform.test.rule.AllowedDevices 24 import android.platform.test.rule.DeviceProduct 25 import android.platform.test.rule.IgnoreLimit 26 import android.platform.test.rule.LimitDevicesRule 27 import android.util.DisplayMetrics 28 import android.view.Surface 29 import androidx.test.core.app.ApplicationProvider.getApplicationContext 30 import androidx.test.platform.app.InstrumentationRegistry 31 import com.android.launcher3.LauncherPrefs.Companion.GRID_NAME 32 import com.android.launcher3.dagger.LauncherAppComponent 33 import com.android.launcher3.dagger.LauncherAppSingleton 34 import com.android.launcher3.testing.shared.ResourceUtils 35 import com.android.launcher3.util.AllModulesMinusWMProxy 36 import com.android.launcher3.util.DisplayController 37 import com.android.launcher3.util.FakePrefsModule 38 import com.android.launcher3.util.NavigationMode 39 import com.android.launcher3.util.SandboxContext 40 import com.android.launcher3.util.WindowBounds 41 import com.android.launcher3.util.rule.TestStabilityRule 42 import com.android.launcher3.util.rule.setFlags 43 import com.android.launcher3.util.window.CachedDisplayInfo 44 import com.android.launcher3.util.window.WindowManagerProxy 45 import com.google.common.truth.Truth 46 import dagger.BindsInstance 47 import dagger.Component 48 import java.io.BufferedReader 49 import java.io.File 50 import java.io.PrintWriter 51 import java.io.StringWriter 52 import kotlin.math.max 53 import kotlin.math.min 54 import org.junit.Rule 55 import org.mockito.kotlin.any 56 import org.mockito.kotlin.doReturn 57 import org.mockito.kotlin.mock 58 import org.mockito.kotlin.spy 59 import org.mockito.kotlin.whenever 60 61 /** 62 * This is an abstract class for DeviceProfile tests that create an InvariantDeviceProfile based on 63 * a real device spec. 64 * 65 * For an implementation that mocks InvariantDeviceProfile, use [FakeInvariantDeviceProfileTest] 66 */ 67 @AllowedDevices(allowed = [DeviceProduct.CF_PHONE, DeviceProduct.ROBOLECTRIC]) 68 @IgnoreLimit(ignoreLimit = BuildConfig.IS_STUDIO_BUILD) 69 abstract class AbstractDeviceProfileTest { 70 protected val testContext: Context = InstrumentationRegistry.getInstrumentation().context 71 protected lateinit var context: SandboxContext 72 protected open val runningContext: Context = getApplicationContext() 73 private val displayController: DisplayController = mock() 74 private val windowManagerProxy: WindowManagerProxy = mock() 75 private lateinit var launcherPrefs: LauncherPrefs 76 77 @get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT) 78 79 @Rule @JvmField val testStabilityRule = TestStabilityRule() 80 81 @Rule @JvmField val limitDevicesRule = LimitDevicesRule() 82 83 class DeviceSpec( 84 val naturalSize: Pair<Int, Int>, 85 var densityDpi: Int, 86 val statusBarNaturalPx: Int, 87 val statusBarRotatedPx: Int, 88 val gesturePx: Int, 89 val cutoutPx: Int, 90 ) 91 92 open val deviceSpecs = 93 mapOf( 94 "phone" to 95 DeviceSpec( 96 Pair(1080, 2400), 97 densityDpi = 420, 98 statusBarNaturalPx = 118, 99 statusBarRotatedPx = 74, 100 gesturePx = 63, 101 cutoutPx = 118, 102 ), 103 "tablet" to 104 DeviceSpec( 105 Pair(2560, 1600), 106 densityDpi = 320, 107 statusBarNaturalPx = 104, 108 statusBarRotatedPx = 104, 109 gesturePx = 0, 110 cutoutPx = 0, 111 ), 112 "twopanel-phone" to 113 DeviceSpec( 114 Pair(1080, 2092), 115 densityDpi = 420, 116 statusBarNaturalPx = 133, 117 statusBarRotatedPx = 110, 118 gesturePx = 63, 119 cutoutPx = 133, 120 ), 121 "twopanel-tablet" to 122 DeviceSpec( 123 Pair(2208, 1840), 124 densityDpi = 420, 125 statusBarNaturalPx = 110, 126 statusBarRotatedPx = 133, 127 gesturePx = 0, 128 cutoutPx = 0, 129 ), 130 ) 131 132 protected fun initializeVarsForPhone( 133 deviceSpec: DeviceSpec, 134 isGestureMode: Boolean = true, 135 isVerticalBar: Boolean = false, 136 isFixedLandscape: Boolean = false, 137 gridName: String? = GRID_NAME.defaultValue, 138 ) { 139 val (naturalX, naturalY) = deviceSpec.naturalSize 140 val windowsBounds = phoneWindowsBounds(deviceSpec, isGestureMode, naturalX, naturalY) 141 val displayInfo = CachedDisplayInfo(Point(naturalX, naturalY), Surface.ROTATION_0) 142 val perDisplayBoundsCache = mapOf(displayInfo to windowsBounds) 143 144 initializeCommonVars( 145 perDisplayBoundsCache, 146 displayInfo, 147 rotation = if (isVerticalBar) Surface.ROTATION_90 else Surface.ROTATION_0, 148 isGestureMode, 149 densityDpi = deviceSpec.densityDpi, 150 isFixedLandscape = isFixedLandscape, 151 gridName = gridName, 152 ) 153 } 154 155 protected fun initializeVarsForTablet( 156 deviceSpec: DeviceSpec, 157 isLandscape: Boolean = false, 158 isGestureMode: Boolean = true, 159 gridName: String? = GRID_NAME.defaultValue, 160 ) { 161 val (naturalX, naturalY) = deviceSpec.naturalSize 162 val windowsBounds = tabletWindowsBounds(deviceSpec, naturalX, naturalY) 163 val displayInfo = CachedDisplayInfo(Point(naturalX, naturalY), Surface.ROTATION_0) 164 val perDisplayBoundsCache = mapOf(displayInfo to windowsBounds) 165 166 initializeCommonVars( 167 perDisplayBoundsCache, 168 displayInfo, 169 rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90, 170 isGestureMode, 171 densityDpi = deviceSpec.densityDpi, 172 gridName = gridName, 173 ) 174 } 175 176 protected fun initializeVarsForTwoPanel( 177 deviceSpecUnfolded: DeviceSpec, 178 deviceSpecFolded: DeviceSpec, 179 isLandscape: Boolean = false, 180 isGestureMode: Boolean = true, 181 isFolded: Boolean = false, 182 gridName: String? = GRID_NAME.defaultValue, 183 ) { 184 val (unfoldedNaturalX, unfoldedNaturalY) = deviceSpecUnfolded.naturalSize 185 val unfoldedWindowsBounds = 186 tabletWindowsBounds(deviceSpecUnfolded, unfoldedNaturalX, unfoldedNaturalY) 187 val unfoldedDisplayInfo = 188 CachedDisplayInfo(Point(unfoldedNaturalX, unfoldedNaturalY), Surface.ROTATION_0) 189 190 val (foldedNaturalX, foldedNaturalY) = deviceSpecFolded.naturalSize 191 val foldedWindowsBounds = 192 phoneWindowsBounds(deviceSpecFolded, isGestureMode, foldedNaturalX, foldedNaturalY) 193 val foldedDisplayInfo = 194 CachedDisplayInfo(Point(foldedNaturalX, foldedNaturalY), Surface.ROTATION_0) 195 196 val perDisplayBoundsCache = 197 mapOf( 198 unfoldedDisplayInfo to unfoldedWindowsBounds, 199 foldedDisplayInfo to foldedWindowsBounds, 200 ) 201 202 if (isFolded) { 203 initializeCommonVars( 204 perDisplayBoundsCache = perDisplayBoundsCache, 205 displayInfo = foldedDisplayInfo, 206 rotation = if (isLandscape) Surface.ROTATION_90 else Surface.ROTATION_0, 207 isGestureMode = isGestureMode, 208 densityDpi = deviceSpecFolded.densityDpi, 209 gridName = gridName, 210 ) 211 } else { 212 initializeCommonVars( 213 perDisplayBoundsCache = perDisplayBoundsCache, 214 displayInfo = unfoldedDisplayInfo, 215 rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90, 216 isGestureMode = isGestureMode, 217 densityDpi = deviceSpecUnfolded.densityDpi, 218 gridName = gridName, 219 ) 220 } 221 } 222 223 private fun phoneWindowsBounds( 224 deviceSpec: DeviceSpec, 225 isGestureMode: Boolean, 226 naturalX: Int, 227 naturalY: Int, 228 ): List<WindowBounds> { 229 val buttonsNavHeight = Utilities.dpToPx(48f, deviceSpec.densityDpi) 230 231 val rotation0Insets = 232 Rect( 233 0, 234 max(deviceSpec.statusBarNaturalPx, deviceSpec.cutoutPx), 235 0, 236 if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight, 237 ) 238 val rotation90Insets = 239 Rect( 240 deviceSpec.cutoutPx, 241 deviceSpec.statusBarRotatedPx, 242 if (isGestureMode) 0 else buttonsNavHeight, 243 if (isGestureMode) deviceSpec.gesturePx else 0, 244 ) 245 val rotation180Insets = 246 Rect( 247 0, 248 deviceSpec.statusBarNaturalPx, 249 0, 250 max( 251 if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight, 252 deviceSpec.cutoutPx, 253 ), 254 ) 255 val rotation270Insets = 256 Rect( 257 if (isGestureMode) 0 else buttonsNavHeight, 258 deviceSpec.statusBarRotatedPx, 259 deviceSpec.cutoutPx, 260 if (isGestureMode) deviceSpec.gesturePx else 0, 261 ) 262 263 return listOf( 264 WindowBounds(Rect(0, 0, naturalX, naturalY), rotation0Insets, Surface.ROTATION_0), 265 WindowBounds(Rect(0, 0, naturalY, naturalX), rotation90Insets, Surface.ROTATION_90), 266 WindowBounds(Rect(0, 0, naturalX, naturalY), rotation180Insets, Surface.ROTATION_180), 267 WindowBounds(Rect(0, 0, naturalY, naturalX), rotation270Insets, Surface.ROTATION_270), 268 ) 269 } 270 271 private fun tabletWindowsBounds( 272 deviceSpec: DeviceSpec, 273 naturalX: Int, 274 naturalY: Int, 275 ): List<WindowBounds> { 276 val naturalInsets = Rect(0, deviceSpec.statusBarNaturalPx, 0, 0) 277 val rotatedInsets = Rect(0, deviceSpec.statusBarRotatedPx, 0, 0) 278 279 return listOf( 280 WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_0), 281 WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_90), 282 WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_180), 283 WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_270), 284 ) 285 } 286 287 private fun initializeCommonVars( 288 perDisplayBoundsCache: Map<CachedDisplayInfo, List<WindowBounds>>, 289 displayInfo: CachedDisplayInfo, 290 rotation: Int, 291 isGestureMode: Boolean = true, 292 densityDpi: Int, 293 isFixedLandscape: Boolean = false, 294 gridName: String? = GRID_NAME.defaultValue, 295 ) { 296 setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_TWOLINE_TOGGLE) 297 // TODO: re-enable as part of b/396211437 298 setFlagsRule.setFlags(false, Flags.FLAG_ENABLE_LAUNCHER_ICON_SHAPES) 299 val windowsBounds = perDisplayBoundsCache[displayInfo]!! 300 val realBounds = windowsBounds[rotation] 301 whenever(windowManagerProxy.getDisplayInfo(any())).thenReturn(displayInfo) 302 whenever(windowManagerProxy.getRealBounds(any(), any())).thenReturn(realBounds) 303 whenever(windowManagerProxy.getCurrentBounds(any())).thenReturn(realBounds.bounds) 304 whenever(windowManagerProxy.getRotation(any())).thenReturn(rotation) 305 whenever(windowManagerProxy.getNavigationMode(any())) 306 .thenReturn( 307 if (isGestureMode) NavigationMode.NO_BUTTON else NavigationMode.THREE_BUTTONS 308 ) 309 doReturn(WindowManagerProxy.INSTANCE[getApplicationContext()].isTaskbarDrawnInProcess) 310 .whenever(windowManagerProxy) 311 .isTaskbarDrawnInProcess 312 313 val density = densityDpi / DisplayMetrics.DENSITY_DEFAULT.toFloat() 314 val config = 315 Configuration(runningContext.resources.configuration).apply { 316 this.densityDpi = densityDpi 317 screenWidthDp = (realBounds.bounds.width() / density).toInt() 318 screenHeightDp = (realBounds.bounds.height() / density).toInt() 319 smallestScreenWidthDp = min(screenWidthDp, screenHeightDp) 320 } 321 val configurationContext = runningContext.createConfigurationContext(config) 322 context = SandboxContext(configurationContext) 323 context.initDaggerComponent( 324 DaggerAbsDPTestSandboxComponent.builder() 325 .bindWMProxy(windowManagerProxy) 326 .bindDisplayController(displayController) 327 ) 328 launcherPrefs = context.appComponent.launcherPrefs 329 launcherPrefs.put( 330 LauncherPrefs.TASKBAR_PINNING.to(false), 331 LauncherPrefs.TASKBAR_PINNING_IN_DESKTOP_MODE.to(true), 332 LauncherPrefs.FIXED_LANDSCAPE_MODE.to(isFixedLandscape), 333 LauncherPrefs.HOTSEAT_COUNT.to(-1), 334 LauncherPrefs.DEVICE_TYPE.to(-1), 335 LauncherPrefs.WORKSPACE_SIZE.to(""), 336 LauncherPrefs.DB_FILE.to(""), 337 LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.to(true), 338 ) 339 if (gridName != null) { 340 launcherPrefs.put(GRID_NAME, gridName) 341 } 342 343 val info = spy(DisplayController.Info(context, windowManagerProxy, perDisplayBoundsCache)) 344 whenever(displayController.info).thenReturn(info) 345 whenever(info.isTransientTaskbar).thenReturn(isGestureMode) 346 } 347 348 /** Asserts that the given device profile matches a previously dumped device profile state. */ 349 protected fun assertDump(dp: DeviceProfile, folderName: String, filename: String) { 350 val dump = dump(context!!, dp, "${folderName}_$filename.txt") 351 var expected = readDumpFromAssets(testContext, "$folderName/$filename.txt") 352 Truth.assertThat(dump).isEqualTo(expected) 353 } 354 355 /** Create a new dump of DeviceProfile, saves to a file in the device and returns it */ 356 protected fun dump(context: Context, dp: DeviceProfile, fileName: String): String { 357 val stringWriter = StringWriter() 358 PrintWriter(stringWriter).use { dp.dump(context, "", it) } 359 return stringWriter.toString().also { content -> writeToDevice(context, fileName, content) } 360 } 361 362 /** Read a file from assets/ and return it as a string */ 363 protected fun readDumpFromAssets(context: Context, fileName: String): String = 364 context.assets.open("dumpTests/$fileName").bufferedReader().use(BufferedReader::readText) 365 366 private fun writeToDevice(context: Context, fileName: String, content: String) { 367 val file = File(context.getDir("dumpTests", Context.MODE_PRIVATE), fileName) 368 file.writeText(content) 369 } 370 371 protected fun Float.dpToPx(): Float { 372 return ResourceUtils.pxFromDp(this, context!!.resources.displayMetrics).toFloat() 373 } 374 375 protected fun Int.dpToPx(): Int { 376 return ResourceUtils.pxFromDp(this.toFloat(), context!!.resources.displayMetrics) 377 } 378 379 protected fun String.xmlToId(): Int { 380 val context = InstrumentationRegistry.getInstrumentation().context 381 return context.resources.getIdentifier(this, "xml", context.packageName) 382 } 383 } 384 385 @LauncherAppSingleton 386 @Component(modules = [AllModulesMinusWMProxy::class, FakePrefsModule::class]) 387 interface AbsDPTestSandboxComponent : LauncherAppComponent { 388 389 @Component.Builder 390 interface Builder : LauncherAppComponent.Builder { bindWMProxynull391 @BindsInstance fun bindWMProxy(proxy: WindowManagerProxy): Builder 392 393 @BindsInstance fun bindDisplayController(displayController: DisplayController): Builder 394 395 override fun build(): AbsDPTestSandboxComponent 396 } 397 } 398