2

Android 12 came up with a new Privacy Settings to disable access to the Camera and Mic sensors, which is referred as Toggles in the docs.

As it is mentioned in the docs:

the system reminds the user that the device-wide toggle is turned off

However, it seems that it only reminds the user when requesting the Camera permission and not when trying to authenticate the user using biometrics (face authentication on Pixel phones, which guess what!? It uses the camera). [I'm using AndroidX biometrics library]

Is there any way to find out if the Camera access has been blocked by the user without requesting any permission?

I guess the note in the docs didn't take into account that the app might use face authentication:

Note: The toggles mentioned in this section shouldn't require changes to your app's logic, as long as you follow privacy best practices.

Notes:

  • You can't register a new face in Settings when camera access is blocked. The Settings app does not show any error, just a blank camera feed
  • I am using Pixel 4 (Android 12)
  • The feature 'Join Wi-Fi by scanning a QR code' does not work and neither shows a feedback to the user if Camera access is blocked (Pixel 5)
rewgoes
  • 656
  • 2
  • 9
  • 23

1 Answers1

3

So, I also looking for a solution - a have a biometric library and few reports appear in DM with the same problem - FaceUnlock doesn't work on Pixel 4 when the camera 'muted'

For now, still now fix, but maybe my research can help someone.

1. I checked the new API for PrivacyToggle's.
Android 12 introduces a new SensorPrivacyManager with supportsSensorToggle() method - it returns TRUE in case of device able to 'mute' camera or mic.

val sensorPrivacyManager = applicationContext
        .getSystemService(SensorPrivacyManager::class.java)
        as SensorPrivacyManager
val supportsMicrophoneToggle = sensorPrivacyManager
        .supportsSensorToggle(Sensors.MICROPHONE)
val supportsCameraToggle = sensorPrivacyManager
        .supportsSensorToggle(Sensors.CAMERA)

If you look into SensorPrivacyManager, you can find that it provides some more useful methods, so I develop the next code:

fun isCameraAccessible(): Boolean {
        return !checkIsPrivacyToggled(SensorPrivacyManager.Sensors.CAMERA)
    }

    @SuppressLint("PrivateApi")
    private fun checkIsPrivacyToggled(sensor: Int): Boolean {
        val sensorPrivacyManager: SensorPrivacyManager =
            appContext.getSystemService(SensorPrivacyManager::class.java)
        if (sensorPrivacyManager.supportsSensorToggle(sensor)) {

            val userHandleField = UserHandle::class.java.getDeclaredField("USER_CURRENT")

            userHandleField.isAccessible = true

                    val userHandle = userHandleField.get(null) as Int

                    val m = SensorPrivacyManager::class.java.getDeclaredMethod(
                        "isSensorPrivacyEnabled",
                        Int::class.javaPrimitiveType,
                        Int::class.javaPrimitiveType
                    )

                    m.isAccessible = true

                    return m.invoke(
                        sensorPrivacyManager,
                        sensor,
                        userHandle
                    ) as Boolean

        }
        return false
    }

Unfortunately, the service rejects this call due to SecurityException - missing android.permission.OBSERVE_SENSOR_PRIVACY, even if we declare it in Manifest. At least on emulator.

2. We can try to identify a new "sensor-in-use" indicator

fun checkForIndicator(){
findViewById<View>(Window.ID_ANDROID_CONTENT)?.let {
                it.setOnApplyWindowInsetsListener { view, windowInsets ->
                    val indicatorBounds = windowInsets.privacyIndicatorBounds
                     if(indicatorBounds !=null){
                         Toast.makeText(view.context, "Camera-in-use detected", Toast.LENGTH_LONG).show()
                     }
                    // change your UI to avoid overlapping
                    windowInsets
                }
            }
}

I didn't test this code (no real device), but as for me - it's not very useful, because we can check the camera indicator only AFTER we start Biometric Auth flow, when I need to understand is camera accessible BEFORE Biometric Auth started.

3. Because of PrivicyToogle related to QuickSettings, I decide that perhaps exists a way how Tiles determinate current Privacy Toggle state. But this API use a very interesting solution - it does not use Settings.Global or Settings.Security section, instead, all preferences saved in "system/sensor_privacy.xml" and not accessible for 3rd party apps.

See SensorPrivacyService.java

I believe that exists a way how to find that Camera is blocked, but seems like some deeper research required

UPDATED 28/10/2021

So after some digging in AOSP sources, I found that APP_OP_CAMERA permission reflects the "blocking" state.

Just call if(SensorPrivacyCheck.isCameraBlocked()){ return } - this call also notify the system to show the "Unblock" dialog

Example

Solution:

@TargetApi(Build.VERSION_CODES.S)
@RestrictTo(RestrictTo.Scope.LIBRARY)
object SensorPrivacyCheck {
    fun isMicrophoneBlocked(): Boolean {
        return Utils.isAtLeastS && checkIsPrivacyToggled(SensorPrivacyManager.Sensors.MICROPHONE)
    }

    fun isCameraBlocked(): Boolean {
        return Utils.isAtLeastS && checkIsPrivacyToggled(SensorPrivacyManager.Sensors.CAMERA)
    }

    @SuppressLint("PrivateApi", "BlockedPrivateApi")
    private fun checkIsPrivacyToggled(sensor: Int): Boolean {
        val sensorPrivacyManager: SensorPrivacyManager =
            AndroidContext.appContext.getSystemService(SensorPrivacyManager::class.java)
        if (sensorPrivacyManager.supportsSensorToggle(sensor)) {
            try {
                val permissionToOp: String =
                    AppOpCompatConstants.getAppOpFromPermission(
                        if (sensor == SensorPrivacyManager.Sensors.CAMERA)
                            Manifest.permission.CAMERA else Manifest.permission.RECORD_AUDIO
                    ) ?: return false

                val noteOp: Int = try {
                    AppOpsManagerCompat.noteOpNoThrow(
                        AndroidContext.appContext,
                        permissionToOp,
                        Process.myUid(),
                        AndroidContext.appContext.packageName
                    )
                } catch (ignored: Throwable) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
                        PermissionUtils.appOpPermissionsCheckMiui(
                            permissionToOp,
                            Process.myUid(),
                            AndroidContext.appContext.packageName
                        ) else AppOpsManagerCompat.MODE_IGNORED
                }
                return noteOp != AppOpsManagerCompat.MODE_ALLOWED
            } catch (e: Throwable) {
                e.printStackTrace()
            }
        }
        return false
    }
}