--- title: 'Android 權限' disqus: kyleAlien --- Android 權限 === ## OverView of Content [**官方文檔**](https://developer.android.com/guide/topics/permissions/overview) 有詳細說明要如何控制 Android 的權限 (在 API 23 以後特別需要) [TOC] ## Manifest 權限聲明 應用所需的**所有的權限都 (不論是普通權限 or 危險權限) 必須提前在 Manifest 中聲明**,若是沒有聲明,可是連用都完全不能使用,也又不用下面這些步驟了 ```xml= <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.snazzyapp"> <!-- 權限聲明 --> <uses-permission android:name="android.permission.SEND_SMS"/> <application ...> ... </application> </manifest> ``` ### 權限比對 * 在 targetSdkVersion >= 23 時 (Gralde),用戶在安裝時是不會看到任何的權限通知,**必須要在用戶使用應用時才去申請權限** ```groovy= android { compileSdkVersion 30 buildToolsVersion "30.0.0" defaultConfig { applicationId "com.example.myapplication" minSdkVersion 21 // 比對設定,若超過 23 則運行時申請 targetSdkVersion 30 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } } ``` :::info * Android 5.1.1 (API 22) <= 系統,則會在 Google Play 安裝時就會要求權限 > ![](https://i.imgur.com/JmiNWlh.png) ::: * 不能指望每次都擁有需要的權限 (你得每次問 :cry:),**若是沒有得到權限就訊行則會拋出 SecurityException** ### 硬體權限 * 由於並不是每台 Android 手機都有你目前應用所需的硬體功能 (像有一些可能缺少了羅盤、NFC 功能),那你的應用就不能使用了 ```xml= <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.snazzyapp"> <!-- 硬體權限聲明 --> <!-- 若 required 是 ture 則不能安裝在該裝置上,若為 false 則代表可以安裝在該裝置上 --> <uses-feature android:name="android.hardware.camera" android:required="false" /> <application ...> ... </application> </manifest> ``` * 若 `android:required="false"` 代表就算該手機並沒有這個硬體裝置,仍可安裝這個應用,這時可以**改為使用 PackageManager#hasSystemFeature 確認設備是否有該功能** ```java= public boolean checkHardware() { return PackageManager.hasSystemFeature(...); } ``` :::success * 若是未在 `<uses-feature>` 中設定,當 Google Play 發現應用請求後會假定你的應用 **必須** 要該權限,相當於幫你設定了 `android:required="true"` ::: ## 保護級別 | 級別 | 說明 | | -------- | -------- | | 普通權限 | 系統不會向用戶提示,用戶也無法取消。這種權限將會直接授予應用,但仍需再 Manifest 中添加權限 | | 簽名權限 | 在安裝時期授予應用權限 | | 危險權限 | 涉及用戶隱私 (對用戶儲存數據),必須在運行時提示用戶授予該應用權限 | | 特殊權限 | **^1^ SYSTEM_ALERT_WINDOW、^2^ WRITE_SETTINGS** 特別敏感 (對系統操作) | ### 權限組 * **對權限的要求會有分組**,避免不必要的多次詢問,例如:READ_SMS 被授予的權限,WRITE_SMS 也會 **自動** 被授予權限 > ![](https://i.imgur.com/5IkQpk5.png) * 在 Android 6.0 (API 23) & 用戶的 targetSdkVersion >= 23,當該應用使用到 **危險權限** 時,會跳出權限詢問框,**Dialog 內容就是 ==權限組==,而不是詳細內容** :::danger * Android 權限組判斷 ? 將來 Android SDK 可能將特定權限移動到另外一個權限組,這些權限組是有可能根據 SDK 改變而改變,**所以==不要依照權限組來判斷權限設置== (不要假設權限組相同)** 例如:Android 8.1 起,將 READ_CONTACTS & WRITE_CONTACTS 放置在同一組,在這之前的版本就必須分開判斷 ::: ## 權限請求 有關於權限的請求,請先了解 **保護級別** | API | 功能 | 注意 | | -------- | -------- | -------- | | checkSelfPermission | 檢查自身權限,返回 PackageManager 兩種情況,^1^PERMISSION_GRANTED(成功),^2^PERMISSION_DENIED(失敗) | **在 API 23 以上** | | requestPermissions | 權限要求,可同時要求多種權限 | 同上運行在 23 以上,並且**有一個自訂的 requestCode(可用來確認請求)** | | onRequestPermissionsResult(覆寫方法) | 當你使用 requestPermissions 後,他會將用戶的結果返回到該函數中 | 它的參數對應了 requestPermissions 參數| | **shouldShowRequestPermissionRationale** | 這個功能較特別,**它可以判斷是否讓用戶進入權限說明**(自己製作) | **^1^用戶不曾拒絕返回 false**,**^2^用戶拒絕之後就返回 ture**,**^3^用戶拒絕並不再同意返回 false** | ### 檢查權限 * 如果該應用需要一個 **危險權限**,**在 Android 6.0 (API 23) 以後用戶可以隨時撤銷該應用的權限授予** :::info 建議**在用戶使用的功能有需求時再檢查 & 請求權限**,不要在應用開始時就要求權限 ::: * **使用 ==ContextCompat== 類的 ++checkSelfPermission++ 方法來檢查權限的授予狀態**,第一個參數為你所需要求的權限(可為多個),可把第二個參數 requestCode 就是這次請求的 ID ```java= /** * 權限檢查函數 checkSelf * 假設檢查為寫入儲存 WRITE_EXTERNAL_STORAGE */ private static final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE; private void checkSelf() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { String msg; if(checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) { msg = "Have permission"; } else { msg = "Request permission"; requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); } showToast(msg); } } private void showToast(String str) { if(str == null) return; Toast.makeText(this, str, Toast.LENGTH_SHORT).show(); } ``` ### 檢查用戶回應 * 這裡必須覆寫 onRequestPermissionsResult 方法,並使用它給予的參數做判斷,**^1^requestCode 對應請求時所填寫的請求 ID,^2^這次請求的權限,^3^對應權限數組返回使用者是否同意** * 以下會配合 shouldShowRequestPermissionRationale 進入請求教學 (或是說,解釋為何需要該權限),這個函數有三種狀況 | 狀況 | 選擇圖示 | 函數返回結果 | | -------- | -------- | -------- | | 用戶不曾拒絕 | ![](https://i.imgur.com/JkKJuu5.png) | false | | 用戶拒絕之後就 | ![](https://i.imgur.com/rMT1ehV.png) | ture | | 用戶拒絕 ++**並不再同意**++ | ![](https://i.imgur.com/bMVTZgN.png) | false | ```java= @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if(requestCode == 1) { if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // 成功 showToast("Open Permission success"); } else { String msg = "Open Permission fail"; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if(shouldShowRequestPermissionRationale(permission)) { // 第一次拒絕後就會觸發 true,若是以確認不再同意則為 false // 解釋權限 (教學) msg = "已拒絕一次"; showTeachDialog(); } } showToast(msg); } } } // 解釋 Dialog public void showTeachDialog() { AlertDialog alertDialog = new AlertDialog.Builder(this) .setMessage(permission + "to recode something") .setTitle("Why use it") .setNegativeButton("Ok", (dialog, which) -> { // 要求權限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); } dialog.dismiss(); }) .setNeutralButton("Think", (dialog, which) -> { dialog.dismiss(); }) .setPositiveButton("Leave", (dialog, which) -> { finish(); }) .create(); alertDialog.show(); } // Toast private void showToast(String str) { if(str == null) return; Toast.makeText(this, str, Toast.LENGTH_SHORT).show(); } ``` **--教學畫面--** > ![](https://i.imgur.com/0LbMQZQ.png) ## Appendix & FAQ :::info ::: ###### tags: `Android 基礎`