diff options
| author | 2024-02-05 11:48:37 +0000 | |
|---|---|---|
| committer | 2024-02-05 11:48:37 +0000 | |
| commit | c80b0a81ff2a8217c089f13b4b6cac49c0659ca2 (patch) | |
| tree | 6f1150e99f85ea60f2bb9f89d054b9cecac765dd | |
| parent | 2771a2d36e6d76ba28f03dda1943f8dce3cf3f96 (diff) | |
| parent | e2fe085b36a6dfececc2a02c649998d5e34d814b (diff) | |
Merge "Take camera protection into account in status bar content insets" into main
14 files changed, 757 insertions, 83 deletions
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt index 84b2c4bfed34..53b262bd29a1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt @@ -21,7 +21,11 @@ import android.content.res.Configuration import android.graphics.Rect import android.view.Display import android.view.DisplayCutout +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.CameraProtectionInfo +import com.android.systemui.SysUICutoutInformation +import com.android.systemui.SysUICutoutProvider import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.statusbar.commandline.CommandRegistry @@ -38,30 +42,30 @@ import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertTrue import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any -import org.mockito.Mock -import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations +@RunWith(AndroidJUnit4::class) @SmallTest class StatusBarContentInsetsProviderTest : SysuiTestCase() { - @Mock private lateinit var dc: DisplayCutout - @Mock private lateinit var contextMock: Context - @Mock private lateinit var display: Display - private lateinit var configurationController: ConfigurationController - + private val sysUICutout = mock<SysUICutoutInformation>() + private val dc = mock<DisplayCutout>() + private val contextMock = mock<Context>() + private val display = mock<Display>() private val configuration = Configuration() + private lateinit var configurationController: ConfigurationController + @Before fun setup() { - MockitoAnnotations.initMocks(this) - `when`(contextMock.display).thenReturn(display) + whenever(sysUICutout.cutout).thenReturn(dc) + whenever(contextMock.display).thenReturn(display) context.ensureTestableResources() - `when`(contextMock.resources).thenReturn(context.resources) - `when`(contextMock.resources.configuration).thenReturn(configuration) - `when`(contextMock.createConfigurationContext(any())).thenAnswer { + whenever(contextMock.resources).thenReturn(context.resources) + whenever(contextMock.resources.configuration).thenReturn(configuration) + whenever(contextMock.createConfigurationContext(any())).thenAnswer { context.createConfigurationContext(it.arguments[0] as Configuration) } configurationController = ConfigurationControllerImpl(contextMock) @@ -117,7 +121,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { bounds = calculateInsetsForRotationWithRotatedResources( currentRotation, targetRotation, - dc, + sysUICutout, screenBounds, sbHeightLandscape, minLeftPadding, @@ -161,7 +165,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { } @Test - fun testCalculateInsetsForRotationWithRotatedResources_topLeftCutout() { + fun testCalculateInsetsForRotationWithRotatedResources_topLeftCutout_noCameraProtection() { // GIVEN a device in portrait mode with width < height and a display cutout in the top-left val screenBounds = Rect(0, 0, 1080, 2160) val dcBounds = Rect(0, 0, 100, 100) @@ -174,7 +178,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { val dotWidth = 10 val statusBarContentHeight = 15 - `when`(dc.boundingRects).thenReturn(listOf(dcBounds)) + whenever(dc.boundingRects).thenReturn(listOf(dcBounds)) // THEN rotations which share a short side should use the greater value between rounded // corner padding and the display cutout's size @@ -187,7 +191,224 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { var bounds = calculateInsetsForRotationWithRotatedResources( currentRotation, targetRotation, - dc, + sysUICutout, + screenBounds, + sbHeightPortrait, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + targetRotation = ROTATION_LANDSCAPE + expectedBounds = Rect(dcBounds.height(), + 0, + screenBounds.height() - minRightPadding, + sbHeightLandscape) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + sysUICutout, + screenBounds, + sbHeightLandscape, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + // THEN the side that does NOT share a short side with the display cutout ignores the + // display cutout bounds + targetRotation = ROTATION_UPSIDE_DOWN + expectedBounds = Rect(minLeftPadding, + 0, + screenBounds.width() - minRightPadding, + sbHeightPortrait) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + sysUICutout, + screenBounds, + sbHeightPortrait, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + // Phone in portrait, seascape (rot_270) bounds + targetRotation = ROTATION_SEASCAPE + expectedBounds = Rect(minLeftPadding, + 0, + screenBounds.height() - dcBounds.height() - dotWidth, + sbHeightLandscape) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + sysUICutout, + screenBounds, + sbHeightLandscape, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + } + + @Test + fun testCalculateInsetsForRotationWithRotatedResources_topLeftCutout_withCameraProtection() { + // GIVEN a device in portrait mode with width < height and a display cutout in the top-left + val screenBounds = Rect(0, 0, 1080, 2160) + val dcBounds = Rect(0, 0, 100, 100) + val protectionBounds = Rect(10, 10, 110, 110) + val minLeftPadding = 20 + val minRightPadding = 20 + val sbHeightPortrait = 100 + val sbHeightLandscape = 60 + val currentRotation = ROTATION_NONE + val isRtl = false + val dotWidth = 10 + val statusBarContentHeight = 15 + + val protectionInfo = mock<CameraProtectionInfo> { + whenever(this.cutoutBounds).thenReturn(protectionBounds) + } + whenever(sysUICutout.cameraProtection).thenReturn(protectionInfo) + whenever(dc.boundingRects).thenReturn(listOf(dcBounds)) + + // THEN rotations which share a short side should use the greater value between rounded + // corner padding, the display cutout's size, and the camera protections' size. + var targetRotation = ROTATION_NONE + var expectedBounds = Rect(protectionBounds.right, + 0, + screenBounds.right - minRightPadding, + sbHeightPortrait) + + var bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + sysUICutout, + screenBounds, + sbHeightPortrait, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + targetRotation = ROTATION_LANDSCAPE + expectedBounds = Rect(protectionBounds.bottom, + 0, + screenBounds.height() - minRightPadding, + sbHeightLandscape) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + sysUICutout, + screenBounds, + sbHeightLandscape, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + // THEN the side that does NOT share a short side with the display cutout ignores the + // display cutout bounds + targetRotation = ROTATION_UPSIDE_DOWN + expectedBounds = Rect(minLeftPadding, + 0, + screenBounds.width() - minRightPadding, + sbHeightPortrait) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + sysUICutout, + screenBounds, + sbHeightPortrait, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + // Phone in portrait, seascape (rot_270) bounds + targetRotation = ROTATION_SEASCAPE + expectedBounds = Rect(minLeftPadding, + 0, + screenBounds.height() - protectionBounds.bottom - dotWidth, + sbHeightLandscape) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + sysUICutout, + screenBounds, + sbHeightLandscape, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + } + + @Test + fun testCalculateInsetsForRotationWithRotatedResources_topRightCutout_noCameraProtection() { + // GIVEN a device in portrait mode with width < height and a display cutout in the top-left + val screenBounds = Rect(0, 0, 1000, 2000) + val dcBounds = Rect(900, 0, 1000, 100) + val minLeftPadding = 20 + val minRightPadding = 20 + val sbHeightPortrait = 100 + val sbHeightLandscape = 60 + val currentRotation = ROTATION_NONE + val isRtl = false + val dotWidth = 10 + val statusBarContentHeight = 15 + + whenever(dc.boundingRects).thenReturn(listOf(dcBounds)) + + // THEN rotations which share a short side should use the greater value between rounded + // corner padding and the display cutout's size + var targetRotation = ROTATION_NONE + var expectedBounds = Rect(minLeftPadding, + 0, + dcBounds.left - dotWidth, + sbHeightPortrait) + + var bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + sysUICutout, screenBounds, sbHeightPortrait, minLeftPadding, @@ -208,7 +429,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { bounds = calculateInsetsForRotationWithRotatedResources( currentRotation, targetRotation, - dc, + sysUICutout, screenBounds, sbHeightLandscape, minLeftPadding, @@ -231,7 +452,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { bounds = calculateInsetsForRotationWithRotatedResources( currentRotation, targetRotation, - dc, + sysUICutout, screenBounds, sbHeightPortrait, minLeftPadding, @@ -253,7 +474,118 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { bounds = calculateInsetsForRotationWithRotatedResources( currentRotation, targetRotation, - dc, + sysUICutout, + screenBounds, + sbHeightLandscape, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + } + + @Test + fun testCalculateInsetsForRotationWithRotatedResources_topRightCutout_withCameraProtection() { + // GIVEN a device in portrait mode with width < height and a display cutout in the top-left + val screenBounds = Rect(0, 0, 1000, 2000) + val dcBounds = Rect(900, 0, 1000, 100) + val protectionBounds = Rect(890, 10, 990, 110) + val minLeftPadding = 20 + val minRightPadding = 20 + val sbHeightPortrait = 100 + val sbHeightLandscape = 60 + val currentRotation = ROTATION_NONE + val isRtl = false + val dotWidth = 10 + val statusBarContentHeight = 15 + + val protectionInfo = mock<CameraProtectionInfo> { + whenever(this.cutoutBounds).thenReturn(protectionBounds) + } + whenever(sysUICutout.cameraProtection).thenReturn(protectionInfo) + whenever(dc.boundingRects).thenReturn(listOf(dcBounds)) + + // THEN rotations which share a short side should use the greater value between rounded + // corner padding, the display cutout's size, and the camera protections' size. + var targetRotation = ROTATION_NONE + var expectedBounds = Rect(minLeftPadding, + 0, + protectionBounds.left - dotWidth, + sbHeightPortrait) + + var bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + sysUICutout, + screenBounds, + sbHeightPortrait, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + targetRotation = ROTATION_LANDSCAPE + expectedBounds = Rect(protectionBounds.bottom, + 0, + screenBounds.height() - minRightPadding, + sbHeightLandscape) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + sysUICutout, + screenBounds, + sbHeightLandscape, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + // THEN the side that does NOT share a short side with the display cutout ignores the + // display cutout bounds + targetRotation = ROTATION_UPSIDE_DOWN + expectedBounds = Rect(minLeftPadding, + 0, + screenBounds.width() - minRightPadding, + sbHeightPortrait) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + sysUICutout, + screenBounds, + sbHeightPortrait, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + // Phone in portrait, seascape (rot_270) bounds + targetRotation = ROTATION_SEASCAPE + expectedBounds = Rect(minLeftPadding, + 0, + screenBounds.height() - protectionBounds.bottom - dotWidth, + sbHeightLandscape) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + sysUICutout, screenBounds, sbHeightLandscape, minLeftPadding, @@ -273,7 +605,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { val bounds = calculateInsetsForRotationWithRotatedResources( currentRotation = ROTATION_NONE, targetRotation = ROTATION_NONE, - displayCutout = dc, + sysUICutout = sysUICutout, maxBounds = Rect(0, 0, 1080, 2160), statusBarHeight = 100, minLeft = 0, @@ -293,7 +625,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { val bounds = calculateInsetsForRotationWithRotatedResources( currentRotation = ROTATION_NONE, targetRotation = ROTATION_NONE, - displayCutout = dc, + sysUICutout = sysUICutout, maxBounds = Rect(0, 0, 1080, 2160), statusBarHeight = 100, minLeft = 0, @@ -321,6 +653,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { val screenBounds = Rect(0, 0, 1080, 2160) // cutout centered at the top val dcBounds = Rect(490, 0, 590, 100) + val protectionBounds = Rect(480, 10, 600, 90) val minLeftPadding = 20 val minRightPadding = 20 val sbHeightPortrait = 100 @@ -330,7 +663,11 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { val dotWidth = 10 val statusBarContentHeight = 15 - `when`(dc.boundingRects).thenReturn(listOf(dcBounds)) + val protectionInfo = mock<CameraProtectionInfo> { + whenever(this.cutoutBounds).thenReturn(protectionBounds) + } + whenever(sysUICutout.cameraProtection).thenReturn(protectionInfo) + whenever(dc.boundingRects).thenReturn(listOf(dcBounds)) // THEN only the landscape/seascape rotations should avoid the cutout area because of the // potential letterboxing @@ -343,7 +680,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { var bounds = calculateInsetsForRotationWithRotatedResources( currentRotation, targetRotation, - dc, + sysUICutout = sysUICutout, screenBounds, sbHeightPortrait, minLeftPadding, @@ -364,7 +701,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { bounds = calculateInsetsForRotationWithRotatedResources( currentRotation, targetRotation, - dc, + sysUICutout = sysUICutout, screenBounds, sbHeightLandscape, minLeftPadding, @@ -385,7 +722,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { bounds = calculateInsetsForRotationWithRotatedResources( currentRotation, targetRotation, - dc, + sysUICutout = sysUICutout, screenBounds, sbHeightPortrait, minLeftPadding, @@ -406,7 +743,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { bounds = calculateInsetsForRotationWithRotatedResources( currentRotation, targetRotation, - dc, + sysUICutout = sysUICutout, screenBounds, sbHeightLandscape, minLeftPadding, @@ -528,7 +865,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { val dotWidth = 10 val statusBarContentHeight = 15 - `when`(dc.boundingRects).thenReturn(listOf(dcBounds)) + whenever(dc.boundingRects).thenReturn(listOf(dcBounds)) // THEN left should be set to the display cutout width, and right should use the minRight val targetRotation = ROTATION_NONE @@ -540,7 +877,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { val bounds = calculateInsetsForRotationWithRotatedResources( currentRotation, targetRotation, - dc, + sysUICutout, screenBounds, sbHeightPortrait, minLeftPadding, @@ -557,7 +894,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { fun testDisplayChanged_returnsUpdatedInsets() { // GIVEN: get insets on the first display and switch to the second display val provider = StatusBarContentInsetsProvider(contextMock, configurationController, - mock<DumpManager>(), mock<CommandRegistry>()) + mock<DumpManager>(), mock<CommandRegistry>(), mock<SysUICutoutProvider>()) configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160)) val firstDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE) @@ -576,7 +913,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { // GIVEN: get insets on the first display, switch to the second display, // get insets and switch back val provider = StatusBarContentInsetsProvider(contextMock, configurationController, - mock<DumpManager>(), mock<CommandRegistry>()) + mock<DumpManager>(), mock<CommandRegistry>(), mock<SysUICutoutProvider>()) configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160)) val firstDisplayInsetsFirstCall = provider @@ -602,7 +939,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { configuration.windowConfiguration.setMaxBounds(0, 0, 100, 100) configurationController.onConfigurationChanged(configuration) val provider = StatusBarContentInsetsProvider(contextMock, configurationController, - mock<DumpManager>(), mock<CommandRegistry>()) + mock<DumpManager>(), mock<CommandRegistry>(), mock<SysUICutoutProvider>()) val listener = object : StatusBarContentInsetsChangedListener { var triggered = false @@ -624,7 +961,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { fun onDensityOrFontScaleChanged_listenerNotified() { configuration.densityDpi = 12 val provider = StatusBarContentInsetsProvider(contextMock, configurationController, - mock<DumpManager>(), mock<CommandRegistry>()) + mock<DumpManager>(), mock<CommandRegistry>(), mock<SysUICutoutProvider>()) val listener = object : StatusBarContentInsetsChangedListener { var triggered = false @@ -645,7 +982,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { @Test fun onThemeChanged_listenerNotified() { val provider = StatusBarContentInsetsProvider(contextMock, configurationController, - mock<DumpManager>(), mock<CommandRegistry>()) + mock<DumpManager>(), mock<CommandRegistry>(), mock<SysUICutoutProvider>()) val listener = object : StatusBarContentInsetsChangedListener { var triggered = false diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 7a83070d1806..65c69f78e9d0 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -542,6 +542,9 @@ <string translatable="false" name="config_protectedCameraId"></string> <!-- Physical ID for the camera of outer display that needs extra protection --> <string translatable="false" name="config_protectedPhysicalCameraId"></string> + <!-- Unique ID of the outer display that contains the camera that needs protection. --> + <string translatable="false" name="config_protectedScreenUniqueId"></string> + <!-- Similar to config_frontBuiltInDisplayCutoutProtection but for inner display. --> <string translatable="false" name="config_innerBuiltInDisplayCutoutProtection"></string> @@ -550,6 +553,8 @@ <string translatable="false" name="config_protectedInnerCameraId"></string> <!-- Physical ID for the camera of inner display that needs extra protection --> <string translatable="false" name="config_protectedInnerPhysicalCameraId"></string> + <!-- Unique ID of the inner display that contains the camera that needs protection. --> + <string translatable="false" name="config_protectedInnerScreenUniqueId"></string> <!-- Comma-separated list of packages to exclude from camera protection e.g. "com.android.systemui,com.android.xyz" --> diff --git a/packages/SystemUI/src/com/android/systemui/CameraProtectionInfo.kt b/packages/SystemUI/src/com/android/systemui/CameraProtectionInfo.kt index bbab4ded3fa2..6314bd9a5615 100644 --- a/packages/SystemUI/src/com/android/systemui/CameraProtectionInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/CameraProtectionInfo.kt @@ -24,4 +24,5 @@ data class CameraProtectionInfo( val physicalCameraId: String?, val cutoutProtectionPath: Path, val cutoutBounds: Rect, + val displayUniqueId: String?, ) diff --git a/packages/SystemUI/src/com/android/systemui/CameraProtectionLoader.kt b/packages/SystemUI/src/com/android/systemui/CameraProtectionLoader.kt index 8fe9389b7b1d..6cee28be78f1 100644 --- a/packages/SystemUI/src/com/android/systemui/CameraProtectionLoader.kt +++ b/packages/SystemUI/src/com/android/systemui/CameraProtectionLoader.kt @@ -25,15 +25,21 @@ import com.android.systemui.res.R import javax.inject.Inject import kotlin.math.roundToInt -class CameraProtectionLoader @Inject constructor(private val context: Context) { +interface CameraProtectionLoader { + fun loadCameraProtectionInfoList(): List<CameraProtectionInfo> +} + +class CameraProtectionLoaderImpl @Inject constructor(private val context: Context) : + CameraProtectionLoader { - fun loadCameraProtectionInfoList(): List<CameraProtectionInfo> { + override fun loadCameraProtectionInfoList(): List<CameraProtectionInfo> { val list = mutableListOf<CameraProtectionInfo>() val front = loadCameraProtectionInfo( R.string.config_protectedCameraId, R.string.config_protectedPhysicalCameraId, - R.string.config_frontBuiltInDisplayCutoutProtection + R.string.config_frontBuiltInDisplayCutoutProtection, + R.string.config_protectedScreenUniqueId, ) if (front != null) { list.add(front) @@ -42,7 +48,8 @@ class CameraProtectionLoader @Inject constructor(private val context: Context) { loadCameraProtectionInfo( R.string.config_protectedInnerCameraId, R.string.config_protectedInnerPhysicalCameraId, - R.string.config_innerBuiltInDisplayCutoutProtection + R.string.config_innerBuiltInDisplayCutoutProtection, + R.string.config_protectedInnerScreenUniqueId, ) if (inner != null) { list.add(inner) @@ -53,7 +60,8 @@ class CameraProtectionLoader @Inject constructor(private val context: Context) { private fun loadCameraProtectionInfo( cameraIdRes: Int, physicalCameraIdRes: Int, - pathRes: Int + pathRes: Int, + displayUniqueIdRes: Int, ): CameraProtectionInfo? { val logicalCameraId = context.getString(cameraIdRes) if (logicalCameraId.isNullOrEmpty()) { @@ -70,11 +78,13 @@ class CameraProtectionLoader @Inject constructor(private val context: Context) { computed.right.roundToInt(), computed.bottom.roundToInt() ) + val displayUniqueId = context.getString(displayUniqueIdRes) return CameraProtectionInfo( logicalCameraId, physicalCameraId, protectionPath, - protectionBounds + protectionBounds, + displayUniqueId ) } diff --git a/packages/SystemUI/src/com/android/systemui/CameraProtectionModule.kt b/packages/SystemUI/src/com/android/systemui/CameraProtectionModule.kt new file mode 100644 index 000000000000..58680a88d670 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/CameraProtectionModule.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui + +import dagger.Binds +import dagger.Module + +@Module +interface CameraProtectionModule { + + @Binds fun cameraProtectionLoaderImpl(impl: CameraProtectionLoaderImpl): CameraProtectionLoader +} diff --git a/packages/SystemUI/src/com/android/systemui/SysUICutoutInformation.kt b/packages/SystemUI/src/com/android/systemui/SysUICutoutInformation.kt new file mode 100644 index 000000000000..fc0b97ea7013 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/SysUICutoutInformation.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui + +import android.view.DisplayCutout + +data class SysUICutoutInformation( + val cutout: DisplayCutout, + val cameraProtection: CameraProtectionInfo? +) diff --git a/packages/SystemUI/src/com/android/systemui/SysUICutoutProvider.kt b/packages/SystemUI/src/com/android/systemui/SysUICutoutProvider.kt new file mode 100644 index 000000000000..aad934124dfb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/SysUICutoutProvider.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui + +import android.content.Context +import android.view.DisplayCutout +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject + +@SysUISingleton +class SysUICutoutProvider +@Inject +constructor( + private val context: Context, + private val cameraProtectionLoader: CameraProtectionLoader, +) { + + private val cameraProtectionList by lazy { + cameraProtectionLoader.loadCameraProtectionInfoList() + } + + fun cutoutInfoForCurrentDisplay(): SysUICutoutInformation? { + val display = context.display + val displayCutout: DisplayCutout = display.cutout ?: return null + val displayUniqueId: String? = display.uniqueId + if (displayUniqueId.isNullOrEmpty()) { + return SysUICutoutInformation(displayCutout, cameraProtection = null) + } + val cameraProtection: CameraProtectionInfo? = + cameraProtectionList.firstOrNull { it.displayUniqueId == displayUniqueId } + return SysUICutoutInformation(displayCutout, cameraProtection) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index efcbd47b67b4..28fd9a994f8a 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -27,6 +27,7 @@ import com.android.keyguard.dagger.ClockRegistryModule; import com.android.keyguard.dagger.KeyguardBouncerComponent; import com.android.systemui.BootCompleteCache; import com.android.systemui.BootCompleteCacheImpl; +import com.android.systemui.CameraProtectionModule; import com.android.systemui.accessibility.AccessibilityModule; import com.android.systemui.accessibility.data.repository.AccessibilityRepositoryModule; import com.android.systemui.appops.dagger.AppOpsModule; @@ -177,6 +178,7 @@ import javax.inject.Named; BouncerInteractorModule.class, BouncerRepositoryModule.class, BouncerViewModule.class, + CameraProtectionModule.class, ClipboardOverlayModule.class, ClockRegistryModule.class, CommunalModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt index 877bd7c11e95..e84b7a077b21 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt @@ -44,6 +44,8 @@ import com.android.systemui.util.leak.RotationUtils.getResourcesForRotation import com.android.app.tracing.traceSection import com.android.systemui.BottomMarginCommand import com.android.systemui.StatusBarInsetsCommand +import com.android.systemui.SysUICutoutInformation +import com.android.systemui.SysUICutoutProvider import com.android.systemui.statusbar.commandline.CommandRegistry import java.io.PrintWriter import java.lang.Math.max @@ -69,6 +71,7 @@ class StatusBarContentInsetsProvider @Inject constructor( val configurationController: ConfigurationController, val dumpManager: DumpManager, val commandRegistry: CommandRegistry, + val sysUICutoutProvider: SysUICutoutProvider, ) : CallbackController<StatusBarContentInsetsChangedListener>, ConfigurationController.ConfigurationListener, Dumpable { @@ -176,7 +179,8 @@ class StatusBarContentInsetsProvider @Inject constructor( */ fun getStatusBarContentInsetsForRotation(@Rotation rotation: Int): Insets = traceSection(tag = "StatusBarContentInsetsProvider.getStatusBarContentInsetsForRotation") { - val displayCutout = checkNotNull(context.display).cutout + val sysUICutout = sysUICutoutProvider.cutoutInfoForCurrentDisplay() + val displayCutout = sysUICutout?.cutout val key = getCacheKey(rotation, displayCutout) val screenBounds = context.resources.configuration.windowConfiguration.maxBounds @@ -187,7 +191,7 @@ class StatusBarContentInsetsProvider @Inject constructor( val width = point.logicalWidth(rotation) val area = insetsCache[key] ?: getAndSetCalculatedAreaForRotation( - rotation, displayCutout, getResourcesForRotation(rotation, context), key) + rotation, sysUICutout, getResourcesForRotation(rotation, context), key) Insets.of(area.left, area.top, /* right= */ width - area.right, /* bottom= */ 0) } @@ -212,10 +216,11 @@ class StatusBarContentInsetsProvider @Inject constructor( fun getStatusBarContentAreaForRotation( @Rotation rotation: Int ): Rect { - val displayCutout = checkNotNull(context.display).cutout + val sysUICutout = sysUICutoutProvider.cutoutInfoForCurrentDisplay() + val displayCutout = sysUICutout?.cutout val key = getCacheKey(rotation, displayCutout) return insetsCache[key] ?: getAndSetCalculatedAreaForRotation( - rotation, displayCutout, getResourcesForRotation(rotation, context), key) + rotation, sysUICutout, getResourcesForRotation(rotation, context), key) } /** @@ -228,18 +233,18 @@ class StatusBarContentInsetsProvider @Inject constructor( private fun getAndSetCalculatedAreaForRotation( @Rotation targetRotation: Int, - displayCutout: DisplayCutout?, + sysUICutout: SysUICutoutInformation?, rotatedResources: Resources, key: CacheKey ): Rect { - return getCalculatedAreaForRotation(displayCutout, targetRotation, rotatedResources) + return getCalculatedAreaForRotation(sysUICutout, targetRotation, rotatedResources) .also { insetsCache.put(key, it) } } private fun getCalculatedAreaForRotation( - displayCutout: DisplayCutout?, + sysUICutout: SysUICutoutInformation?, @Rotation targetRotation: Int, rotatedResources: Resources ): Rect { @@ -271,7 +276,7 @@ class StatusBarContentInsetsProvider @Inject constructor( return calculateInsetsForRotationWithRotatedResources( currentRotation, targetRotation, - displayCutout, + sysUICutout, context.resources.configuration.windowConfiguration.maxBounds, SystemBarUtils.getStatusBarHeightForRotation(context, targetRotation), minLeft, @@ -415,7 +420,7 @@ fun getPrivacyChipBoundingRectForInsets( fun calculateInsetsForRotationWithRotatedResources( @Rotation currentRotation: Int, @Rotation targetRotation: Int, - displayCutout: DisplayCutout?, + sysUICutout: SysUICutoutInformation?, maxBounds: Rect, statusBarHeight: Int, minLeft: Int, @@ -434,7 +439,7 @@ fun calculateInsetsForRotationWithRotatedResources( val rotZeroBounds = getRotationZeroDisplayBounds(maxBounds, currentRotation) return getStatusBarContentBounds( - displayCutout, + sysUICutout, statusBarHeight, rotZeroBounds.right, rotZeroBounds.bottom, @@ -470,7 +475,7 @@ fun calculateInsetsForRotationWithRotatedResources( * rotation */ private fun getStatusBarContentBounds( - displayCutout: DisplayCutout?, + sysUICutout: SysUICutoutInformation?, sbHeight: Int, width: Int, height: Int, @@ -489,19 +494,17 @@ private fun getStatusBarContentBounds( val logicalDisplayWidth = if (targetRotation.isHorizontal()) height else width - val cutoutRects = displayCutout?.boundingRects - if (cutoutRects == null || cutoutRects.isEmpty()) { - return Rect(minLeft, - insetTop, - logicalDisplayWidth - minRight, - sbHeight) + val cutoutRects = sysUICutout?.cutout?.boundingRects + if (cutoutRects.isNullOrEmpty()) { + return Rect(minLeft, insetTop, logicalDisplayWidth - minRight, sbHeight) } - val relativeRotation = if (currentRotation - targetRotation < 0) { - currentRotation - targetRotation + 4 - } else { - currentRotation - targetRotation - } + val relativeRotation = + if (currentRotation - targetRotation < 0) { + currentRotation - targetRotation + 4 + } else { + currentRotation - targetRotation + } // Size of the status bar window for the given rotation relative to our exact rotation val sbRect = sbRect(relativeRotation, sbHeight, Pair(cWidth, cHeight)) @@ -509,19 +512,26 @@ private fun getStatusBarContentBounds( var leftMargin = minLeft var rightMargin = minRight for (cutoutRect in cutoutRects) { + val protectionRect = sysUICutout.cameraProtection?.cutoutBounds + val actualCutoutRect = + if (protectionRect?.intersects(cutoutRect) == true) { + rectUnion(cutoutRect, protectionRect) + } else { + cutoutRect + } // There is at most one non-functional area per short edge of the device. So if the status // bar doesn't share a short edge with the cutout, we can ignore its insets because there // will be no letter-boxing to worry about - if (!shareShortEdge(sbRect, cutoutRect, cWidth, cHeight)) { + if (!shareShortEdge(sbRect, actualCutoutRect, cWidth, cHeight)) { continue } - if (cutoutRect.touchesLeftEdge(relativeRotation, cWidth, cHeight)) { - var logicalWidth = cutoutRect.logicalWidth(relativeRotation) + if (actualCutoutRect.touchesLeftEdge(relativeRotation, cWidth, cHeight)) { + var logicalWidth = actualCutoutRect.logicalWidth(relativeRotation) if (isRtl) logicalWidth += dotWidth leftMargin = max(logicalWidth, leftMargin) - } else if (cutoutRect.touchesRightEdge(relativeRotation, cWidth, cHeight)) { - var logicalWidth = cutoutRect.logicalWidth(relativeRotation) + } else if (actualCutoutRect.touchesRightEdge(relativeRotation, cWidth, cHeight)) { + var logicalWidth = actualCutoutRect.logicalWidth(relativeRotation) if (!isRtl) logicalWidth += dotWidth rightMargin = max(rightMargin, logicalWidth) } @@ -532,6 +542,11 @@ private fun getStatusBarContentBounds( return Rect(leftMargin, insetTop, logicalDisplayWidth - rightMargin, sbHeight) } +private fun rectUnion(first: Rect, second: Rect) = Rect(first).apply { union(second) } + +private fun Rect.intersects(other: Rect): Boolean = + intersects(other.left, other.top, other.right, other.bottom) + /* * Returns the inset top of the status bar. * diff --git a/packages/SystemUI/tests/src/com/android/systemui/CameraAvailabilityListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/CameraAvailabilityListenerTest.kt index 64cd5262eade..f776a63c54ae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/CameraAvailabilityListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/CameraAvailabilityListenerTest.kt @@ -347,8 +347,8 @@ class CameraAvailabilityListenerTest : SysuiTestCase() { return CameraAvailabilityListener.build( context, context.mainExecutor, - CameraProtectionLoader((context)) - ) + CameraProtectionLoaderImpl((context)) + ) .also { it.addTransitionCallback(cameraTransitionCallback) it.startListening() diff --git a/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderImplTest.kt index 238e5e9197a3..a19a0c7d12a3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderImplTest.kt @@ -27,9 +27,9 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) -class CameraProtectionLoaderTest : SysuiTestCase() { +class CameraProtectionLoaderImplTest : SysuiTestCase() { - private val loader = CameraProtectionLoader(context) + private val loader = CameraProtectionLoaderImpl(context) @Before fun setUp() { @@ -39,19 +39,21 @@ class CameraProtectionLoaderTest : SysuiTestCase() { R.string.config_frontBuiltInDisplayCutoutProtection, OUTER_CAMERA_PROTECTION_PATH ) + overrideResource(R.string.config_protectedScreenUniqueId, OUTER_SCREEN_UNIQUE_ID) overrideResource(R.string.config_protectedInnerCameraId, INNER_CAMERA_LOGICAL_ID) overrideResource(R.string.config_protectedInnerPhysicalCameraId, INNER_CAMERA_PHYSICAL_ID) overrideResource( R.string.config_innerBuiltInDisplayCutoutProtection, INNER_CAMERA_PROTECTION_PATH ) + overrideResource(R.string.config_protectedInnerScreenUniqueId, INNER_SCREEN_UNIQUE_ID) } @Test fun loadCameraProtectionInfoList() { - val protectionInfos = loader.loadCameraProtectionInfoList().map { it.toTestableVersion() } + val protectionList = loadProtectionList() - assertThat(protectionInfos) + assertThat(protectionList) .containsExactly(OUTER_CAMERA_PROTECTION_INFO, INNER_CAMERA_PROTECTION_INFO) } @@ -59,18 +61,18 @@ class CameraProtectionLoaderTest : SysuiTestCase() { fun loadCameraProtectionInfoList_outerCameraIdEmpty_onlyReturnsInnerInfo() { overrideResource(R.string.config_protectedCameraId, "") - val protectionInfos = loader.loadCameraProtectionInfoList().map { it.toTestableVersion() } + val protectionList = loadProtectionList() - assertThat(protectionInfos).containsExactly(INNER_CAMERA_PROTECTION_INFO) + assertThat(protectionList).containsExactly(INNER_CAMERA_PROTECTION_INFO) } @Test fun loadCameraProtectionInfoList_innerCameraIdEmpty_onlyReturnsOuterInfo() { overrideResource(R.string.config_protectedInnerCameraId, "") - val protectionInfos = loader.loadCameraProtectionInfoList().map { it.toTestableVersion() } + val protectionList = loadProtectionList() - assertThat(protectionInfos).containsExactly(OUTER_CAMERA_PROTECTION_INFO) + assertThat(protectionList).containsExactly(OUTER_CAMERA_PROTECTION_INFO) } @Test @@ -78,13 +80,16 @@ class CameraProtectionLoaderTest : SysuiTestCase() { overrideResource(R.string.config_protectedCameraId, "") overrideResource(R.string.config_protectedInnerCameraId, "") - val protectionInfos = loader.loadCameraProtectionInfoList().map { it.toTestableVersion() } + val protectionList = loadProtectionList() - assertThat(protectionInfos).isEmpty() + assertThat(protectionList).isEmpty() } + private fun loadProtectionList() = + loader.loadCameraProtectionInfoList().map { it.toTestableVersion() } + private fun CameraProtectionInfo.toTestableVersion() = - TestableProtectionInfo(logicalCameraId, physicalCameraId, cutoutBounds) + TestableProtectionInfo(logicalCameraId, physicalCameraId, cutoutBounds, displayUniqueId) /** * "Testable" version, because the original version contains a Path property, which doesn't @@ -94,6 +99,7 @@ class CameraProtectionLoaderTest : SysuiTestCase() { val logicalCameraId: String, val physicalCameraId: String?, val cutoutBounds: Rect, + val displayUniqueId: String?, ) companion object { @@ -102,11 +108,13 @@ class CameraProtectionLoaderTest : SysuiTestCase() { private const val OUTER_CAMERA_PROTECTION_PATH = "M 0,0 H 10,10 V 10,10 H 0,10 Z" private val OUTER_CAMERA_PROTECTION_BOUNDS = Rect(/* left = */ 0, /* top = */ 0, /* right = */ 10, /* bottom = */ 10) + private const val OUTER_SCREEN_UNIQUE_ID = "111" private val OUTER_CAMERA_PROTECTION_INFO = TestableProtectionInfo( OUTER_CAMERA_LOGICAL_ID, OUTER_CAMERA_PHYSICAL_ID, - OUTER_CAMERA_PROTECTION_BOUNDS + OUTER_CAMERA_PROTECTION_BOUNDS, + OUTER_SCREEN_UNIQUE_ID, ) private const val INNER_CAMERA_LOGICAL_ID = "2" @@ -114,11 +122,13 @@ class CameraProtectionLoaderTest : SysuiTestCase() { private const val INNER_CAMERA_PROTECTION_PATH = "M 0,0 H 20,20 V 20,20 H 0,20 Z" private val INNER_CAMERA_PROTECTION_BOUNDS = Rect(/* left = */ 0, /* top = */ 0, /* right = */ 20, /* bottom = */ 20) + private const val INNER_SCREEN_UNIQUE_ID = "222" private val INNER_CAMERA_PROTECTION_INFO = TestableProtectionInfo( INNER_CAMERA_LOGICAL_ID, INNER_CAMERA_PHYSICAL_ID, - INNER_CAMERA_PROTECTION_BOUNDS + INNER_CAMERA_PROTECTION_BOUNDS, + INNER_SCREEN_UNIQUE_ID, ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/FakeCameraProtectionLoader.kt b/packages/SystemUI/tests/src/com/android/systemui/FakeCameraProtectionLoader.kt new file mode 100644 index 000000000000..f769b4e5f2d0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/FakeCameraProtectionLoader.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui + +import com.android.systemui.res.R + +class FakeCameraProtectionLoader(private val context: SysuiTestableContext) : + CameraProtectionLoader { + + private val realLoader = CameraProtectionLoaderImpl(context) + + override fun loadCameraProtectionInfoList(): List<CameraProtectionInfo> = + realLoader.loadCameraProtectionInfoList() + + fun clearProtectionInfoList() { + context.orCreateTestableResources.addOverride(R.string.config_protectedCameraId, "") + context.orCreateTestableResources.addOverride(R.string.config_protectedInnerCameraId, "") + } + + fun addAllProtections() { + addOuterCameraProtection() + addInnerCameraProtection() + } + + fun addOuterCameraProtection(displayUniqueId: String = "111") { + context.orCreateTestableResources.addOverride(R.string.config_protectedCameraId, "1") + context.orCreateTestableResources.addOverride( + R.string.config_protectedPhysicalCameraId, + "11" + ) + context.orCreateTestableResources.addOverride( + R.string.config_frontBuiltInDisplayCutoutProtection, + "M 0,0 H 10,10 V 10,10 H 0,10 Z" + ) + context.orCreateTestableResources.addOverride( + R.string.config_protectedScreenUniqueId, + displayUniqueId + ) + } + + fun addInnerCameraProtection(displayUniqueId: String = "222") { + context.orCreateTestableResources.addOverride(R.string.config_protectedInnerCameraId, "2") + context.orCreateTestableResources.addOverride( + R.string.config_protectedInnerPhysicalCameraId, + "22" + ) + context.orCreateTestableResources.addOverride( + R.string.config_innerBuiltInDisplayCutoutProtection, + "M 0,0 H 20,20 V 20,20 H 0,20 Z" + ) + context.orCreateTestableResources.addOverride( + R.string.config_protectedInnerScreenUniqueId, + displayUniqueId + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index 1f1fa7259cd5..c20367efa5da 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -177,7 +177,7 @@ public class ScreenDecorationsTest extends SysuiTestCase { new FakeFacePropertyRepository(); private List<DecorProvider> mMockCutoutList; private final CameraProtectionLoader mCameraProtectionLoader = - new CameraProtectionLoader(mContext); + new CameraProtectionLoaderImpl(mContext); @Before public void setup() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/SysUICutoutProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/SysUICutoutProviderTest.kt new file mode 100644 index 000000000000..f37c4ae613ff --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/SysUICutoutProviderTest.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui + +import android.view.Display +import android.view.DisplayAdjustments +import android.view.DisplayCutout +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SysUICutoutProviderTest : SysuiTestCase() { + + private val fakeProtectionLoader = FakeCameraProtectionLoader(context) + + @Test + fun cutoutInfoForCurrentDisplay_noCutout_returnsNull() { + val noCutoutDisplay = createDisplay(cutout = null) + val noCutoutDisplayContext = context.createDisplayContext(noCutoutDisplay) + val provider = SysUICutoutProvider(noCutoutDisplayContext, fakeProtectionLoader) + + val sysUICutout = provider.cutoutInfoForCurrentDisplay() + + assertThat(sysUICutout).isNull() + } + + @Test + fun cutoutInfoForCurrentDisplay_returnsCutout() { + val cutoutDisplay = createDisplay() + val cutoutDisplayContext = context.createDisplayContext(cutoutDisplay) + val provider = SysUICutoutProvider(cutoutDisplayContext, fakeProtectionLoader) + + val sysUICutout = provider.cutoutInfoForCurrentDisplay()!! + + assertThat(sysUICutout.cutout).isEqualTo(cutoutDisplay.cutout) + } + + @Test + fun cutoutInfoForCurrentDisplay_noAssociatedProtection_returnsNoProtection() { + val cutoutDisplay = createDisplay() + val cutoutDisplayContext = context.createDisplayContext(cutoutDisplay) + val provider = SysUICutoutProvider(cutoutDisplayContext, fakeProtectionLoader) + + val sysUICutout = provider.cutoutInfoForCurrentDisplay()!! + + assertThat(sysUICutout.cameraProtection).isNull() + } + + @Test + fun cutoutInfoForCurrentDisplay_outerDisplay_protectionAssociated_returnsProtection() { + fakeProtectionLoader.addOuterCameraProtection(displayUniqueId = OUTER_DISPLAY_UNIQUE_ID) + val outerDisplayContext = context.createDisplayContext(OUTER_DISPLAY) + val provider = SysUICutoutProvider(outerDisplayContext, fakeProtectionLoader) + + val sysUICutout = provider.cutoutInfoForCurrentDisplay()!! + + assertThat(sysUICutout.cameraProtection).isNotNull() + } + + @Test + fun cutoutInfoForCurrentDisplay_outerDisplay_protectionNotAvailable_returnsNullProtection() { + fakeProtectionLoader.clearProtectionInfoList() + val outerDisplayContext = context.createDisplayContext(OUTER_DISPLAY) + val provider = SysUICutoutProvider(outerDisplayContext, fakeProtectionLoader) + + val sysUICutout = provider.cutoutInfoForCurrentDisplay()!! + + assertThat(sysUICutout.cameraProtection).isNull() + } + + @Test + fun cutoutInfoForCurrentDisplay_displayWithNullId_protectionsWithNoId_returnsNullProtection() { + fakeProtectionLoader.addOuterCameraProtection(displayUniqueId = "") + val displayContext = context.createDisplayContext(createDisplay(uniqueId = null)) + val provider = SysUICutoutProvider(displayContext, fakeProtectionLoader) + + val sysUICutout = provider.cutoutInfoForCurrentDisplay()!! + + assertThat(sysUICutout.cameraProtection).isNull() + } + + @Test + fun cutoutInfoForCurrentDisplay_displayWithEmptyId_protectionsWithNoId_returnsNullProtection() { + fakeProtectionLoader.addOuterCameraProtection(displayUniqueId = "") + val displayContext = context.createDisplayContext(createDisplay(uniqueId = "")) + val provider = SysUICutoutProvider(displayContext, fakeProtectionLoader) + + val sysUICutout = provider.cutoutInfoForCurrentDisplay()!! + + assertThat(sysUICutout.cameraProtection).isNull() + } + + companion object { + private const val OUTER_DISPLAY_UNIQUE_ID = "outer" + private val OUTER_DISPLAY = createDisplay(uniqueId = OUTER_DISPLAY_UNIQUE_ID) + + private fun createDisplay( + uniqueId: String? = "uniqueId", + cutout: DisplayCutout? = mock<DisplayCutout>() + ) = + mock<Display> { + whenever(this.displayAdjustments).thenReturn(DisplayAdjustments()) + whenever(this.cutout).thenReturn(cutout) + whenever(this.uniqueId).thenReturn(uniqueId) + } + } +} |