• 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.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