diff options
3 files changed, 237 insertions, 24 deletions
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 1add90ff4083..09918842f754 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -531,19 +531,25 @@ </string> <!-- A path similar to frameworks/base/core/res/res/values/config.xml - config_mainBuiltInDisplayCutout that describes a path larger than the exact path of a display - cutout. If present as well as config_enableDisplayCutoutProtection is set to true, then - SystemUI will draw this "protection path" instead of the display cutout path that is normally - used for anti-aliasing. + config_mainBuiltInDisplayCutout that describes a path larger than the exact path of a outer + display cutout. If present as well as config_enableDisplayCutoutProtection is set to true, + then SystemUI will draw this "protection path" instead of the display cutout path that is + normally used for anti-aliasing. This path will only be drawn when the front-facing camera turns on, otherwise the main DisplayCutout path will be rendered --> <string translatable="false" name="config_frontBuiltInDisplayCutoutProtection"></string> - <!-- ID for the camera that needs extra protection --> + <!-- ID for the camera of outer display that needs extra protection --> <string translatable="false" name="config_protectedCameraId"></string> + <!-- Similar to config_frontBuiltInDisplayCutoutProtection but for inner display. --> + <string translatable="false" name="config_innerBuiltInDisplayCutoutProtection"></string> + + <!-- ID for the camera of inner display that needs extra protection --> + <string translatable="false" name="config_protectedInnerCameraId"></string> + <!-- Comma-separated list of packages to exclude from camera protection e.g. "com.android.systemui,com.android.xyz" --> <string translatable="false" name="config_cameraProtectionExcludedPackages"></string> diff --git a/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt b/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt index 2d2ebe99d651..d33d279023da 100644 --- a/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt +++ b/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt @@ -17,6 +17,7 @@ package com.android.systemui import android.content.Context +import android.content.res.Resources import android.graphics.Path import android.graphics.Rect import android.graphics.RectF @@ -33,37 +34,32 @@ import kotlin.math.roundToInt */ class CameraAvailabilityListener( private val cameraManager: CameraManager, - private val cutoutProtectionPath: Path, - private val targetCameraId: String, + private val cameraProtectionInfoList: List<CameraProtectionInfo>, excludedPackages: String, private val executor: Executor ) { - private var cutoutBounds = Rect() private val excludedPackageIds: Set<String> private val listeners = mutableListOf<CameraTransitionCallback>() private val availabilityCallback: CameraManager.AvailabilityCallback = object : CameraManager.AvailabilityCallback() { override fun onCameraClosed(cameraId: String) { - if (targetCameraId == cameraId) { - notifyCameraInactive() + cameraProtectionInfoList.forEach { + if (cameraId == it.cameraId) { + notifyCameraInactive() + } } } override fun onCameraOpened(cameraId: String, packageId: String) { - if (targetCameraId == cameraId && !isExcluded(packageId)) { - notifyCameraActive() + cameraProtectionInfoList.forEach { + if (cameraId == it.cameraId && !isExcluded(packageId)) { + notifyCameraActive(it) + } } } } init { - val computed = RectF() - cutoutProtectionPath.computeBounds(computed, false /* unused */) - cutoutBounds.set( - computed.left.roundToInt(), - computed.top.roundToInt(), - computed.right.roundToInt(), - computed.bottom.roundToInt()) excludedPackageIds = excludedPackages.split(",").toSet() } @@ -100,8 +96,10 @@ class CameraAvailabilityListener( cameraManager.unregisterAvailabilityCallback(availabilityCallback) } - private fun notifyCameraActive() { - listeners.forEach { it.onApplyCameraProtection(cutoutProtectionPath, cutoutBounds) } + private fun notifyCameraActive(info: CameraProtectionInfo) { + listeners.forEach { + it.onApplyCameraProtection(info.cutoutProtectionPath, info.cutoutBounds) + } } private fun notifyCameraInactive() { @@ -121,12 +119,11 @@ class CameraAvailabilityListener( val manager = context .getSystemService(Context.CAMERA_SERVICE) as CameraManager val res = context.resources - val pathString = res.getString(R.string.config_frontBuiltInDisplayCutoutProtection) - val cameraId = res.getString(R.string.config_protectedCameraId) + val cameraProtectionInfoList = loadCameraProtectionInfoList(res) val excluded = res.getString(R.string.config_cameraProtectionExcludedPackages) return CameraAvailabilityListener( - manager, pathFromString(pathString), cameraId, excluded, executor) + manager, cameraProtectionInfoList, excluded, executor) } private fun pathFromString(pathString: String): Path { @@ -140,5 +137,53 @@ class CameraAvailabilityListener( return p } + + private fun loadCameraProtectionInfoList(res: Resources): List<CameraProtectionInfo> { + val list = mutableListOf<CameraProtectionInfo>() + val front = loadCameraProtectionInfo( + res, + R.string.config_protectedCameraId, + R.string.config_frontBuiltInDisplayCutoutProtection + ) + if (front != null) { + list.add(front) + } + val inner = loadCameraProtectionInfo( + res, + R.string.config_protectedInnerCameraId, + R.string.config_innerBuiltInDisplayCutoutProtection + ) + if (inner != null) { + list.add(inner) + } + return list + } + + private fun loadCameraProtectionInfo( + res: Resources, + cameraIdRes: Int, + pathRes: Int + ): CameraProtectionInfo? { + val cameraId = res.getString(cameraIdRes) + if (cameraId == null || cameraId.isEmpty()) { + return null + } + val protectionPath = pathFromString(res.getString(pathRes)) + val computed = RectF() + protectionPath.computeBounds(computed) + val protectionBounds = Rect( + computed.left.roundToInt(), + computed.top.roundToInt(), + computed.right.roundToInt(), + computed.bottom.roundToInt() + ) + return CameraProtectionInfo(cameraId, protectionPath, protectionBounds) + } } + + data class CameraProtectionInfo ( + val cameraId: String, + val cutoutProtectionPath: Path, + val cutoutBounds: Rect + ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/CameraAvailabilityListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/CameraAvailabilityListenerTest.kt new file mode 100644 index 000000000000..b1421b21b377 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/CameraAvailabilityListenerTest.kt @@ -0,0 +1,162 @@ +package com.android.systemui + +import android.graphics.Path +import android.graphics.Rect +import android.graphics.RectF +import android.hardware.camera2.CameraManager +import android.testing.AndroidTestingRunner +import android.util.PathParser +import androidx.test.filters.SmallTest +import com.android.systemui.res.R +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.withArgCaptor +import java.util.concurrent.Executor +import kotlin.math.roundToInt +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class CameraAvailabilityListenerTest : SysuiTestCase() { + companion object { + const val EXCLUDED_PKG = "test.excluded.package" + const val CAMERA_ID_FRONT = "0" + const val CAMERA_ID_INNER = "1" + const val PROTECTION_PATH_STRING_FRONT = "M 50,50 a 20,20 0 1 0 40,0 a 20,20 0 1 0 -40,0 Z" + const val PROTECTION_PATH_STRING_INNER = "M 40,40 a 10,10 0 1 0 20,0 a 10,10 0 1 0 -20,0 Z" + val PATH_RECT_FRONT = rectFromPath(pathFromString(PROTECTION_PATH_STRING_FRONT)) + val PATH_RECT_INNER = rectFromPath(pathFromString(PROTECTION_PATH_STRING_INNER)) + + private fun pathFromString(pathString: String): Path { + val spec = pathString.trim() + val p: Path + try { + p = PathParser.createPathFromPathData(spec) + } catch (e: Throwable) { + throw IllegalArgumentException("Invalid protection path", e) + } + + return p + } + + private fun rectFromPath(path: Path): Rect { + val computed = RectF() + path.computeBounds(computed) + return Rect( + computed.left.roundToInt(), + computed.top.roundToInt(), + computed.right.roundToInt(), + computed.bottom.roundToInt() + ) + } + } + + @Mock private lateinit var cameraManager: CameraManager + @Mock + private lateinit var cameraTransitionCb: CameraAvailabilityListener.CameraTransitionCallback + private lateinit var cameraAvailabilityListener: CameraAvailabilityListener + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + context + .getOrCreateTestableResources() + .addOverride(R.string.config_cameraProtectionExcludedPackages, EXCLUDED_PKG) + context + .getOrCreateTestableResources() + .addOverride(R.string.config_protectedCameraId, CAMERA_ID_FRONT) + context + .getOrCreateTestableResources() + .addOverride( + R.string.config_frontBuiltInDisplayCutoutProtection, + PROTECTION_PATH_STRING_FRONT + ) + context + .getOrCreateTestableResources() + .addOverride(R.string.config_protectedInnerCameraId, CAMERA_ID_INNER) + context + .getOrCreateTestableResources() + .addOverride( + R.string.config_innerBuiltInDisplayCutoutProtection, + PROTECTION_PATH_STRING_INNER + ) + + context.addMockSystemService(CameraManager::class.java, cameraManager) + + cameraAvailabilityListener = + CameraAvailabilityListener.Factory.build(context, context.mainExecutor) + } + + @Test + fun testFrontCamera() { + var path: Path? = null + var rect: Rect? = null + val callback = + object : CameraAvailabilityListener.CameraTransitionCallback { + override fun onApplyCameraProtection(protectionPath: Path, bounds: Rect) { + path = protectionPath + rect = bounds + } + + override fun onHideCameraProtection() {} + } + + cameraAvailabilityListener.addTransitionCallback(callback) + cameraAvailabilityListener.startListening() + + val callbackCaptor = withArgCaptor { + verify(cameraManager).registerAvailabilityCallback(any(Executor::class.java), capture()) + } + + callbackCaptor.onCameraOpened(CAMERA_ID_FRONT, "") + assertNotNull(path) + assertEquals(PATH_RECT_FRONT, rect) + } + + @Test + fun testInnerCamera() { + var path: Path? = null + var rect: Rect? = null + val callback = + object : CameraAvailabilityListener.CameraTransitionCallback { + override fun onApplyCameraProtection(protectionPath: Path, bounds: Rect) { + path = protectionPath + rect = bounds + } + + override fun onHideCameraProtection() {} + } + + cameraAvailabilityListener.addTransitionCallback(callback) + cameraAvailabilityListener.startListening() + + val callbackCaptor = withArgCaptor { + verify(cameraManager).registerAvailabilityCallback(any(Executor::class.java), capture()) + } + + callbackCaptor.onCameraOpened(CAMERA_ID_INNER, "") + assertNotNull(path) + assertEquals(PATH_RECT_INNER, rect) + } + + @Test + fun testExcludedPackage() { + cameraAvailabilityListener.addTransitionCallback(cameraTransitionCb) + cameraAvailabilityListener.startListening() + + val callbackCaptor = withArgCaptor { + verify(cameraManager).registerAvailabilityCallback(any(Executor::class.java), capture()) + } + callbackCaptor.onCameraOpened(CAMERA_ID_FRONT, EXCLUDED_PKG) + + verify(cameraTransitionCb, never()) + .onApplyCameraProtection(any(Path::class.java), any(Rect::class.java)) + } +} |