1. Android 6.0 在執行時請求許可權介紹
從 Android 6.0(API 級別 23)開始,使用者開始在應用執行時向其授予許可權,而不是在應用安裝時授予。此方法可以簡化應用安裝過程,因為使用者在安裝或更新應用時不需要授予許可權。它還讓使用者可以對應用的功能進行更多控制;例如,使用者可以選擇為相機應用提供相機訪問許可權,而不提供裝置位置的訪問許可權。使用者可以隨時進入應用的設定頁面修改許可權。
1.1、為什麼需要執行時請求許可權
iPhone上的App都是預設下載安裝的,然後執行App時需要什麼許可權就彈窗向使用者申請,這對使用者來說就非常好。因為使用者不想給App許可權就不給,而Android 6.0以前是這樣的,我下載了一個App安裝,系統就彈出這個App需要使用的全部的許可權,就給我看一下,我需要這個App 的話,只能同意所有的許可權都給這個App,要麼我不安裝這個App。
1.2、 Android許可權介紹
系統許可權分為兩類:
- 正常許可權不會直接給使用者隱私權帶來風險。如果您的應用在其manifest中列出了正常許可權,系統將自動授予該許可權。
- 危險許可權會授予應用訪問使用者機密資料的許可權。如果您的應用在其manifest中列出了正常許可權,系統將自動授予該許可權。如果您列出了危險許可權,則使用者必須明確批准您的應用使用這些許可權,也就是說manifest檔案中定義的危險許可權將不會隨著安裝自動授予。
表 1. 危險許可權和許可權組。
許可權組 | 許可權 |
---|---|
CALENDAR | |
CAMERA | |
CONTACTS | |
LOCATION | |
MICROPHONE | |
PHONE | |
SENSORS | |
SMS | |
STORAGE |
從上圖中我們可以看到,Android系統把危險許可權分了9大組,這樣也是為了簡化許可權的申請機制。如果你申請了android.permission.READ_CONTACTS讀取聯絡人的許可權,那麼6.0 系統就會把這一組中其他的許可權也打包給你。我覺得這個和iOS的隱私管理機制非常相似,在iOS系統設定的“隱私->通訊錄”中可以看到,如果你給一個App通訊錄的許可權,那麼這個App既可以讀也可以寫的
Android 6.0裡面只有危險許可權才需要執行時獲取的
1.3、android系統對許可權的處理場景分析
- 如果裝置執行的是 Android 6.0(API 級別 23)或更高版本,並且應用的 targetSdkVersion 是 23 或更高版本,則應用在執行時向使用者請求許可權。使用者可隨時呼叫許可權,因此應用在每次執行時均需檢查自身是否具備所需的許可權。
- 如果裝置執行的是 Android 5.1(API 級別 22)或更低版本,並且應用的 targetSdkVersion 是 22 或更低版本,則系統會在使用者安裝應用時要求使用者授予許可權。如果將新許可權新增到更新的應用版本,系統會在使用者更新應用時要求授予該許可權。使用者一旦安裝應用,他們撤銷許可權的唯一方式是解除安裝應用。
- 如果裝置執行的是 Android 6.0(API 級別 23)或更高版本,並且應用的targetSdkVersion 是 22 或更低版本,此時Android系統會把你申請的全部許可權都給你。使用者依然可以進入App的設定介面把許可權關閉!
例如以下圖片中使用者在android6.0的版本的設定中把許可權關閉,此時你的許可權就用不了了。那麼程式需要考慮對6.0及以上版本的相容,具體參考下面的(android.support.v4.content.PermissionChecker)。
值得注意的是Android系統有一套自動許可權調整的機制,我們知道android每次sdk升級有可能會加入新的許可權,而你的app已經發布到使用者手機上安裝了,除了升級不可能修改Androidmanifest檔案了,此時你可能擔心自己的app能夠在這些新的sdk版本的手機上執行正常嗎,其實android已經考慮了這種場景,Android 將根據為 targetSdkVersion 屬性提供的值決定應用是否需要許可權。如果該值低於在其中新增許可權的版本,則 Android 會為App自動新增該許可權。
例如,API 級別 4 中加入了 WRITE_EXTERNAL_STORAGE 許可權,用以限制訪問共享儲存空間。如果您的 targetSdkVersion 為 3 或更低版本,則會向更新 Android 版本裝置上的應用新增此許可權。
2、如何申請許可權
申請許可權過程包括以下幾個步驟:
- 檢查許可權
- 請求許可權
- 處理許可權請求響應
- 解釋應用為什麼需要許可權
2.1、檢查許可權
如果您的應用需要危險許可權,則每次執行需要這一許可權的操作時您都必須檢查自己是否具有該許可權。使用者始終可以自由呼叫此許可權,因此,即使應用昨天使用了相機,它不能假設自己今天仍具有該許可權,因為使用者可能在設定裡面關閉了。
要檢查您是否具有某項許可權,請呼叫 ContextCompat.checkSelfPermission() 方法。例如,以下程式碼段顯示瞭如何檢查 Activity 是否具有在日曆中進行寫入的許可權:
1 2 3 | // 假設thisActivity是當前螢幕最前端正在和使用者互動的activity int permissionCheck = ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.WRITE_CALENDAR); |
如果應用具有此許可權,方法將返回 PackageManager.PERMISSION_GRANTED,並且應用可以繼續操作。如果應用不具有此許可權,方法將返回 PERMISSION_DENIED,且應用必須明確向使用者要求許可權。
也可以使用ActivityCompat,它們兩個的checkSelfPermission方法是同一個,因為ActivityCompat繼承自ContextCompat,而checkSelfPermission是ContextCompat中的方法。
檢查許可權會有一些特別的問題需要注意,主要有以下兩個:
- 如果App的targetSdkVersion小於23並且執行在Android 6.0系統上,怎麼去檢測使用者關閉了許可權呢?
- 國內有些手機廠商自己定製了手機許可權管理(例如:小米),普通的checkSelfPermission已經不準確了,該如何處理?
- 問題一:
android.support.v4.content.PermissionChecker可以幫我們解決這個問題。這個類的文件是這麼描述的:
For apps targeting API lower than android.os.Build.VERSION_CODES.M these permissions are always granted as such apps do not expect permission revocations and would crash. Therefore, when the user disables a permission for a legacy app in the UI the platform disables the APIs guarded by this permission making them a no-op which is doing nothing or returning an empty result or default error.
翻譯一下是:
當app的targetsdkversion小於23的時候,這些許可權預設都會自動給當前app,但如果app沒有考慮在6.0裝置中被使用者主動撤銷該許可權的場景,那麼可能造成app的崩潰。於是app在使用該許可權過程中系統許可權檢查時如果這個許可權被使用者撤銷了,那麼對應請求的API會什麼都不做或者返回一個空的結果,或者出錯。
PermissionChecker.checkSelfPermission方法就是用於檢查App自身有沒有某一個許可權,這個方法的返回結果只有三種:
- PERMISSION_GRANTED: 已授權
- PERMISSION_DENIED: 沒有被授權
- PERMISSION_DENIED_APP_OP: 沒有被授權
PERMISSION_DENIED和PERMISSION_DENIED_APP_OP都表示沒有被授權,但是它們的區別就在於targetSdkVersion的值,如果targetSdkVersion小於23,就返回PERMISSION_DENIED_APP_OP,否則就返回PERMISSION_DENIED
因此,如果你的App的targetSdkVersion小於23,但是執行在Android 6.0及以後的系統上,你可以用下面的程式碼來檢測app是否有某個許可權:
1 | PermissionChecker.checkSelfPermission(context, permission) == PermissionChecker. PERMISSION_DENIED_APP_OP |
- 問題二:
國產很多手機在google之前已經做了自己的許可權管理,例如小米,所以即使執行在6.0的手機系統上面並使用ContextCompat的checkSelfPermission方法即便返回 PackageManager.PERMISSION_GRANTED 也可能不準確。如果出現這種情況我們需要做一次特殊處理,此時我們需要用到android的隱藏API — AppOpsManager
AppOpsManager官方的解釋是系統內部使用,不提供給APP開發者使用。一個小米裝置相容判斷的程式碼如下:
1 2 3 4 5 6 7 8 9 10 | @TargetApi(Build.VERSION_CODES.M) private static boolean hasSelfPermissionForXiaomi(Context context, String permission) { AppOpsManager appOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); String op = AppOpsManager.permissionToOp(permission); if (!TextUtils.isEmpty(op)) { int checkOp = appOpsManager.checkOp(op, Process.myUid(), context.getPackageName()); return checkOp == AppOpsManager.MODE_ALLOWED && ActivityCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED; } return true; } |
如上面所說小米這樣的手機已經在android 6.0 之前已經有了自己的許可權管理,如果你也想判斷小於6.0的小米手機是否賦予了某個許可權也是可以做到的,還是用到AppOpsManager,但是由於AppOpsManager在API 19才引入的,所以使用時需要指定 @TargetApi(19),示例程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @TargetApi(19) private static boolean hasSelfPermissionForXiaomi(Context context, String permission) { Object opsManager = context.getSystemService(Context.APP_OPS_SERVICE); Class<?> clazz = opsManager.getClass(); try { Method dispatchMethod = clazz.getMethod("checkOp", new Class[] { int.class, int.class, String.class }); int checkOp = (Integer) dispatchMethod.invoke(opsManager, new Object[] {AppOpsChecker.OP_CAMERA, Binder.getCallingUid(), context.getApplicationContext().getPackageName() }); return checkOp == AppOpsManager.MODE_ALLOWED && ActivityCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED; } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } return true; } |
這個 AppOpsChecker.OP_CAMERA 你就需要從原始碼中拷貝出來了,在API 19裡面是引用不到的。
2.2、請求許可權
如果您的應用需要應用manifest檔案中列出的危險許可權,那麼,它必須要求使用者授予該許可權。Android 為您提供了多種許可權請求方式。呼叫這些方法將顯示一個標準的 Android 對話方塊,不過,您不能對它們進行自定義。
如果上面的許可權檢查步驟中結果是應用尚無所需的許可權,則應用必須呼叫一個 requestPermissions() 方法,以請求適當的許可權。應用將傳遞其所需的許可權,以及您指定用於識別此許可權請求的整型
提示使用者授予或拒絕許可權的系統對話方塊如下:
以下程式碼可以檢查應用是否具備讀取使用者聯絡人的許可權,並根據需要請求該許可權:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // 這裡的 thisActivity 是當前螢幕最前端正在和使用者互動的activity if (ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { // 是否需要給使用者一個解釋? if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity, Manifest.permission.READ_CONTACTS)) { // 顯示給使用者需要這個許可權的理由,這個需要是非同步的(不能阻塞當前執行緒去等待使用者的響應!) ,在使用者看完這個解釋後,繼續嘗試請求這些許可權 } else { // 不需要解釋, 我們可以開始請求許可權 ActivityCompat.requestPermissions(thisActivity, new String[]{Manifest.permission.READ_CONTACTS}, MY_PERMISSIONS_REQUEST_READ_CONTACTS); // MY_PERMISSIONS_REQUEST_READ_CONTACTS 這個是app定義的整形常量,用於標識一個請求,回撥方法中會獲得這個請求對應的結果 } } |
注:當您的應用呼叫 requestPermissions() 時,系統將向使用者顯示一個標準對話方塊。您的應用
2.3、處理許可權請求響應
當應用請求許可權時,系統將向使用者顯示一個對話方塊。當使用者響應時,系統將呼叫應用的 onRequestPermissionsResult() 方法,向其傳遞使用者響應。您的應用必須覆寫該方法,以瞭解是否已獲得相應許可權。回撥會將您傳遞的相同請求程式碼傳遞給 requestPermissions()。例如,如果應用請求 READ_CONTACTS 訪問許可權,則它可能採用以下回撥方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case MY_PERMISSIONS_REQUEST_READ_CONTACTS: { // 如果授權取消 這個結果陣列是空 if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // 許可權已經授權, 很棒!可以繼續聯絡人相關的操作了 } else { // 許可權被拒絕了,很糟糕! 禁用和該許可權相關的功能 } return; } // 其它的'case' 程式碼去處理其它的許可權請求回撥 } } |
注意:
當系統要求使用者授予許可權時,使用者可以選擇指示系統不再要求提供該許可權(即勾選對話方塊裡的不在提示)。這種情況下,無論應用在什麼時候使用 requestPermissions() 再次要求該許可權,系統都會立即拒絕此請求。系統會呼叫您的 onRequestPermissionsResult() 回撥方法,並傳遞 PERMISSION_DENIED
2.4 解釋應用為什麼需要許可權
在某些情況下,您可能需要幫助使用者瞭解您的應用為什麼需要某項許可權。例如,如果使用者啟動一個攝影應用,使用者對應用要求使用相機的許可權可能不會感到吃驚,但使用者可能無法理解為什麼此應用想要訪問使用者的位置或聯絡人。在請求許可權之前,不妨為使用者提供一個解釋。請記住,您不需要通過解釋來說服使用者;如果您提供太多解釋,使用者可能發現應用令人失望並將其移除。
您可以採用的一個方法是僅在使用者已拒絕某項許可權請求時提供解釋。如果使用者繼續嘗試使用需要某項許可權的功能,但繼續拒絕許可權請求,則可能表明使用者不理解應用為什麼需要此許可權才能提供相關功能。對於這種情況,比較好的做法是顯示解釋。
為了幫助查詢使用者可能需要解釋的情形,Android 提供了一個實用程式方法,即 shouldShowRequestPermissionRationale()。如果應用之前請求過此許可權但使用者拒絕了請求,此方法將返回 true。
注:如果使用者在過去拒絕了許可權請求,並在許可權請求系統對話方塊中選擇了 Don’t ask again 選項,此方法將返回 false。如果裝置規範禁止應用具有該許可權,此方法也會返回 false。