权限声明与运行时请求:AndroidManifest + rememberLauncherForActivityResult

相机功能是敏感操作,Android 自 6.0(API 23)起强制执行运行时权限模型。即使已在 AndroidManifest.xml 中声明,应用仍须在用户首次使用相机时弹出系统对话框请求授权,用户可选择「允许」或「拒绝」。拒绝后再次请求时,系统会提供「不再询问」选项,开发者必须妥善处理这些状态。

1. 原理与层级

  • 清单声明:告知系统应用需要相机硬件。若缺失,即使运行时请求成功也无法使用相机。
  • 运行时请求:通过 ActivityResult API 启动系统权限对话框,在回调中获取结果。
  • 状态处理:需要区分「已授权」「已拒绝」「拒绝且不再询问」三种状态,并给出相应 UI 提示或引导。
核心 API:rememberLauncherForActivityResult + ActivityResultContracts.RequestPermission

2. 实现流程

  1. 在 AndroidManifest.xml 中添加相机权限声明。
  2. 在 Composable 函数中通过 rememberLauncherForActivityResult 创建权限请求启动器。
  3. 检查当前权限是否已授予(ContextCompat.checkSelfPermission)。
  4. 若未授权,调用启动器的 launch() 方法。
  5. 在回调中根据 isGranted 布尔值更新 UI 状态。
  6. 处理二次拒绝、权限永久拒绝(需跳转系统设置)等情况。
最佳实践:始终先判断是否应显示 rationale(解释为何需要权限),再触发请求。Android 提供了 shouldShowRequestPermissionRationale 方法。
运行时相机权限请求流程 应用启动/点击拍照 权限已授予? 打开相机预览 请求 用户响应 允许 拒绝 提示/禁用相机按钮

3. 清单声明

在 app/src/main/AndroidManifest.xml 的 <manifest> 内添加:

<uses-permission android:name="android.permission.CAMERA" />

<!-- 如果应用仅在支持相机的设备上运行,可以添加: -->
<uses-feature android:name="android.hardware.camera" android:required="true" />
注意:android:required="true" 会使不支持相机的设备无法安装应用。若允许使用用户头像等可选功能,应设为 false 并在代码中检查 PackageManager.hasSystemFeature。

4. 运行时请求代码

@Composable
fun CameraPermissionGate(
    onGranted: () -> Unit,
    onDenied: (shouldShowRationale: Boolean) -> Unit
) {
    val context = LocalContext.current
    var permissionState by remember { mutableStateOf(PermissionState.UNKNOWN) }

    val permissionLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission()
    ) { isGranted ->
        permissionState = if (isGranted) PermissionState.GRANTED else PermissionState.DENIED
    }

    LaunchedEffect(Unit) {
        when {
            ContextCompat.checkSelfPermission(
                context, Manifest.permission.CAMERA
            ) == PackageManager.PERMISSION_GRANTED -> {
                permissionState = PermissionState.GRANTED
                onGranted()
            }
            else -> {
                val shouldShowRationale = ActivityCompat.shouldShowRequestPermissionRationale(
                    context as Activity, Manifest.permission.CAMERA
                )
                permissionState = PermissionState.REQUESTING
                permissionLauncher.launch(Manifest.permission.CAMERA)
                // 实际授权结果在回调中处理
            }
        }
    }

    when (permissionState) {
        PermissionState.GRANTED -> { /* 显示相机预览 */ }
        PermissionState.DENIED -> {
            val shouldShowRationale = ActivityCompat.shouldShowRequestPermissionRationale(
                context as Activity, Manifest.permission.CAMERA
            )
            onDenied(shouldShowRationale)
        }
        PermissionState.REQUESTING -> { /* 显示加载或等待状态 */ }
        PermissionState.UNKNOWN -> { /* 初始状态 */ }
    }
}

enum class PermissionState {
    UNKNOWN, GRANTED, DENIED, REQUESTING
}

5. 调试与验证

  • 模拟拒绝:在模拟器中第一次弹出对话框时选择「拒绝」。
  • 模拟「不再询问」:在真机上连续拒绝两次,第三次请求时系统会自动拒绝且不弹出对话框。
  • adb 命令快速测试:
    # 授予相机权限
    adb shell pm grant com.example.cameraapp android.permission.CAMERA
    
    # 撤销相机权限
    adb shell pm revoke com.example.cameraapp android.permission.CAMERA
  • 权限日志:在回调中添加日志 Log.d("Permission", "Granted=$isGranted") 并查看 logcat。

6. 常见错误

错误现象 原因 解决
调用 CameraX.bindToLifecycle 时抛出 SecurityException 权限未授予即尝试访问相机 确保进入相机预览前已检查权限状态
权限对话框不弹出 清单中未声明 CAMERA 权限,或混淆规则移除了权限 检查 AndroidManifest.xml 及 ProGuard 规则
第二次请求时对话框不再弹出 用户勾选了「不再询问」 应跳转系统设置页面引导用户手动开启
shouldShowRequestPermissionRationale 返回永远 false 权限被永久拒绝后或设备策略限制 直接视为无法再请求,提供跳转设置按钮

7. 性能与稳定性优化

  • 避免重复请求:使用状态机管理权限阶段,只在 PermissionState.UNKNOWN 或 DENIED 时触发请求。
  • Compose 重组优化:将权限启动器定义在顶层 Composable,避免重组时重新创建。
  • 生命周期安全:rememberLauncherForActivityResult 内部已处理 Activity 重建,无需额外保存实例状态。
  • 权限降级防护:即使以前授予过,也应在每次打开相机前做运行时检查,因为用户可能在系统设置中撤销权限。
  • 多权限场景:若同时需要相机和录音权限,改用 RequestMultiplePermissions 合同,避免多次弹出对话框。
提示:Android 11(API 30)引入了「自动重置权限」功能,长时间未使用的应用会被系统自动撤销权限。应在 onResume 或 LaunchedEffect 中重新检查权限状态。