• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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