在Android M(6.0)以前,若是應用須要某個權限,咱們能夠在Manifest文件中指定便可html
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET" />
複製代碼
在安裝時,安裝工具會彈出對話框告知用戶當前安裝的應用所須要的權限:android
爲了更加靈活地控制權限,在Android M以後,對於某些權限,須要程序動態向用戶申請,靜態註冊不在起做用。如咱們在應用內調起攝像頭時,咱們須要本身向系統發出權限申請,系統會彈出對話框告訴用戶這個操做須要什麼權限,用戶選擇以後,系統再把結果返回給應用:安全
一個權限被用戶容許後,還能夠被收回,收回權限的用戶操做一共有兩種:bash
因此,對於須要權限的操做,在使用時每次都須要判斷是否已經受權,由於用戶能夠隨時收回權限。app
Android對各類權限進行了劃分,一共三類:ide
正常權限指對用戶隱私不敏感的信息,好比咱們經常使用的聯網權限 INTERNET。上圖中包含CAMERA和INTERNET權限的APK在Android M上安裝效果以下:函數
危險權限就是咱們須要適配的重點區域了,全部的危險權限都是在運行時(須要時)纔會申請,因此固然在安裝時也無需展現了。須要注意的是,權限進行了分組,每一組中只要有一個權限被授予了,那麼組內其它權限也會被授予。工具
SYSTEM_ALERT_WINDOW:設置懸浮窗 WRITE_SETTINGS:修改系統設置 這些權限在各種安全衛士上使用較多,大部分狀況下咱們都不須要。基本流程就是發一個權限申請給系統權限設置頁面,用戶授予權限以後,在onActivityResult中獲取結果。ui
以上基礎能夠在這篇文章中得到:聊一聊Android 6.0的運行時權限this
在Android M的SDK中,在Activity中新增了進行運行時權限適配的三個API:
void requestPermissions(String[] permissions, int requestCode)//請求權限,參數能夠是一個權限或者是多個。
void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)//請求權限以後的回調。
boolean shouldShowRequestPermissionRationale(String permission)//是否有必要告訴用戶咱們須要這個權限的緣由。
複製代碼
Context中添加了一個API:
int checkSelfPermission(String permission)//用來檢測當前應用是否具備某個權限。
複製代碼
因爲這些API都是Android M以上版本纔有,爲了不咱們在代碼裏面引入過多的版本判斷,support包23版本中添加了個對應的API:
ActivityCompat.requestPermissions(Activity activity,String[] permissions,int requestCode)
FragmentActivity.onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
boolean ActivityCompat.shouldShowRequestPermissionRationale(Activity, String permission)
ContextCompat.checkSelfPermission(String permission)
複製代碼
官方training中有個例子,以應用獲取權限READ_CONTACTS爲例,在獲取權限以後,咱們要讀取手機的聯繫人列表操做:readContacts()。
// 檢查是否已經具備權限
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);
//用戶受權的結果會回調到FragmentActivity的onRequestPermissionsResult
}
}else {
//已經擁有受權
readContacts();
}
複製代碼
在onRequestPermissionsResult中:
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) {
readContacts();
} else {
//權限沒能受權經過,能夠考慮彈個toast告訴用戶
}
return;
}
}
}
複製代碼
上面這個流程對於大部分權限來講沒有問題,可是,若是個人應用中某個權限是必須的,上面的流程就有問題了,至於問題是什麼,咱們先看看系統的受權交互界面: 應用在第一次請求某個權限時,彈出的對話框以下:
若是用戶選擇拒絕,那麼下次在請求時,以下圖:
若是用戶不勾選,直接拒絕,那麼之後在請求時都會彈出這個帶有複選框的對話框;
若是用戶勾選了 「再也不提示」,那麼之後APP在請求權限時,並不會提示受權對話框,而是直接回調到onRequestPermissionsResult,而且結果是拒絕受權。
可悲的是API沒有提供一個接口告訴咱們用戶已經選擇了再也不詢問,那麼採起training中的流程時,若是某一個權限是必須的而被用戶勾選再也不提示,那麼這個app永遠不會執行到readContacts()方法了,並且用戶也得不到任何提示,若是我開發的是一個聯繫人APP,這不是坑爹麼?
也許你會說不是有shouldShowRequestPermissionRationale方法用來描述是否要告訴用戶咱們爲何須要這個權限麼?可是這個方法是有缺陷的,下面咱們來解釋一下各個操做之間這個函數返回值的變化:
[用戶操做序列][函數返回結果][用戶選擇]
[第一次請求][false][拒絕]--->第二次請求[true][拒絕,勾選]--->第三次請求[false][...]
[第一次請求][false][拒絕]--->第二次請求[true][拒絕,不勾選]這個操做能夠重複N次--->第N+2次請求[true][拒絕,勾選]--->第N+3次請求[false][操做]
這裏咱們能夠看到shouldShowRequestPermissionRationale方法
返回false是有二義性的,既能夠表明以前沒有請求過這個權限,也能夠表明用戶選擇了再也不詢問,可是這兩種狀況下咱們的處理邏輯確定不一致。不過這個函數若是兩次請求之間值的變化是由 true-->false,那麼必然是用戶點擊了never ask again!!
咱們能夠從Google本身家的APP找到一些靈感,好比相機應用。這裏我先把相機的權限去掉,而後我打開相機,此時會彈出對話框,詢問權限,此時若是拒絕並勾選再也不提示以後,它會直接彈出一個對話框告訴用戶去給APP添加權限,若是咱們點擊設置,會直接到相機應用的設置頁面,這就完成了對用戶進行權限設置的引導。
須要注意的是,點擊去設置以後,若是用戶在設置頁面給予了相應的權限,在返回時發現相機已經關閉了,能夠判斷點擊設置以後,相機就把本身finish()掉了。其實咱們能夠經過startActivityForResult啓動設置頁面,在設置頁面返回到onActivityResult中再去判斷相應的請求是否已經授予權限。
啓動設置頁面:
private void startAppSetting() {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
intent.setData(uri);
activity.startActivityForResult(intent, PERMISSIONS_REQUEST_READ_CONTACTS);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
//注意,這裏不須要判斷 resultCode == Activity.RESULT_OK ,由於設置頁面是不會給咱們設置結果的
//設置
if(requestCode == PERMISSIONS_REQUEST_READ_CONTACTS){
if (ContextCompat.checkSelfPermission(thisActivity,Manifest.permission.READ_CONTACTS) {
//用戶已經在設置頁面受權
readContacts();
}
}
}
複製代碼
因此問題的根本就是咱們須要知道用戶點擊了「再也不詢問」。既然shouldShowRequestPermissionRationale的false存在二義性,那麼咱們只能加入一個本地的標記來輔助區分,這個標記保存的是上一次請求時的shouldShowRequestPermissionRationale結果。
//設置標記,能夠存放到SP
private void setFlag() {
boolean flag = ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,Manifest.permission.READ_CONTACTS);
//存儲flag到sp
}
private boolean getFlag() {
//從sp中讀出flag
}
//是否須要彈出對話框
private boolean needShowGuide() {
return getFlag()
&& ! ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,Manifest.permission.READ_CONTACTS)
}
複製代碼
若是這個標記是true,而當前的結果爲false,表示這兩次請求之間用戶點擊了「再也不詢問」,此時,咱們就能夠彈出對話框
issue戳這裏
Google官方最佳實踐是這樣說的:
但若是在menifest文件中申請了"android.permission.CAMERA"權限,那麼經過Intent使用相機的時候也須要動態申請權限,具體緣由請戳上面的issue。 這是一個bug。