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