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