Android Runtime Permissions 權限處理
在 Android 的安全模型中,權限管理是保護使用者隱私的核心。從 Android 6.0 (API 23) 開始,系統導入了執行時期權限 (Runtime Permissions) 機制。這意味著「危險權限」不僅要在專案中宣告,還必須在 App 運行時,於真正需要該功能的那一刻向使用者請求授權。
權限的類別
Android 將權限分為幾個主要類別,這決定了你是否需要向使用者顯示授權視窗:
1. 一般權限 (Normal Permissions)
這類權限對使用者隱私風險較低。你只需在 Manifest 宣告,系統會在安裝時自動授予。
- 範例:
INTERNET(網路存取),ACCESS_NETWORK_STATE(檢查連線狀態),VIBRATE(震動)。
2. 危險權限 (Dangerous Permissions)
這類權限涉及使用者私密資料或裝置核心功能。必須在執行時動態請求授權。
- 範例:
CAMERA(相機),READ_CONTACTS(讀取聯絡人),ACCESS_FINE_LOCATION(精確定位)。
3. 特殊權限 (Special Permissions)
這類權限通常用於高度敏感的操作,使用者必須到「系統設定」頁面手動開啟,無法透過簡單的對話框請求。
- 範例:
SYSTEM_ALERT_WINDOW(顯示在其他應用程式上層),WRITE_SETTINGS(修改系統設定)。
第一步:在 Manifest 宣告權限
無論是什麼類別的權限,第一步永遠是在 AndroidManifest.xml 中使用 <uses-permission> 標籤宣告:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 一般權限:宣告後自動獲得 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 危險權限:宣告後仍需動態請求 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application ...>
...
</application>
</manifest>
第二步:請求單一權限 (Jetpack Compose)
在現代的 Compose 開發中,我們使用 rememberLauncherForActivityResult 配合 ActivityResultContracts.RequestPermission 契約來處理請求。
@Composable
fun CameraPermissionScreen() {
val context = LocalContext.current
// 1. 宣告權限請求的 Launcher
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
// 使用者同意了,可執行拍照邏輯
} else {
// 使用者拒絕了
}
}
Button(onClick = {
// 2. 檢查目前是否已擁有權限
val permissionCheck = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
// 已擁有權限,直接執行
} else {
// 尚未擁有,啟動請求對話框
launcher.launch(Manifest.permission.CAMERA)
}
}) {
Text("請求相機權限")
}
}
第三步:同時請求多項權限
有時你的功能需要多個權限(例如地圖功能需要粗略定位與精確定位)。使用 RequestMultiplePermissions。
val multiplePermissionsLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestMultiplePermissions()
) { permissionsMap ->
val areAllGranted = permissionsMap.values.all { it }
if (areAllGranted) {
// 全部權限都拿到了
} else {
// 有部分權限被拒絕
}
}
// 啟動方式
multiplePermissionsLauncher.launch(
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
第四步:處理 Rationale (解釋理由)
根據 Google 指南,如果使用者曾經拒絕過權限,你在第二次請求前應該顯示一段「說明理由」的 UI。這時我們需要檢查 shouldShowRequestPermissionRationale。
val activity = context as Activity
// 檢查是否應該向使用者解釋為什麼需要這個權限
val shouldShowRationale = activity.shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)
if (shouldShowRationale) {
// TODO: 顯示你自己的對話框告知使用者:
// 「我們需要相機來掃描 QR Code,請在接下來的視窗點選同意。」
}
第五步:處理「不再詢問」與永久拒絕
如果使用者多次拒絕或勾選了「不再詢問」,系統視窗將不再出現。此時你唯一的選擇是引導使用者前往 App 系統設定頁面手動開啟。
fun openAppSettings(context: Context) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)
}
現代 Android (API 33+) 的重大變更
隨著 Android 版本演進,權限變得更加精細:
- 通知權限 (Android 13):新增了
POST_NOTIFICATIONS危險權限。 - 媒體存取 (Android 13):原本的
READ_EXTERNAL_STORAGE被拆分為更細的權限:READ_MEDIA_IMAGESREAD_MEDIA_VIDEOREAD_MEDIA_AUDIO
- 相片挑選器 (Photo Picker):對於單純選圖需求,現在建議使用
PickVisualMedia契約,它不需要任何權限就能安全地選取相片。
UX 最佳實踐
- Contextual Request:不要在 App 一啟動就噴出三四個權限請求。應該在使用者點擊「拍照」或「定位」那一刻才請求。
- 解釋價值:如果權限是非必備的(如通知),請明確告訴使用者開啟後能得到什麼好處。
- 優雅降級:如果使用者堅持不給權限,App 應該能在不具備該功能的情況下繼續執行,而不是直接閃退或無法使用。
對於複雜的權限需求,目前社群仍廣泛使用 Google 的 Accompanist Permissions 庫,雖然它已進入維護模式,但其提供的宣告式 API 在 Compose 中仍然非常方便。