1 /*
2  * Copyright (C) 2021 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 
17 package com.android.systemui.statusbar.phone
18 
19 import android.content.Context
20 import android.content.res.Configuration
21 import android.graphics.Rect
22 import android.view.Display
23 import android.view.DisplayCutout
24 import androidx.test.filters.SmallTest
25 import com.android.systemui.SysuiTestCase
26 import com.android.systemui.dump.DumpManager
27 import com.android.systemui.statusbar.policy.ConfigurationController
28 import com.android.systemui.util.leak.RotationUtils
29 import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE
30 import com.android.systemui.util.leak.RotationUtils.ROTATION_NONE
31 import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE
32 import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN
33 import com.android.systemui.util.leak.RotationUtils.Rotation
34 import com.google.common.truth.Truth.assertThat
35 import junit.framework.Assert.assertTrue
36 import org.junit.Before
37 import org.junit.Test
38 import org.mockito.ArgumentMatchers.any
39 import org.mockito.Mock
40 import org.mockito.Mockito.`when`
41 import org.mockito.Mockito.mock
42 import org.mockito.MockitoAnnotations
43 
44 @SmallTest
45 class StatusBarContentInsetsProviderTest : SysuiTestCase() {
46 
47     @Mock private lateinit var dc: DisplayCutout
48     @Mock private lateinit var contextMock: Context
49     @Mock private lateinit var display: Display
50     private lateinit var configurationController: ConfigurationController
51 
52     private val configuration = Configuration()
53 
54     @Before
55     fun setup() {
56         MockitoAnnotations.initMocks(this)
57         `when`(contextMock.display).thenReturn(display)
58 
59         context.ensureTestableResources()
60         `when`(contextMock.resources).thenReturn(context.resources)
61         `when`(contextMock.resources.configuration).thenReturn(configuration)
62         `when`(contextMock.createConfigurationContext(any())).thenAnswer {
63             context.createConfigurationContext(it.arguments[0] as Configuration)
64         }
65 
66         configurationController = ConfigurationControllerImpl(contextMock)
67     }
68 
69     @Test
70     fun testGetBoundingRectForPrivacyChipForRotation_noCutout() {
71         val screenBounds = Rect(0, 0, 1080, 2160)
72         val minLeftPadding = 20
73         val minRightPadding = 20
74         val sbHeightPortrait = 100
75         val sbHeightLandscape = 60
76         val currentRotation = ROTATION_NONE
77         val chipWidth = 30
78         val dotWidth = 10
79 
80         var isRtl = false
81         var targetRotation = ROTATION_NONE
82         var bounds = calculateInsetsForRotationWithRotatedResources(
83                 currentRotation,
84                 targetRotation,
85                 null,
86                 screenBounds,
87                 sbHeightPortrait,
88                 minLeftPadding,
89                 minRightPadding,
90                 isRtl,
91                 dotWidth)
92 
93         var chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl)
94         /* 1080 - 20 (rounded corner) - 30 (chip),
95         *  0 (sb top)
96         *  1080 - 20 (rounded corner) + 10 ( dot),
97         *  100 (sb height portrait)
98         */
99         var expected = Rect(1030, 0, 1070, 100)
100         assertRects(expected, chipBounds, currentRotation, targetRotation)
101         isRtl = true
102         chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl)
103         /* 0 + 20 (rounded corner) - 10 (dot),
104         *  0 (sb top)
105         *  0 + 20 (rounded corner) + 30 (chip),
106         *  100 (sb height portrait)
107         */
108         expected = Rect(10, 0, 50, 100)
109         assertRects(expected, chipBounds, currentRotation, targetRotation)
110 
111         isRtl = false
112         targetRotation = ROTATION_LANDSCAPE
113         bounds = calculateInsetsForRotationWithRotatedResources(
114                 currentRotation,
115                 targetRotation,
116                 dc,
117                 screenBounds,
118                 sbHeightLandscape,
119                 minLeftPadding,
120                 minRightPadding,
121                 isRtl,
122                 dotWidth)
123 
124         chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl)
125         /* 2160 - 20 (rounded corner) - 30 (chip),
126         *  0 (sb top)
127         *  2160 - 20 (rounded corner) + 10 ( dot),
128         *  60 (sb height landscape)
129         */
130         expected = Rect(2110, 0, 2150, 60)
131         assertRects(expected, chipBounds, currentRotation, targetRotation)
132         isRtl = true
133         chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl)
134         /* 0 + 20 (rounded corner) - 10 (dot),
135         *  0 (sb top)
136         *  0 + 20 (rounded corner) + 30 (chip),
137         *  60 (sb height landscape)
138         */
139         expected = Rect(10, 0, 50, 60)
140         assertRects(expected, chipBounds, currentRotation, targetRotation)
141     }
142 
143     @Test
144     fun testCalculateInsetsForRotationWithRotatedResources_topLeftCutout() {
145         // GIVEN a device in portrait mode with width < height and a display cutout in the top-left
146         val screenBounds = Rect(0, 0, 1080, 2160)
147         val dcBounds = Rect(0, 0, 100, 100)
148         val minLeftPadding = 20
149         val minRightPadding = 20
150         val sbHeightPortrait = 100
151         val sbHeightLandscape = 60
152         val currentRotation = ROTATION_NONE
153         val isRtl = false
154         val dotWidth = 10
155 
156         `when`(dc.boundingRects).thenReturn(listOf(dcBounds))
157 
158         // THEN rotations which share a short side should use the greater value between rounded
159         // corner padding and the display cutout's size
160         var targetRotation = ROTATION_NONE
161         var expectedBounds = Rect(dcBounds.right,
162                 0,
163                 screenBounds.right - minRightPadding,
164                 sbHeightPortrait)
165 
166         var bounds = calculateInsetsForRotationWithRotatedResources(
167                 currentRotation,
168                 targetRotation,
169                 dc,
170                 screenBounds,
171                 sbHeightPortrait,
172                 minLeftPadding,
173                 minRightPadding,
174                 isRtl,
175                 dotWidth)
176 
177         assertRects(expectedBounds, bounds, currentRotation, targetRotation)
178 
179         targetRotation = ROTATION_LANDSCAPE
180         expectedBounds = Rect(dcBounds.height(),
181                 0,
182                 screenBounds.height() - minRightPadding,
183                 sbHeightLandscape)
184 
185         bounds = calculateInsetsForRotationWithRotatedResources(
186                 currentRotation,
187                 targetRotation,
188                 dc,
189                 screenBounds,
190                 sbHeightLandscape,
191                 minLeftPadding,
192                 minRightPadding,
193                 isRtl,
194                 dotWidth)
195 
196         assertRects(expectedBounds, bounds, currentRotation, targetRotation)
197 
198         // THEN the side that does NOT share a short side with the display cutout ignores the
199         // display cutout bounds
200         targetRotation = ROTATION_UPSIDE_DOWN
201         expectedBounds = Rect(minLeftPadding,
202                 0,
203                 screenBounds.width() - minRightPadding,
204                 sbHeightPortrait)
205 
206         bounds = calculateInsetsForRotationWithRotatedResources(
207                 currentRotation,
208                 targetRotation,
209                 dc,
210                 screenBounds,
211                 sbHeightPortrait,
212                 minLeftPadding,
213                 minRightPadding,
214                 isRtl,
215                 dotWidth)
216 
217         assertRects(expectedBounds, bounds, currentRotation, targetRotation)
218 
219         // Phone in portrait, seascape (rot_270) bounds
220         targetRotation = ROTATION_SEASCAPE
221         expectedBounds = Rect(minLeftPadding,
222                 0,
223                 screenBounds.height() - dcBounds.height() - dotWidth,
224                 sbHeightLandscape)
225 
226         bounds = calculateInsetsForRotationWithRotatedResources(
227                 currentRotation,
228                 targetRotation,
229                 dc,
230                 screenBounds,
231                 sbHeightLandscape,
232                 minLeftPadding,
233                 minRightPadding,
234                 isRtl,
235                 dotWidth)
236 
237         assertRects(expectedBounds, bounds, currentRotation, targetRotation)
238     }
239 
240     @Test
241     fun testCalculateInsetsForRotationWithRotatedResources_nonCornerCutout() {
242         // GIVEN phone in portrait mode, where width < height and the cutout is not in the corner
243         // the assumption here is that if the cutout does NOT touch the corner then we have room to
244         // layout the status bar in the given space.
245 
246         val screenBounds = Rect(0, 0, 1080, 2160)
247         // cutout centered at the top
248         val dcBounds = Rect(490, 0, 590, 100)
249         val minLeftPadding = 20
250         val minRightPadding = 20
251         val sbHeightPortrait = 100
252         val sbHeightLandscape = 60
253         val currentRotation = ROTATION_NONE
254         val isRtl = false
255         val dotWidth = 10
256 
257         `when`(dc.boundingRects).thenReturn(listOf(dcBounds))
258 
259         // THEN only the landscape/seascape rotations should avoid the cutout area because of the
260         // potential letterboxing
261         var targetRotation = ROTATION_NONE
262         var expectedBounds = Rect(minLeftPadding,
263                 0,
264                 screenBounds.right - minRightPadding,
265                 sbHeightPortrait)
266 
267         var bounds = calculateInsetsForRotationWithRotatedResources(
268                 currentRotation,
269                 targetRotation,
270                 dc,
271                 screenBounds,
272                 sbHeightPortrait,
273                 minLeftPadding,
274                 minRightPadding,
275                 isRtl,
276                 dotWidth)
277 
278         assertRects(expectedBounds, bounds, currentRotation, targetRotation)
279 
280         targetRotation = ROTATION_LANDSCAPE
281         expectedBounds = Rect(dcBounds.height(),
282                 0,
283                 screenBounds.height() - minRightPadding,
284                 sbHeightLandscape)
285 
286         bounds = calculateInsetsForRotationWithRotatedResources(
287                 currentRotation,
288                 targetRotation,
289                 dc,
290                 screenBounds,
291                 sbHeightLandscape,
292                 minLeftPadding,
293                 minRightPadding,
294                 isRtl,
295                 dotWidth)
296 
297         assertRects(expectedBounds, bounds, currentRotation, targetRotation)
298 
299         targetRotation = ROTATION_UPSIDE_DOWN
300         expectedBounds = Rect(minLeftPadding,
301                 0,
302                 screenBounds.right - minRightPadding,
303                 sbHeightPortrait)
304 
305         bounds = calculateInsetsForRotationWithRotatedResources(
306                 currentRotation,
307                 targetRotation,
308                 dc,
309                 screenBounds,
310                 sbHeightPortrait,
311                 minLeftPadding,
312                 minRightPadding,
313                 isRtl,
314                 dotWidth)
315 
316         assertRects(expectedBounds, bounds, currentRotation, targetRotation)
317 
318         targetRotation = ROTATION_SEASCAPE
319         expectedBounds = Rect(minLeftPadding,
320                 0,
321                 screenBounds.height() - dcBounds.height() - dotWidth,
322                 sbHeightLandscape)
323 
324         bounds = calculateInsetsForRotationWithRotatedResources(
325                 currentRotation,
326                 targetRotation,
327                 dc,
328                 screenBounds,
329                 sbHeightLandscape,
330                 minLeftPadding,
331                 minRightPadding,
332                 isRtl,
333                 dotWidth)
334 
335         assertRects(expectedBounds, bounds, currentRotation, targetRotation)
336     }
337 
338     @Test
339     fun testCalculateInsetsForRotationWithRotatedResources_noCutout() {
340         // GIVEN device in portrait mode, where width < height and no cutout
341         val currentRotation = ROTATION_NONE
342         val screenBounds = Rect(0, 0, 1080, 2160)
343         val minLeftPadding = 20
344         val minRightPadding = 20
345         val sbHeightPortrait = 100
346         val sbHeightLandscape = 60
347         val isRtl = false
348         val dotWidth = 10
349 
350         // THEN content insets should only use rounded corner padding
351         var targetRotation = ROTATION_NONE
352         var expectedBounds = Rect(minLeftPadding,
353                 0,
354                 screenBounds.right - minRightPadding,
355                 sbHeightPortrait)
356 
357         var bounds = calculateInsetsForRotationWithRotatedResources(
358                 currentRotation,
359                 targetRotation,
360                 null, /* no cutout */
361                 screenBounds,
362                 sbHeightPortrait,
363                 minLeftPadding,
364                 minRightPadding,
365                 isRtl,
366                 dotWidth)
367         assertRects(expectedBounds, bounds, currentRotation, targetRotation)
368 
369         targetRotation = ROTATION_LANDSCAPE
370         expectedBounds = Rect(minLeftPadding,
371                 0,
372                 screenBounds.height() - minRightPadding,
373                 sbHeightLandscape)
374 
375         bounds = calculateInsetsForRotationWithRotatedResources(
376                 currentRotation,
377                 targetRotation,
378                 null, /* no cutout */
379                 screenBounds,
380                 sbHeightLandscape,
381                 minLeftPadding,
382                 minRightPadding,
383                 isRtl,
384                 dotWidth)
385         assertRects(expectedBounds, bounds, currentRotation, targetRotation)
386 
387         targetRotation = ROTATION_UPSIDE_DOWN
388         expectedBounds = Rect(minLeftPadding,
389                 0,
390                 screenBounds.width() - minRightPadding,
391                 sbHeightPortrait)
392 
393         bounds = calculateInsetsForRotationWithRotatedResources(
394                 currentRotation,
395                 targetRotation,
396                 null, /* no cutout */
397                 screenBounds,
398                 sbHeightPortrait,
399                 minLeftPadding,
400                 minRightPadding,
401                 isRtl,
402                 dotWidth)
403         assertRects(expectedBounds, bounds, currentRotation, targetRotation)
404 
405         targetRotation = ROTATION_LANDSCAPE
406         expectedBounds = Rect(minLeftPadding,
407                 0,
408                 screenBounds.height() - minRightPadding,
409                 sbHeightLandscape)
410 
411         bounds = calculateInsetsForRotationWithRotatedResources(
412                 currentRotation,
413                 targetRotation,
414                 null, /* no cutout */
415                 screenBounds,
416                 sbHeightLandscape,
417                 minLeftPadding,
418                 minRightPadding,
419                 isRtl,
420                 dotWidth)
421         assertRects(expectedBounds, bounds, currentRotation, targetRotation)
422     }
423 
424     @Test
425     fun testMinLeftRight_accountsForDisplayCutout() {
426         // GIVEN a device in portrait mode with width < height and a display cutout in the top-left
427         val screenBounds = Rect(0, 0, 1080, 2160)
428         val dcBounds = Rect(0, 0, 100, 100)
429         val minLeftPadding = 80
430         val minRightPadding = 150
431         val sbHeightPortrait = 100
432         val sbHeightLandscape = 60
433         val currentRotation = ROTATION_NONE
434         val isRtl = false
435         val dotWidth = 10
436 
437         `when`(dc.boundingRects).thenReturn(listOf(dcBounds))
438 
439         // THEN left should be set to the display cutout width, and right should use the minRight
440         var targetRotation = ROTATION_NONE
441         var expectedBounds = Rect(dcBounds.right,
442                 0,
443                 screenBounds.right - minRightPadding,
444                 sbHeightPortrait)
445 
446         var bounds = calculateInsetsForRotationWithRotatedResources(
447                 currentRotation,
448                 targetRotation,
449                 dc,
450                 screenBounds,
451                 sbHeightPortrait,
452                 minLeftPadding,
453                 minRightPadding,
454                 isRtl,
455                 dotWidth)
456 
457         assertRects(expectedBounds, bounds, currentRotation, targetRotation)
458     }
459 
460     @Test
461     fun testDisplayChanged_returnsUpdatedInsets() {
462         // GIVEN: get insets on the first display and switch to the second display
463         val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
464             mock(DumpManager::class.java))
465 
466         configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160))
467         val firstDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE)
468 
469         configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 800, 600))
470 
471         // WHEN: get insets on the second display
472         val secondDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE)
473 
474         // THEN: insets are updated
475         assertThat(firstDisplayInsets).isNotEqualTo(secondDisplayInsets)
476     }
477 
478     @Test
479     fun testDisplayChangedAndReturnedBack_returnsTheSameInsets() {
480         // GIVEN: get insets on the first display, switch to the second display,
481         // get insets and switch back
482         val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
483             mock(DumpManager::class.java))
484 
485         configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160))
486         val firstDisplayInsetsFirstCall = provider
487             .getStatusBarContentAreaForRotation(ROTATION_NONE)
488 
489         configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 800, 600))
490         provider.getStatusBarContentAreaForRotation(ROTATION_NONE)
491 
492         configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160))
493 
494         // WHEN: get insets on the first display again
495         val firstDisplayInsetsSecondCall = provider
496             .getStatusBarContentAreaForRotation(ROTATION_NONE)
497 
498         // THEN: insets for the first and second calls for the first display are the same
499         assertThat(firstDisplayInsetsFirstCall).isEqualTo(firstDisplayInsetsSecondCall)
500     }
501 
502     // Regression test for b/245799099
503     @Test
504     fun onMaxBoundsChanged_listenerNotified() {
505         // Start out with an existing configuration with bounds
506         configuration.windowConfiguration.setMaxBounds(0, 0, 100, 100)
507         configurationController.onConfigurationChanged(configuration)
508         val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
509                 mock(DumpManager::class.java))
510         val listener = object : StatusBarContentInsetsChangedListener {
511             var triggered = false
512 
513             override fun onStatusBarContentInsetsChanged() {
514                 triggered = true
515             }
516         }
517         provider.addCallback(listener)
518 
519         // WHEN the config is updated with new bounds
520         configuration.windowConfiguration.setMaxBounds(0, 0, 456, 789)
521         configurationController.onConfigurationChanged(configuration)
522 
523         // THEN the listener is notified
524         assertThat(listener.triggered).isTrue()
525     }
526 
527     @Test
528     fun onDensityOrFontScaleChanged_listenerNotified() {
529         configuration.densityDpi = 12
530         val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
531                 mock(DumpManager::class.java))
532         val listener = object : StatusBarContentInsetsChangedListener {
533             var triggered = false
534 
535             override fun onStatusBarContentInsetsChanged() {
536                 triggered = true
537             }
538         }
539         provider.addCallback(listener)
540 
541         // WHEN the config is updated
542         configuration.densityDpi = 20
543         configurationController.onConfigurationChanged(configuration)
544 
545         // THEN the listener is notified
546         assertThat(listener.triggered).isTrue()
547     }
548 
549     @Test
550     fun onThemeChanged_listenerNotified() {
551         val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
552                 mock(DumpManager::class.java))
553         val listener = object : StatusBarContentInsetsChangedListener {
554             var triggered = false
555 
556             override fun onStatusBarContentInsetsChanged() {
557                 triggered = true
558             }
559         }
560         provider.addCallback(listener)
561 
562         configurationController.notifyThemeChanged()
563 
564         // THEN the listener is notified
565         assertThat(listener.triggered).isTrue()
566     }
567 
568     private fun assertRects(
569         expected: Rect,
570         actual: Rect,
571         @Rotation currentRotation: Int,
572         @Rotation targetRotation: Int
573     ) {
574         assertTrue(
575                 "Rects must match. currentRotation=${RotationUtils.toString(currentRotation)}" +
576                 " targetRotation=${RotationUtils.toString(targetRotation)}" +
577                 " expected=$expected actual=$actual",
578                 expected.equals(actual))
579     }
580 }
581