1 /*
2  * Copyright (C) 2024 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
18 
19 import android.graphics.Rect
20 import android.view.Display
21 import android.view.DisplayAdjustments
22 import android.view.DisplayCutout
23 import android.view.DisplayInfo
24 import android.view.Surface
25 import android.view.Surface.Rotation
26 import androidx.test.ext.junit.runners.AndroidJUnit4
27 import androidx.test.filters.SmallTest
28 import com.android.systemui.util.mockito.any
29 import com.android.systemui.util.mockito.mock
30 import com.android.systemui.util.mockito.whenever
31 import com.google.common.truth.Truth.assertThat
32 import org.junit.Test
33 import org.junit.runner.RunWith
34 
35 @SmallTest
36 @RunWith(AndroidJUnit4::class)
37 class SysUICutoutProviderTest : SysuiTestCase() {
38 
39     private val fakeProtectionLoader = FakeCameraProtectionLoader(context)
40 
41     @Test
cutoutInfoForCurrentDisplay_noCutout_returnsNullnull42     fun cutoutInfoForCurrentDisplay_noCutout_returnsNull() {
43         val noCutoutDisplay = createDisplay(cutout = null)
44         val noCutoutDisplayContext = context.createDisplayContext(noCutoutDisplay)
45         val provider = SysUICutoutProvider(noCutoutDisplayContext, fakeProtectionLoader)
46 
47         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()
48 
49         assertThat(sysUICutout).isNull()
50     }
51 
52     @Test
cutoutInfoForCurrentDisplay_returnsCutoutnull53     fun cutoutInfoForCurrentDisplay_returnsCutout() {
54         val cutoutDisplay = createDisplay()
55         val cutoutDisplayContext = context.createDisplayContext(cutoutDisplay)
56         val provider = SysUICutoutProvider(cutoutDisplayContext, fakeProtectionLoader)
57 
58         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
59 
60         assertThat(sysUICutout.cutout).isEqualTo(cutoutDisplay.cutout)
61     }
62 
63     @Test
cutoutInfoForCurrentDisplay_noAssociatedProtection_returnsNoProtectionnull64     fun cutoutInfoForCurrentDisplay_noAssociatedProtection_returnsNoProtection() {
65         val cutoutDisplay = createDisplay()
66         val cutoutDisplayContext = context.createDisplayContext(cutoutDisplay)
67         val provider = SysUICutoutProvider(cutoutDisplayContext, fakeProtectionLoader)
68 
69         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
70 
71         assertThat(sysUICutout.cameraProtection).isNull()
72     }
73 
74     @Test
cutoutInfoForCurrentDisplay_outerDisplay_protectionAssociated_returnsProtectionnull75     fun cutoutInfoForCurrentDisplay_outerDisplay_protectionAssociated_returnsProtection() {
76         fakeProtectionLoader.addOuterCameraProtection(displayUniqueId = OUTER_DISPLAY_UNIQUE_ID)
77         val outerDisplayContext = context.createDisplayContext(OUTER_DISPLAY)
78         val provider = SysUICutoutProvider(outerDisplayContext, fakeProtectionLoader)
79 
80         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
81 
82         assertThat(sysUICutout.cameraProtection).isNotNull()
83     }
84 
85     @Test
cutoutInfoForCurrentDisplay_outerDisplay_protectionNotAvailable_returnsNullProtectionnull86     fun cutoutInfoForCurrentDisplay_outerDisplay_protectionNotAvailable_returnsNullProtection() {
87         fakeProtectionLoader.clearProtectionInfoList()
88         val outerDisplayContext = context.createDisplayContext(OUTER_DISPLAY)
89         val provider = SysUICutoutProvider(outerDisplayContext, fakeProtectionLoader)
90 
91         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
92 
93         assertThat(sysUICutout.cameraProtection).isNull()
94     }
95 
96     @Test
cutoutInfoForCurrentDisplay_displayWithNullId_protectionsWithNoId_returnsNullProtectionnull97     fun cutoutInfoForCurrentDisplay_displayWithNullId_protectionsWithNoId_returnsNullProtection() {
98         fakeProtectionLoader.addOuterCameraProtection(displayUniqueId = "")
99         val displayContext = context.createDisplayContext(createDisplay(uniqueId = null))
100         val provider = SysUICutoutProvider(displayContext, fakeProtectionLoader)
101 
102         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
103 
104         assertThat(sysUICutout.cameraProtection).isNull()
105     }
106 
107     @Test
cutoutInfoForCurrentDisplay_displayWithEmptyId_protectionsWithNoId_returnsNullProtectionnull108     fun cutoutInfoForCurrentDisplay_displayWithEmptyId_protectionsWithNoId_returnsNullProtection() {
109         fakeProtectionLoader.addOuterCameraProtection(displayUniqueId = "")
110         val displayContext = context.createDisplayContext(createDisplay(uniqueId = ""))
111         val provider = SysUICutoutProvider(displayContext, fakeProtectionLoader)
112 
113         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
114 
115         assertThat(sysUICutout.cameraProtection).isNull()
116     }
117 
118     @Test
cutoutInfo_rotation0_returnsOriginalProtectionBoundsnull119     fun cutoutInfo_rotation0_returnsOriginalProtectionBounds() {
120         val provider =
121             setUpProviderWithCameraProtection(
122                 displayWidth = 500,
123                 displayHeight = 1000,
124                 rotation = Surface.ROTATION_0,
125                 protectionBounds =
126                     Rect(/* left = */ 440, /* top = */ 10, /* right = */ 490, /* bottom = */ 110)
127             )
128 
129         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
130 
131         assertThat(sysUICutout.cameraProtection!!.bounds)
132             .isEqualTo(
133                 Rect(/* left = */ 440, /* top = */ 10, /* right = */ 490, /* bottom = */ 110)
134             )
135     }
136 
137     @Test
cutoutInfo_rotation90_returnsRotatedProtectionBoundsnull138     fun cutoutInfo_rotation90_returnsRotatedProtectionBounds() {
139         val provider =
140             setUpProviderWithCameraProtection(
141                 displayWidth = 500,
142                 displayHeight = 1000,
143                 rotation = Surface.ROTATION_90,
144                 protectionBounds =
145                     Rect(/* left = */ 440, /* top = */ 10, /* right = */ 490, /* bottom = */ 110)
146             )
147 
148         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
149 
150         assertThat(sysUICutout.cameraProtection!!.bounds)
151             .isEqualTo(Rect(/* left = */ 10, /* top = */ 10, /* right = */ 110, /* bottom = */ 60))
152     }
153 
154     @Test
cutoutInfo_withRotation_doesNotMutateOriginalBoundsnull155     fun cutoutInfo_withRotation_doesNotMutateOriginalBounds() {
156         val displayNaturalWidth = 500
157         val displayNaturalHeight = 1000
158         val originalProtectionBounds =
159             Rect(/* left = */ 440, /* top = */ 10, /* right = */ 490, /* bottom = */ 110)
160         // Safe copy as we don't know at which layer the mutation could happen
161         val originalProtectionBoundsCopy = Rect(originalProtectionBounds)
162         val display =
163             createDisplay(
164                 uniqueId = OUTER_DISPLAY_UNIQUE_ID,
165                 rotation = Surface.ROTATION_180,
166                 width = displayNaturalWidth,
167                 height = displayNaturalHeight,
168             )
169         fakeProtectionLoader.addOuterCameraProtection(
170             displayUniqueId = OUTER_DISPLAY_UNIQUE_ID,
171             bounds = originalProtectionBounds
172         )
173         val provider =
174             SysUICutoutProvider(context.createDisplayContext(display), fakeProtectionLoader)
175 
176         // Here we get the rotated bounds once
177         provider.cutoutInfoForCurrentDisplayAndRotation()
178 
179         // Rotate display back to original rotation
180         whenever(display.rotation).thenReturn(Surface.ROTATION_0)
181 
182         // Now the bounds should match the original ones. We are checking for mutation since Rect
183         // is mutable and has many methods that mutate the instance, and it is easy to do it by
184         // mistake.
185         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
186         assertThat(sysUICutout.cameraProtection!!.bounds).isEqualTo(originalProtectionBoundsCopy)
187     }
188 
189     @Test
cutoutInfo_rotation180_returnsRotatedProtectionBoundsnull190     fun cutoutInfo_rotation180_returnsRotatedProtectionBounds() {
191         val provider =
192             setUpProviderWithCameraProtection(
193                 displayWidth = 500,
194                 displayHeight = 1000,
195                 rotation = Surface.ROTATION_180,
196                 protectionBounds =
197                     Rect(/* left = */ 440, /* top = */ 10, /* right = */ 490, /* bottom = */ 110)
198             )
199 
200         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
201 
202         assertThat(sysUICutout.cameraProtection!!.bounds)
203             .isEqualTo(Rect(/* left = */ 10, /* top = */ 890, /* right = */ 60, /* bottom = */ 990))
204     }
205 
206     @Test
cutoutInfo_rotation270_returnsRotatedProtectionBoundsnull207     fun cutoutInfo_rotation270_returnsRotatedProtectionBounds() {
208         val provider =
209             setUpProviderWithCameraProtection(
210                 displayWidth = 500,
211                 displayHeight = 1000,
212                 rotation = Surface.ROTATION_270,
213                 protectionBounds =
214                     Rect(/* left = */ 440, /* top = */ 10, /* right = */ 490, /* bottom = */ 110)
215             )
216 
217         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
218 
219         assertThat(sysUICutout.cameraProtection!!.bounds)
220             .isEqualTo(
221                 Rect(/* left = */ 890, /* top = */ 440, /* right = */ 990, /* bottom = */ 490)
222             )
223     }
224 
setUpProviderWithCameraProtectionnull225     private fun setUpProviderWithCameraProtection(
226         displayWidth: Int,
227         displayHeight: Int,
228         rotation: Int = Surface.ROTATION_0,
229         protectionBounds: Rect,
230     ): SysUICutoutProvider {
231         val display =
232             createDisplay(
233                 uniqueId = OUTER_DISPLAY_UNIQUE_ID,
234                 rotation = rotation,
235                 width =
236                     if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
237                         displayWidth
238                     } else {
239                         displayHeight
240                     },
241                 height =
242                     if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180)
243                         displayHeight
244                     else displayWidth,
245             )
246         fakeProtectionLoader.addOuterCameraProtection(
247             displayUniqueId = OUTER_DISPLAY_UNIQUE_ID,
248             bounds = protectionBounds
249         )
250         return SysUICutoutProvider(context.createDisplayContext(display), fakeProtectionLoader)
251     }
252 
253     companion object {
254         private const val OUTER_DISPLAY_UNIQUE_ID = "outer"
255         private val OUTER_DISPLAY = createDisplay(uniqueId = OUTER_DISPLAY_UNIQUE_ID)
256 
createDisplaynull257         private fun createDisplay(
258             width: Int = 500,
259             height: Int = 1000,
260             @Rotation rotation: Int = Surface.ROTATION_0,
261             uniqueId: String? = "uniqueId",
262             cutout: DisplayCutout? = mock<DisplayCutout>()
263         ) =
264             mock<Display> {
265                 whenever(this.getDisplayInfo(any())).thenAnswer {
266                     val displayInfo = it.arguments[0] as DisplayInfo
267                     displayInfo.rotation = rotation
268                     displayInfo.logicalWidth = width
269                     displayInfo.logicalHeight = height
270                     return@thenAnswer true
271                 }
272                 // Setting width and height to smaller values to simulate real behavior of this API
273                 // not always returning the real display size
274                 whenever(this.width).thenReturn(width - 5)
275                 whenever(this.height).thenReturn(height - 10)
276                 whenever(this.rotation).thenReturn(rotation)
277                 whenever(this.displayAdjustments).thenReturn(DisplayAdjustments())
278                 whenever(this.cutout).thenReturn(cutout)
279                 whenever(this.uniqueId).thenReturn(uniqueId)
280             }
281     }
282 }
283