From e2fe085b36a6dfececc2a02c649998d5e34d814b Mon Sep 17 00:00:00 2001 From: Chris Göllner Date: Tue, 30 Jan 2024 12:01:51 +0000 Subject: Take camera protection into account in status bar content insets + Introduces SysUICutoutInformation, which bundles display cutout and camera protection info. To be able to associate a camera protection with a specific display cutout, we have to add the unique display id that is associated with the camera protection info, and then match it with the display of the cutout. Change-Id: Ic5336d17dfafae981ca5f12231941fa3bf77b006 Test: StatusBarContentInsetsProviderTest.kt Flag: NONE Bug: 321955113 --- .../phone/StatusBarContentInsetsProviderTest.kt | 1017 ++++++++++++++++++++ packages/SystemUI/res/values/config.xml | 5 + .../com/android/systemui/CameraProtectionInfo.kt | 1 + .../com/android/systemui/CameraProtectionLoader.kt | 22 +- .../com/android/systemui/CameraProtectionModule.kt | 26 + .../com/android/systemui/SysUICutoutInformation.kt | 24 + .../com/android/systemui/SysUICutoutProvider.kt | 47 + .../android/systemui/dagger/SystemUIModule.java | 2 + .../phone/StatusBarContentInsetsProvider.kt | 69 +- .../systemui/CameraAvailabilityListenerTest.kt | 4 +- .../systemui/CameraProtectionLoaderImplTest.kt | 134 +++ .../android/systemui/CameraProtectionLoaderTest.kt | 124 --- .../android/systemui/FakeCameraProtectionLoader.kt | 70 ++ .../android/systemui/ScreenDecorationsTest.java | 2 +- .../android/systemui/SysUICutoutProviderTest.kt | 127 +++ .../phone/StatusBarContentInsetsProviderTest.kt | 680 ------------- 16 files changed, 1514 insertions(+), 840 deletions(-) create mode 100644 packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt create mode 100644 packages/SystemUI/src/com/android/systemui/CameraProtectionModule.kt create mode 100644 packages/SystemUI/src/com/android/systemui/SysUICutoutInformation.kt create mode 100644 packages/SystemUI/src/com/android/systemui/SysUICutoutProvider.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderImplTest.kt delete mode 100644 packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/FakeCameraProtectionLoader.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/SysUICutoutProviderTest.kt delete mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt new file mode 100644 index 000000000000..53b262bd29a1 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt @@ -0,0 +1,1017 @@ +/* + * Copyright (C) 2021 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.statusbar.phone + +import android.content.Context +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 +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.leak.RotationUtils +import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE +import com.android.systemui.util.leak.RotationUtils.ROTATION_NONE +import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE +import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN +import com.android.systemui.util.leak.RotationUtils.Rotation +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +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 + +@RunWith(AndroidJUnit4::class) +@SmallTest +class StatusBarContentInsetsProviderTest : SysuiTestCase() { + + private val sysUICutout = mock() + private val dc = mock() + private val contextMock = mock() + private val display = mock() + private val configuration = Configuration() + + private lateinit var configurationController: ConfigurationController + + @Before + fun setup() { + whenever(sysUICutout.cutout).thenReturn(dc) + whenever(contextMock.display).thenReturn(display) + + context.ensureTestableResources() + 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) + } + + @Test + fun testGetBoundingRectForPrivacyChipForRotation_noCutout() { + val screenBounds = Rect(0, 0, 1080, 2160) + val minLeftPadding = 20 + val minRightPadding = 20 + val sbHeightPortrait = 100 + val sbHeightLandscape = 60 + val currentRotation = ROTATION_NONE + val chipWidth = 30 + val dotWidth = 10 + val statusBarContentHeight = 15 + + var isRtl = false + var targetRotation = ROTATION_NONE + var bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + null, + screenBounds, + sbHeightPortrait, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + var chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl) + /* 1080 - 20 (rounded corner) - 30 (chip), + * 0 (sb top) + * 1080 - 20 (rounded corner) + 10 ( dot), + * 100 (sb height portrait) + */ + var expected = Rect(1030, 0, 1070, 100) + assertRects(expected, chipBounds, currentRotation, targetRotation) + isRtl = true + chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl) + /* 0 + 20 (rounded corner) - 10 (dot), + * 0 (sb top) + * 0 + 20 (rounded corner) + 30 (chip), + * 100 (sb height portrait) + */ + expected = Rect(10, 0, 50, 100) + assertRects(expected, chipBounds, currentRotation, targetRotation) + + isRtl = false + targetRotation = ROTATION_LANDSCAPE + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + sysUICutout, + screenBounds, + sbHeightLandscape, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl) + /* 2160 - 20 (rounded corner) - 30 (chip), + * 0 (sb top) + * 2160 - 20 (rounded corner) + 10 ( dot), + * 60 (sb height landscape) + */ + expected = Rect(2110, 0, 2150, 60) + assertRects(expected, chipBounds, currentRotation, targetRotation) + isRtl = true + chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl) + /* 0 + 20 (rounded corner) - 10 (dot), + * 0 (sb top) + * 0 + 20 (rounded corner) + 30 (chip), + * 60 (sb height landscape) + */ + expected = Rect(10, 0, 50, 60) + assertRects(expected, chipBounds, currentRotation, targetRotation) + } + + @Test + fun privacyChipBoundingRectForInsets_usesTopInset() { + val chipWidth = 30 + val dotWidth = 10 + val isRtl = false + val contentRect = + Rect(/* left = */ 0, /* top = */ 10, /* right = */ 1000, /* bottom = */ 100) + + val chipBounds = + getPrivacyChipBoundingRectForInsets(contentRect, dotWidth, chipWidth, isRtl) + + assertThat(chipBounds.top).isEqualTo(contentRect.top) + } + + @Test + 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) + 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(dcBounds.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(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 { + 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, + 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_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 { + 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, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + } + + @Test + fun calculateInsetsForRotationWithRotatedResources_bottomAlignedMarginDisabled_noTopInset() { + whenever(dc.boundingRects).thenReturn(emptyList()) + + val bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation = ROTATION_NONE, + targetRotation = ROTATION_NONE, + sysUICutout = sysUICutout, + maxBounds = Rect(0, 0, 1080, 2160), + statusBarHeight = 100, + minLeft = 0, + minRight = 0, + isRtl = false, + dotWidth = 10, + bottomAlignedMargin = BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight = 15) + + assertThat(bounds.top).isEqualTo(0) + } + + @Test + fun calculateInsetsForRotationWithRotatedResources_bottomAlignedMargin_topBasedOnMargin() { + whenever(dc.boundingRects).thenReturn(emptyList()) + + val bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation = ROTATION_NONE, + targetRotation = ROTATION_NONE, + sysUICutout = sysUICutout, + maxBounds = Rect(0, 0, 1080, 2160), + statusBarHeight = 100, + minLeft = 0, + minRight = 0, + isRtl = false, + dotWidth = 10, + bottomAlignedMargin = 5, + statusBarContentHeight = 15) + + // Content in the status bar is centered vertically. To achieve the bottom margin we want, + // we need to "shrink" the height of the status bar until the centered content has the + // desired bottom margin. To achieve this shrinking, we use top inset/padding. + // "New" SB height = bottom margin * 2 + content height + // Top inset = SB height - "New" SB height + val expectedTopInset = 75 + assertThat(bounds.top).isEqualTo(expectedTopInset) + } + + @Test + fun testCalculateInsetsForRotationWithRotatedResources_nonCornerCutout() { + // GIVEN phone in portrait mode, where width < height and the cutout is not in the corner + // the assumption here is that if the cutout does NOT touch the corner then we have room to + // layout the status bar in the given space. + + 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 + val sbHeightLandscape = 60 + val currentRotation = ROTATION_NONE + val isRtl = false + val dotWidth = 10 + val statusBarContentHeight = 15 + + val protectionInfo = mock { + 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 + var targetRotation = ROTATION_NONE + var expectedBounds = Rect(minLeftPadding, + 0, + screenBounds.right - minRightPadding, + sbHeightPortrait) + + var bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + sysUICutout = 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 = sysUICutout, + screenBounds, + sbHeightLandscape, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + targetRotation = ROTATION_UPSIDE_DOWN + expectedBounds = Rect(minLeftPadding, + 0, + screenBounds.right - minRightPadding, + sbHeightPortrait) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + sysUICutout = sysUICutout, + screenBounds, + sbHeightPortrait, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + targetRotation = ROTATION_SEASCAPE + expectedBounds = Rect(minLeftPadding, + 0, + screenBounds.height() - dcBounds.height() - dotWidth, + sbHeightLandscape) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + sysUICutout = sysUICutout, + screenBounds, + sbHeightLandscape, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + } + + @Test + fun testCalculateInsetsForRotationWithRotatedResources_noCutout() { + // GIVEN device in portrait mode, where width < height and no cutout + val currentRotation = ROTATION_NONE + val screenBounds = Rect(0, 0, 1080, 2160) + val minLeftPadding = 20 + val minRightPadding = 20 + val sbHeightPortrait = 100 + val sbHeightLandscape = 60 + val isRtl = false + val dotWidth = 10 + val statusBarContentHeight = 15 + + // THEN content insets should only use rounded corner padding + var targetRotation = ROTATION_NONE + var expectedBounds = Rect(minLeftPadding, + 0, + screenBounds.right - minRightPadding, + sbHeightPortrait) + + var bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + null, /* no cutout */ + screenBounds, + sbHeightPortrait, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + targetRotation = ROTATION_LANDSCAPE + expectedBounds = Rect(minLeftPadding, + 0, + screenBounds.height() - minRightPadding, + sbHeightLandscape) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + null, /* no cutout */ + screenBounds, + sbHeightLandscape, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + targetRotation = ROTATION_UPSIDE_DOWN + expectedBounds = Rect(minLeftPadding, + 0, + screenBounds.width() - minRightPadding, + sbHeightPortrait) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + null, /* no cutout */ + screenBounds, + sbHeightPortrait, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + targetRotation = ROTATION_LANDSCAPE + expectedBounds = Rect(minLeftPadding, + 0, + screenBounds.height() - minRightPadding, + sbHeightLandscape) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + null, /* no cutout */ + screenBounds, + sbHeightLandscape, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + } + + @Test + fun testMinLeftRight_accountsForDisplayCutout() { + // 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 minLeftPadding = 80 + val minRightPadding = 150 + 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 left should be set to the display cutout width, and right should use the minRight + val targetRotation = ROTATION_NONE + val expectedBounds = Rect(dcBounds.right, + 0, + screenBounds.right - minRightPadding, + sbHeightPortrait) + + val bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + sysUICutout, + screenBounds, + sbHeightPortrait, + minLeftPadding, + minRightPadding, + isRtl, + dotWidth, + BOTTOM_ALIGNED_MARGIN_NONE, + statusBarContentHeight) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + } + + @Test + fun testDisplayChanged_returnsUpdatedInsets() { + // GIVEN: get insets on the first display and switch to the second display + val provider = StatusBarContentInsetsProvider(contextMock, configurationController, + mock(), mock(), mock()) + + configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160)) + val firstDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE) + + configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 800, 600)) + + // WHEN: get insets on the second display + val secondDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE) + + // THEN: insets are updated + assertThat(firstDisplayInsets).isNotEqualTo(secondDisplayInsets) + } + + @Test + fun testDisplayChangedAndReturnedBack_returnsTheSameInsets() { + // GIVEN: get insets on the first display, switch to the second display, + // get insets and switch back + val provider = StatusBarContentInsetsProvider(contextMock, configurationController, + mock(), mock(), mock()) + + configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160)) + val firstDisplayInsetsFirstCall = provider + .getStatusBarContentAreaForRotation(ROTATION_NONE) + + configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 800, 600)) + provider.getStatusBarContentAreaForRotation(ROTATION_NONE) + + configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160)) + + // WHEN: get insets on the first display again + val firstDisplayInsetsSecondCall = provider + .getStatusBarContentAreaForRotation(ROTATION_NONE) + + // THEN: insets for the first and second calls for the first display are the same + assertThat(firstDisplayInsetsFirstCall).isEqualTo(firstDisplayInsetsSecondCall) + } + + // Regression test for b/245799099 + @Test + fun onMaxBoundsChanged_listenerNotified() { + // Start out with an existing configuration with bounds + configuration.windowConfiguration.setMaxBounds(0, 0, 100, 100) + configurationController.onConfigurationChanged(configuration) + val provider = StatusBarContentInsetsProvider(contextMock, configurationController, + mock(), mock(), mock()) + val listener = object : StatusBarContentInsetsChangedListener { + var triggered = false + + override fun onStatusBarContentInsetsChanged() { + triggered = true + } + } + provider.addCallback(listener) + + // WHEN the config is updated with new bounds + configuration.windowConfiguration.setMaxBounds(0, 0, 456, 789) + configurationController.onConfigurationChanged(configuration) + + // THEN the listener is notified + assertThat(listener.triggered).isTrue() + } + + @Test + fun onDensityOrFontScaleChanged_listenerNotified() { + configuration.densityDpi = 12 + val provider = StatusBarContentInsetsProvider(contextMock, configurationController, + mock(), mock(), mock()) + val listener = object : StatusBarContentInsetsChangedListener { + var triggered = false + + override fun onStatusBarContentInsetsChanged() { + triggered = true + } + } + provider.addCallback(listener) + + // WHEN the config is updated + configuration.densityDpi = 20 + configurationController.onConfigurationChanged(configuration) + + // THEN the listener is notified + assertThat(listener.triggered).isTrue() + } + + @Test + fun onThemeChanged_listenerNotified() { + val provider = StatusBarContentInsetsProvider(contextMock, configurationController, + mock(), mock(), mock()) + val listener = object : StatusBarContentInsetsChangedListener { + var triggered = false + + override fun onStatusBarContentInsetsChanged() { + triggered = true + } + } + provider.addCallback(listener) + + configurationController.notifyThemeChanged() + + // THEN the listener is notified + assertThat(listener.triggered).isTrue() + } + + private fun assertRects( + expected: Rect, + actual: Rect, + @Rotation currentRotation: Int, + @Rotation targetRotation: Int + ) { + assertTrue( + "Rects must match. currentRotation=${RotationUtils.toString(currentRotation)}" + + " targetRotation=${RotationUtils.toString(targetRotation)}" + + " expected=$expected actual=$actual", + expected.equals(actual)) + } + + companion object { + private const val BOTTOM_ALIGNED_MARGIN_NONE = -1 + } +} 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 @@ + + + @@ -550,6 +553,8 @@ + + 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 +} + +class CameraProtectionLoaderImpl @Inject constructor(private val context: Context) : + CameraProtectionLoader { - fun loadCameraProtectionInfoList(): List { + override fun loadCameraProtectionInfoList(): List { val list = mutableListOf() 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, 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/CameraProtectionLoaderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderImplTest.kt new file mode 100644 index 000000000000..a19a0c7d12a3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderImplTest.kt @@ -0,0 +1,134 @@ +/* + * 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.graphics.Rect +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.res.R +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CameraProtectionLoaderImplTest : SysuiTestCase() { + + private val loader = CameraProtectionLoaderImpl(context) + + @Before + fun setUp() { + overrideResource(R.string.config_protectedCameraId, OUTER_CAMERA_LOGICAL_ID) + overrideResource(R.string.config_protectedPhysicalCameraId, OUTER_CAMERA_PHYSICAL_ID) + overrideResource( + 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 protectionList = loadProtectionList() + + assertThat(protectionList) + .containsExactly(OUTER_CAMERA_PROTECTION_INFO, INNER_CAMERA_PROTECTION_INFO) + } + + @Test + fun loadCameraProtectionInfoList_outerCameraIdEmpty_onlyReturnsInnerInfo() { + overrideResource(R.string.config_protectedCameraId, "") + + val protectionList = loadProtectionList() + + assertThat(protectionList).containsExactly(INNER_CAMERA_PROTECTION_INFO) + } + + @Test + fun loadCameraProtectionInfoList_innerCameraIdEmpty_onlyReturnsOuterInfo() { + overrideResource(R.string.config_protectedInnerCameraId, "") + + val protectionList = loadProtectionList() + + assertThat(protectionList).containsExactly(OUTER_CAMERA_PROTECTION_INFO) + } + + @Test + fun loadCameraProtectionInfoList_innerAndOuterCameraIdsEmpty_returnsEmpty() { + overrideResource(R.string.config_protectedCameraId, "") + overrideResource(R.string.config_protectedInnerCameraId, "") + + val protectionList = loadProtectionList() + + assertThat(protectionList).isEmpty() + } + + private fun loadProtectionList() = + loader.loadCameraProtectionInfoList().map { it.toTestableVersion() } + + private fun CameraProtectionInfo.toTestableVersion() = + TestableProtectionInfo(logicalCameraId, physicalCameraId, cutoutBounds, displayUniqueId) + + /** + * "Testable" version, because the original version contains a Path property, which doesn't + * implement equals. + */ + private data class TestableProtectionInfo( + val logicalCameraId: String, + val physicalCameraId: String?, + val cutoutBounds: Rect, + val displayUniqueId: String?, + ) + + companion object { + private const val OUTER_CAMERA_LOGICAL_ID = "1" + private const val OUTER_CAMERA_PHYSICAL_ID = "11" + 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_SCREEN_UNIQUE_ID, + ) + + private const val INNER_CAMERA_LOGICAL_ID = "2" + private const val INNER_CAMERA_PHYSICAL_ID = "22" + 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_SCREEN_UNIQUE_ID, + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderTest.kt deleted file mode 100644 index 238e5e9197a3..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderTest.kt +++ /dev/null @@ -1,124 +0,0 @@ -/* - * 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.graphics.Rect -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.res.R -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -@SmallTest -@RunWith(AndroidJUnit4::class) -class CameraProtectionLoaderTest : SysuiTestCase() { - - private val loader = CameraProtectionLoader(context) - - @Before - fun setUp() { - overrideResource(R.string.config_protectedCameraId, OUTER_CAMERA_LOGICAL_ID) - overrideResource(R.string.config_protectedPhysicalCameraId, OUTER_CAMERA_PHYSICAL_ID) - overrideResource( - R.string.config_frontBuiltInDisplayCutoutProtection, - OUTER_CAMERA_PROTECTION_PATH - ) - 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 - ) - } - - @Test - fun loadCameraProtectionInfoList() { - val protectionInfos = loader.loadCameraProtectionInfoList().map { it.toTestableVersion() } - - assertThat(protectionInfos) - .containsExactly(OUTER_CAMERA_PROTECTION_INFO, INNER_CAMERA_PROTECTION_INFO) - } - - @Test - fun loadCameraProtectionInfoList_outerCameraIdEmpty_onlyReturnsInnerInfo() { - overrideResource(R.string.config_protectedCameraId, "") - - val protectionInfos = loader.loadCameraProtectionInfoList().map { it.toTestableVersion() } - - assertThat(protectionInfos).containsExactly(INNER_CAMERA_PROTECTION_INFO) - } - - @Test - fun loadCameraProtectionInfoList_innerCameraIdEmpty_onlyReturnsOuterInfo() { - overrideResource(R.string.config_protectedInnerCameraId, "") - - val protectionInfos = loader.loadCameraProtectionInfoList().map { it.toTestableVersion() } - - assertThat(protectionInfos).containsExactly(OUTER_CAMERA_PROTECTION_INFO) - } - - @Test - fun loadCameraProtectionInfoList_innerAndOuterCameraIdsEmpty_returnsEmpty() { - overrideResource(R.string.config_protectedCameraId, "") - overrideResource(R.string.config_protectedInnerCameraId, "") - - val protectionInfos = loader.loadCameraProtectionInfoList().map { it.toTestableVersion() } - - assertThat(protectionInfos).isEmpty() - } - - private fun CameraProtectionInfo.toTestableVersion() = - TestableProtectionInfo(logicalCameraId, physicalCameraId, cutoutBounds) - - /** - * "Testable" version, because the original version contains a Path property, which doesn't - * implement equals. - */ - private data class TestableProtectionInfo( - val logicalCameraId: String, - val physicalCameraId: String?, - val cutoutBounds: Rect, - ) - - companion object { - private const val OUTER_CAMERA_LOGICAL_ID = "1" - private const val OUTER_CAMERA_PHYSICAL_ID = "11" - 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 val OUTER_CAMERA_PROTECTION_INFO = - TestableProtectionInfo( - OUTER_CAMERA_LOGICAL_ID, - OUTER_CAMERA_PHYSICAL_ID, - OUTER_CAMERA_PROTECTION_BOUNDS - ) - - private const val INNER_CAMERA_LOGICAL_ID = "2" - private const val INNER_CAMERA_PHYSICAL_ID = "22" - 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 val INNER_CAMERA_PROTECTION_INFO = - TestableProtectionInfo( - INNER_CAMERA_LOGICAL_ID, - INNER_CAMERA_PHYSICAL_ID, - INNER_CAMERA_PROTECTION_BOUNDS - ) - } -} 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 = + 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 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() + ) = + mock { + whenever(this.displayAdjustments).thenReturn(DisplayAdjustments()) + whenever(this.cutout).thenReturn(cutout) + whenever(this.uniqueId).thenReturn(uniqueId) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt deleted file mode 100644 index 84b2c4bfed34..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt +++ /dev/null @@ -1,680 +0,0 @@ -/* - * Copyright (C) 2021 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.statusbar.phone - -import android.content.Context -import android.content.res.Configuration -import android.graphics.Rect -import android.view.Display -import android.view.DisplayCutout -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.dump.DumpManager -import com.android.systemui.statusbar.commandline.CommandRegistry -import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.util.leak.RotationUtils -import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE -import com.android.systemui.util.leak.RotationUtils.ROTATION_NONE -import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE -import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN -import com.android.systemui.util.leak.RotationUtils.Rotation -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever -import com.google.common.truth.Truth.assertThat -import junit.framework.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.mockito.ArgumentMatchers.any -import org.mockito.Mock -import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations - -@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 configuration = Configuration() - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - `when`(contextMock.display).thenReturn(display) - - context.ensureTestableResources() - `when`(contextMock.resources).thenReturn(context.resources) - `when`(contextMock.resources.configuration).thenReturn(configuration) - `when`(contextMock.createConfigurationContext(any())).thenAnswer { - context.createConfigurationContext(it.arguments[0] as Configuration) - } - configurationController = ConfigurationControllerImpl(contextMock) - } - - @Test - fun testGetBoundingRectForPrivacyChipForRotation_noCutout() { - val screenBounds = Rect(0, 0, 1080, 2160) - val minLeftPadding = 20 - val minRightPadding = 20 - val sbHeightPortrait = 100 - val sbHeightLandscape = 60 - val currentRotation = ROTATION_NONE - val chipWidth = 30 - val dotWidth = 10 - val statusBarContentHeight = 15 - - var isRtl = false - var targetRotation = ROTATION_NONE - var bounds = calculateInsetsForRotationWithRotatedResources( - currentRotation, - targetRotation, - null, - screenBounds, - sbHeightPortrait, - minLeftPadding, - minRightPadding, - isRtl, - dotWidth, - BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight) - - var chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl) - /* 1080 - 20 (rounded corner) - 30 (chip), - * 0 (sb top) - * 1080 - 20 (rounded corner) + 10 ( dot), - * 100 (sb height portrait) - */ - var expected = Rect(1030, 0, 1070, 100) - assertRects(expected, chipBounds, currentRotation, targetRotation) - isRtl = true - chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl) - /* 0 + 20 (rounded corner) - 10 (dot), - * 0 (sb top) - * 0 + 20 (rounded corner) + 30 (chip), - * 100 (sb height portrait) - */ - expected = Rect(10, 0, 50, 100) - assertRects(expected, chipBounds, currentRotation, targetRotation) - - isRtl = false - targetRotation = ROTATION_LANDSCAPE - bounds = calculateInsetsForRotationWithRotatedResources( - currentRotation, - targetRotation, - dc, - screenBounds, - sbHeightLandscape, - minLeftPadding, - minRightPadding, - isRtl, - dotWidth, - BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight) - - chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl) - /* 2160 - 20 (rounded corner) - 30 (chip), - * 0 (sb top) - * 2160 - 20 (rounded corner) + 10 ( dot), - * 60 (sb height landscape) - */ - expected = Rect(2110, 0, 2150, 60) - assertRects(expected, chipBounds, currentRotation, targetRotation) - isRtl = true - chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl) - /* 0 + 20 (rounded corner) - 10 (dot), - * 0 (sb top) - * 0 + 20 (rounded corner) + 30 (chip), - * 60 (sb height landscape) - */ - expected = Rect(10, 0, 50, 60) - assertRects(expected, chipBounds, currentRotation, targetRotation) - } - - @Test - fun privacyChipBoundingRectForInsets_usesTopInset() { - val chipWidth = 30 - val dotWidth = 10 - val isRtl = false - val contentRect = - Rect(/* left = */ 0, /* top = */ 10, /* right = */ 1000, /* bottom = */ 100) - - val chipBounds = - getPrivacyChipBoundingRectForInsets(contentRect, dotWidth, chipWidth, isRtl) - - assertThat(chipBounds.top).isEqualTo(contentRect.top) - } - - @Test - fun testCalculateInsetsForRotationWithRotatedResources_topLeftCutout() { - // 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 minLeftPadding = 20 - val minRightPadding = 20 - val sbHeightPortrait = 100 - val sbHeightLandscape = 60 - val currentRotation = ROTATION_NONE - val isRtl = false - val dotWidth = 10 - val statusBarContentHeight = 15 - - `when`(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(dcBounds.right, - 0, - screenBounds.right - minRightPadding, - sbHeightPortrait) - - var bounds = calculateInsetsForRotationWithRotatedResources( - currentRotation, - targetRotation, - dc, - 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, - dc, - 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, - dc, - 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, - dc, - screenBounds, - sbHeightLandscape, - minLeftPadding, - minRightPadding, - isRtl, - dotWidth, - BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight) - - assertRects(expectedBounds, bounds, currentRotation, targetRotation) - } - - @Test - fun calculateInsetsForRotationWithRotatedResources_bottomAlignedMarginDisabled_noTopInset() { - whenever(dc.boundingRects).thenReturn(emptyList()) - - val bounds = calculateInsetsForRotationWithRotatedResources( - currentRotation = ROTATION_NONE, - targetRotation = ROTATION_NONE, - displayCutout = dc, - maxBounds = Rect(0, 0, 1080, 2160), - statusBarHeight = 100, - minLeft = 0, - minRight = 0, - isRtl = false, - dotWidth = 10, - bottomAlignedMargin = BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight = 15) - - assertThat(bounds.top).isEqualTo(0) - } - - @Test - fun calculateInsetsForRotationWithRotatedResources_bottomAlignedMargin_topBasedOnMargin() { - whenever(dc.boundingRects).thenReturn(emptyList()) - - val bounds = calculateInsetsForRotationWithRotatedResources( - currentRotation = ROTATION_NONE, - targetRotation = ROTATION_NONE, - displayCutout = dc, - maxBounds = Rect(0, 0, 1080, 2160), - statusBarHeight = 100, - minLeft = 0, - minRight = 0, - isRtl = false, - dotWidth = 10, - bottomAlignedMargin = 5, - statusBarContentHeight = 15) - - // Content in the status bar is centered vertically. To achieve the bottom margin we want, - // we need to "shrink" the height of the status bar until the centered content has the - // desired bottom margin. To achieve this shrinking, we use top inset/padding. - // "New" SB height = bottom margin * 2 + content height - // Top inset = SB height - "New" SB height - val expectedTopInset = 75 - assertThat(bounds.top).isEqualTo(expectedTopInset) - } - - @Test - fun testCalculateInsetsForRotationWithRotatedResources_nonCornerCutout() { - // GIVEN phone in portrait mode, where width < height and the cutout is not in the corner - // the assumption here is that if the cutout does NOT touch the corner then we have room to - // layout the status bar in the given space. - - val screenBounds = Rect(0, 0, 1080, 2160) - // cutout centered at the top - val dcBounds = Rect(490, 0, 590, 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 - - `when`(dc.boundingRects).thenReturn(listOf(dcBounds)) - - // THEN only the landscape/seascape rotations should avoid the cutout area because of the - // potential letterboxing - var targetRotation = ROTATION_NONE - var expectedBounds = Rect(minLeftPadding, - 0, - screenBounds.right - minRightPadding, - sbHeightPortrait) - - var bounds = calculateInsetsForRotationWithRotatedResources( - currentRotation, - targetRotation, - dc, - 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, - dc, - screenBounds, - sbHeightLandscape, - minLeftPadding, - minRightPadding, - isRtl, - dotWidth, - BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight) - - assertRects(expectedBounds, bounds, currentRotation, targetRotation) - - targetRotation = ROTATION_UPSIDE_DOWN - expectedBounds = Rect(minLeftPadding, - 0, - screenBounds.right - minRightPadding, - sbHeightPortrait) - - bounds = calculateInsetsForRotationWithRotatedResources( - currentRotation, - targetRotation, - dc, - screenBounds, - sbHeightPortrait, - minLeftPadding, - minRightPadding, - isRtl, - dotWidth, - BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight) - - assertRects(expectedBounds, bounds, currentRotation, targetRotation) - - targetRotation = ROTATION_SEASCAPE - expectedBounds = Rect(minLeftPadding, - 0, - screenBounds.height() - dcBounds.height() - dotWidth, - sbHeightLandscape) - - bounds = calculateInsetsForRotationWithRotatedResources( - currentRotation, - targetRotation, - dc, - screenBounds, - sbHeightLandscape, - minLeftPadding, - minRightPadding, - isRtl, - dotWidth, - BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight) - - assertRects(expectedBounds, bounds, currentRotation, targetRotation) - } - - @Test - fun testCalculateInsetsForRotationWithRotatedResources_noCutout() { - // GIVEN device in portrait mode, where width < height and no cutout - val currentRotation = ROTATION_NONE - val screenBounds = Rect(0, 0, 1080, 2160) - val minLeftPadding = 20 - val minRightPadding = 20 - val sbHeightPortrait = 100 - val sbHeightLandscape = 60 - val isRtl = false - val dotWidth = 10 - val statusBarContentHeight = 15 - - // THEN content insets should only use rounded corner padding - var targetRotation = ROTATION_NONE - var expectedBounds = Rect(minLeftPadding, - 0, - screenBounds.right - minRightPadding, - sbHeightPortrait) - - var bounds = calculateInsetsForRotationWithRotatedResources( - currentRotation, - targetRotation, - null, /* no cutout */ - screenBounds, - sbHeightPortrait, - minLeftPadding, - minRightPadding, - isRtl, - dotWidth, - BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight) - assertRects(expectedBounds, bounds, currentRotation, targetRotation) - - targetRotation = ROTATION_LANDSCAPE - expectedBounds = Rect(minLeftPadding, - 0, - screenBounds.height() - minRightPadding, - sbHeightLandscape) - - bounds = calculateInsetsForRotationWithRotatedResources( - currentRotation, - targetRotation, - null, /* no cutout */ - screenBounds, - sbHeightLandscape, - minLeftPadding, - minRightPadding, - isRtl, - dotWidth, - BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight) - assertRects(expectedBounds, bounds, currentRotation, targetRotation) - - targetRotation = ROTATION_UPSIDE_DOWN - expectedBounds = Rect(minLeftPadding, - 0, - screenBounds.width() - minRightPadding, - sbHeightPortrait) - - bounds = calculateInsetsForRotationWithRotatedResources( - currentRotation, - targetRotation, - null, /* no cutout */ - screenBounds, - sbHeightPortrait, - minLeftPadding, - minRightPadding, - isRtl, - dotWidth, - BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight) - assertRects(expectedBounds, bounds, currentRotation, targetRotation) - - targetRotation = ROTATION_LANDSCAPE - expectedBounds = Rect(minLeftPadding, - 0, - screenBounds.height() - minRightPadding, - sbHeightLandscape) - - bounds = calculateInsetsForRotationWithRotatedResources( - currentRotation, - targetRotation, - null, /* no cutout */ - screenBounds, - sbHeightLandscape, - minLeftPadding, - minRightPadding, - isRtl, - dotWidth, - BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight) - assertRects(expectedBounds, bounds, currentRotation, targetRotation) - } - - @Test - fun testMinLeftRight_accountsForDisplayCutout() { - // 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 minLeftPadding = 80 - val minRightPadding = 150 - val sbHeightPortrait = 100 - val sbHeightLandscape = 60 - val currentRotation = ROTATION_NONE - val isRtl = false - val dotWidth = 10 - val statusBarContentHeight = 15 - - `when`(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 - val expectedBounds = Rect(dcBounds.right, - 0, - screenBounds.right - minRightPadding, - sbHeightPortrait) - - val bounds = calculateInsetsForRotationWithRotatedResources( - currentRotation, - targetRotation, - dc, - screenBounds, - sbHeightPortrait, - minLeftPadding, - minRightPadding, - isRtl, - dotWidth, - BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight) - - assertRects(expectedBounds, bounds, currentRotation, targetRotation) - } - - @Test - fun testDisplayChanged_returnsUpdatedInsets() { - // GIVEN: get insets on the first display and switch to the second display - val provider = StatusBarContentInsetsProvider(contextMock, configurationController, - mock(), mock()) - - configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160)) - val firstDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE) - - configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 800, 600)) - - // WHEN: get insets on the second display - val secondDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE) - - // THEN: insets are updated - assertThat(firstDisplayInsets).isNotEqualTo(secondDisplayInsets) - } - - @Test - fun testDisplayChangedAndReturnedBack_returnsTheSameInsets() { - // GIVEN: get insets on the first display, switch to the second display, - // get insets and switch back - val provider = StatusBarContentInsetsProvider(contextMock, configurationController, - mock(), mock()) - - configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160)) - val firstDisplayInsetsFirstCall = provider - .getStatusBarContentAreaForRotation(ROTATION_NONE) - - configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 800, 600)) - provider.getStatusBarContentAreaForRotation(ROTATION_NONE) - - configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160)) - - // WHEN: get insets on the first display again - val firstDisplayInsetsSecondCall = provider - .getStatusBarContentAreaForRotation(ROTATION_NONE) - - // THEN: insets for the first and second calls for the first display are the same - assertThat(firstDisplayInsetsFirstCall).isEqualTo(firstDisplayInsetsSecondCall) - } - - // Regression test for b/245799099 - @Test - fun onMaxBoundsChanged_listenerNotified() { - // Start out with an existing configuration with bounds - configuration.windowConfiguration.setMaxBounds(0, 0, 100, 100) - configurationController.onConfigurationChanged(configuration) - val provider = StatusBarContentInsetsProvider(contextMock, configurationController, - mock(), mock()) - val listener = object : StatusBarContentInsetsChangedListener { - var triggered = false - - override fun onStatusBarContentInsetsChanged() { - triggered = true - } - } - provider.addCallback(listener) - - // WHEN the config is updated with new bounds - configuration.windowConfiguration.setMaxBounds(0, 0, 456, 789) - configurationController.onConfigurationChanged(configuration) - - // THEN the listener is notified - assertThat(listener.triggered).isTrue() - } - - @Test - fun onDensityOrFontScaleChanged_listenerNotified() { - configuration.densityDpi = 12 - val provider = StatusBarContentInsetsProvider(contextMock, configurationController, - mock(), mock()) - val listener = object : StatusBarContentInsetsChangedListener { - var triggered = false - - override fun onStatusBarContentInsetsChanged() { - triggered = true - } - } - provider.addCallback(listener) - - // WHEN the config is updated - configuration.densityDpi = 20 - configurationController.onConfigurationChanged(configuration) - - // THEN the listener is notified - assertThat(listener.triggered).isTrue() - } - - @Test - fun onThemeChanged_listenerNotified() { - val provider = StatusBarContentInsetsProvider(contextMock, configurationController, - mock(), mock()) - val listener = object : StatusBarContentInsetsChangedListener { - var triggered = false - - override fun onStatusBarContentInsetsChanged() { - triggered = true - } - } - provider.addCallback(listener) - - configurationController.notifyThemeChanged() - - // THEN the listener is notified - assertThat(listener.triggered).isTrue() - } - - private fun assertRects( - expected: Rect, - actual: Rect, - @Rotation currentRotation: Int, - @Rotation targetRotation: Int - ) { - assertTrue( - "Rects must match. currentRotation=${RotationUtils.toString(currentRotation)}" + - " targetRotation=${RotationUtils.toString(targetRotation)}" + - " expected=$expected actual=$actual", - expected.equals(actual)) - } - - companion object { - private const val BOTTOM_ALIGNED_MARGIN_NONE = -1 - } -} -- cgit v1.2.3-59-g8ed1b