Loading packages/SystemUI/res/values/config.xml +11 −5 Original line number Diff line number Diff line Loading @@ -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> Loading packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt +64 −19 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) { 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() } Loading Loading @@ -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() { Loading @@ -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 { Loading @@ -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 ) } packages/SystemUI/tests/src/com/android/systemui/CameraAvailabilityListenerTest.kt 0 → 100644 +162 −0 Original line number Diff line number Diff line 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)) } } Loading
packages/SystemUI/res/values/config.xml +11 −5 Original line number Diff line number Diff line Loading @@ -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> Loading
packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt +64 −19 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) { 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() } Loading Loading @@ -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() { Loading @@ -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 { Loading @@ -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 ) }
packages/SystemUI/tests/src/com/android/systemui/CameraAvailabilityListenerTest.kt 0 → 100644 +162 −0 Original line number Diff line number Diff line 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)) } }