爲什麼系統禁用錄音權限後,在Android 6.0以上版本手機運行崩潰?爲什麼清單文件聲明瞭錄音權限,Android 6.0以下版本僅第一次提示權限授予窗口?爲什麼使用運行時權限請求,返回權限以授予?怎麼讓Android應用程序在每次操作時識別系統是否禁用對應權限?如果你和TeachCourse一樣存在很多很多的疑問,說明你還沒明白傳統在manifest清單文件聲明權限和運行時權限請求之間的區別。
一、對比傳統權限聲明和運行時權限請求的區別
傳統權限聲明針對Android 6.0及其以下版本使用,Android 6.0對應的API版本23,聲明的方式直接將所有應用程序用到的權限同一在manifest
清單文件中定義,使用標籤<uses-permission>
,應用程序點擊安裝的過程,羅列清單文件聲明的所有權限,安裝完成後用戶可以選擇是否授予應用程序某個隱私的權限,Android系統提供:允許、提示和禁止三種選擇,下面看一組演示:
-
build.gradle
選擇編譯版本、目標版本都是API 19,運行在Android 4.4.2系統(華爲)效果1: -
build.gradle
選擇編譯版本、目標版本都是API 19,運行在Android 6.0.1系統(小米)效果2: -
build.gradle
選擇編譯版本、目標版本都是API 23,運行在Android 4.4.2系統(華爲)效果3: -
build.gradle
選擇編譯版本、目標版本都是API 23,運行在Android 6.0.1系統(小米)效果4:
效果1演示傳統權限授予過程,在完成安裝的過程中可以選擇某個權限是否允許、提示和禁止狀態;效果2演示低版本應用程序在Android 6.0以上系統安裝過程,默認授予應用程序清單文件聲明的所有權限,小米手機測試無法修改權限狀態;效果3和效果4演示API版本23開發的應用程序分別安裝在低版本和高版本系統權限授予過程,安裝在低版本時授予權限過程和傳統的方式一樣,用戶可以修改權限的狀態;安裝到高版本時授予權限過程發生了很大變化,用戶安裝過程無法修改權限狀態,最後運行應用程序的錄音功能,出現閃退、崩潰現象。到這裏,你是不是和TeachCourse一樣,有一點點明白傳統權限聲明和運行時權限請求之間的區別嗎?
二、深入理解運行時權限請求過程
是不是我們可以大致認爲:使用API 23及其以上版本開發的應用程序安裝在Android 6.0系統以下的手機,默認授予應用程序清單文件所有的權限,安裝在Android 6.0系統以上的手機默認禁止清單文件聲明的所有權限?應用程序在無法獲取權限的過程,調用Android開發庫提供的一些方法,某個方法返回null或屬性爲null,就可能導致使用部分功能時應用程序崩潰,而部分被禁止權限的功能雖然不會導致程序崩潰,但也無法獲取正確的數值。
運行時權限的出現,一改傳統清單文件一鍵授權的不足,防止用戶安裝過程的慣性操作,獲取了用戶某些隱私權限,這些權限包括:收集位置信息,讀取短信內容,記錄用戶數據等,然後進行一些非法操作:發送短信訂閱資費套餐,扣取手機話費等,爲了用戶隱私信息的安全,API 23開發的應用程序統一在運行時提醒用戶授予權限,僅授予針對當前功能使用到的權限,未使用到的權限默認禁止。
那麼如何兼容低版本的應用程序呢?以及如何讓高版本的應用程序也能在Android 6.0以下系統正常運行?那可能就像文章開頭演示的四種效果圖
運行時權限涉及的幾個過程:第一檢查權限是否被授予,使用方法checkPermission()
;第二請求獲取權限,使用方法requestPermissions()
;第三用戶是否授予應用程序權限,監聽回調方法onRequestPermissionsResult()
,爲了防止應用API
23開發的應用程序在Android 6.0以上系統正常安裝,在代碼中添加權限檢查,如下:
int flag = getPackageManager().checkPermission(android.Manifest.permission.RECORD_AUDIO,getPackageName());
if (PackageManager.PERMISSION_GRANTED==flag){
/**執行錄音操作**/
...
}else {
/**提示用戶操作**/
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, REQUEST_CODE_GET_RECORDER_PERMISSION);
}
針對效果圖4,運行時請求獲取錄音權限,然後點擊禁止後回調方法onRequestPermissionsResult()
,如下圖:
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == REQUEST_CODE_GET_RECORDER_PERMISSION) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
...
} else {
...
ToastUtil.getInstance(this).showToast(Toast.LENGTH_SHORT, "錄音權限被禁用,請在權限管理修改");
}
return;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
應用程序請求授予權限後,如果用戶點擊禁止,以後每次權限檢查不再出現選擇提示窗口,onRequestPermissionsResult()
方法返回權限的狀態是PackageManager.PERMISSION_DENIED
,防止反覆彈出要求用戶授予權限彈窗,如果開發者仍然期待在用戶沒有禁止權限狀態後,再次提醒用戶授予權限,需要調用方法shouldShowRequestPermissionRationale()
,該方法的目的顯示系統UI說明提示用戶重新授予應用程序權限,Nexus
5測試運行效果,如下:
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == REQUEST_CODE_GET_RECORDER_PERMISSION) {
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
boolean isSecondRequest = ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[0]);
if (isSecondRequest)
/**重新請求授予權限,顯示權限說明(該說明屬於系統UI內容,區別第一次彈窗)**/
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, 0x11);
else
Toast.makeText(this, "錄音權限被禁用,請在權限管理修改", Toast.LENGTH_SHORT).show();
}
return;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
查看shouldShowRequestPermissionRationale()
源碼說明,詳細瞭解該方法的使用:獲取是否你應該通過顯示UI說明請求授權的原因,只有當你沒有獲得該權限,同時當前上下文環境需要的權限沒有明確和用戶溝通——對於獲取該權限有什麼用處,這時候你應該調用該方法。比如說,如果你寫了一個拍照功能的APP,請求了用戶可能需要的拍照權限,而沒有解釋爲什麼請求的權限是必須的,可能用戶沒覺得不正常;然而如果當前APP在拍照時請求獲取位置的權限,這時對於一個不精通技術的用戶來說可能想知道定位和拍照是怎樣的一種聯繫。在這個情景之下,你大概會選擇通過一個顯示UI說明請求授權的原因
/**
* Gets whether you should show UI with rationale for requesting a permission.
* You should do this only if you do not have the permission and the context in
* which the permission is requested does not clearly communicate to the user
* what would be the benefit from granting this permission.
* <p>
* For example, if you write a camera app, requesting the camera permission
* would be expected by the user and no rationale for why it is requested is
* needed. If however, the app needs location for tagging photos then a non-tech
* savvy user may wonder how location is related to taking photos. In this case
* you may choose to show UI with rationale of requesting this permission.
* </p>
*
* @param activity The target activity.
* @param permission A permission your app wants to request.
* @return Whether you can show permission rationale UI.
*
* @see #checkSelfPermission(android.content.Context, String)
* @see #requestPermissions(android.app.Activity, String[], int)
*/
public static boolean shouldShowRequestPermissionRationale(@NonNull Activity activity,
@NonNull String permission) {
if (Build.VERSION.SDK_INT >= 23) {
return ActivityCompatApi23.shouldShowRequestPermissionRationale(activity, permission);
}
return false;
}
說明:
在Android 6.0.1系統的小米手機測試,在用戶禁止後,調用shouldShowRequestPermissionRationale
該方法沒有顯示說明;使用Android
Studio內置的Nexus 5模擬器測試用戶第一次請求權限彈窗只有DENY和ALLOW選項,在用戶選擇DENY後再次調用requestPermissions
方法,彈窗除了DENY和ALLOW選項外,還多了一個Never
ask again複選框
三、關於ActivityCompat
的說明
在上面檢查授予權限的代碼中,TeachCourse使用了getPackageManager().checkPermission()
這個方法檢查,考慮到謙容高低版本API的問題,還是推薦使用v4包下的ActivityCompat.checkSelfPermission()
這個靜態方法或者父類ContextCompat.checkSelfPermission()
;請求權限推薦使用ActivityCompat.requestPermissions()
這個靜態方法,如果第一次禁止後,重新彈窗顯示UI說明,調用靜態方法ActivityCompat.shouldShowRequestPermissionRationale()
後重新授權,具體可以查看ActivityCompat
源碼理解它們之間的關係。
在API 23的版本中,查看ActivityCompat
的源碼,上述的三個方法最終來自受保護的類ActivityCompatApi23
,在源碼中檢查了應用程序的API版本。
public static void requestPermissions(final @NonNull Activity activity,
final @NonNull String[] permissions, final int requestCode) {
if (Build.VERSION.SDK_INT >= 23) {
ActivityCompatApi23.requestPermissions(activity, permissions, requestCode);
}
...
}
響應用戶授權狀態的回調方法onRequestPermissionsResult()
屬於ActivityCompat
內部的一個接口,如果沒有猜錯的話,僅在API
23以後的版本中,實現了ActivityCompat.OnRequestPermissionsResultCallback
接口的Activity子類,才能回調onRequestPermissionsResult()
方法,同時也會看到FragmentActivity、AppCompatActivity源碼實現了上述接口。
public class ActivityCompat extends ContextCompat {
/**
* This interface is the contract for receiving the results for permission requests.
*/
public interface OnRequestPermissionsResultCallback {
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults);
}
...
四、運行時權限策略
提出了運行時權限,在運行應用程序的時候,每使用應用程序的一個功能開發者就需要請求授權一次,那必然會加大了開發者的工作量,請求權限的代碼會變得很多,同時本來運行時權限的申請方式本來就比傳統權限請求方式複雜,如果再讓開發者一次次請求授權那肯定非常反感。爲了解決權限反覆多次請求的問題,Google採用了權限分組的策略:同一組的多個權限,只要獲得了用戶授予的一個權限,同時可以使用同組的其他權限,權限的分組情況如下圖:
4.1 Permission-Group,具體可以查看源碼android.Manifest.permission_group
-
android.permission-group.CALENDAR
-
android.permission.READ_CALENDAR
-
android.permission.WRITE_CALENDAR
-
-
android.permission-group.CAMERA
-
android.permission.CAMERA
-
-
android.permission-group.CONTACTS
-
android.permission.READ_CONTACTS
-
android.permission.WRITE_CONTACTS
-
android.permission.GET_GET_ACCOUNTS
-
-
android.permission-group.LOCATION
-
android.permission.ACCESS_COARSE_LOCATION
-
android.permission.ACCESS_FINE_LOCATION
-
-
android.permission-group.MICROPHONE
-
android.permission.RECORD_AUDIO
-
-
android.permission-group.PHONE
-
android.permission.READ_PHONE_STATE
-
android.permission.CALL_PHONE
-
android.permission.READ_CALL_LOG
-
android.permission.WRITE_CALL_LOG
-
android.permission.ADD_VOICEMAIL
-
android.permission.USE_SIP
-
android.permission.PROCESS_OUTGOING_CALLS
-
-
android.permission-group.SENSORS
-
android.permission.BODY_SENSORS
-
-
android.permission-group.SMS
-
android.permission.SEND_SMS
-
android.permission.RECEIVE_SMS
-
android.permission.READ_SMS
-
android.permission.RECEIVE_WAP_PUSH
-
android.permission.RECEIVE_MMS
-
-
android.permission-group.STORAGE
-
android.permission.READ_EXTERNAL_STORAGE
-
android.permission.WRITE_EXTERNAL_STORAGE
-
參考資料:https://developer.android.google.cn/guide/topics/security/permissions.html#perm-groups
ps:
Demo已上傳GitHub,路徑activity\AudioMainActivity.java
下載地址
https://github.com/TeachCourse/AllDemos