Android M 權(quán)限使用解析

權(quán)限

第三方庫:easypermissions

1.1 權(quán)限授予

在Android M(6.0)之前,如果應(yīng)用需要某個(gè)權(quán)限,我們可以在Manifest文件中指定即可

<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET" />

在安裝時(shí),安裝工具會(huì)彈出對(duì)話框告知用戶當(dāng)前安裝的應(yīng)用所需要的權(quán)限:

image

此時(shí),用戶只有兩個(gè)選擇,繼續(xù)安裝 or 直接不安裝。在應(yīng)用安裝后,用戶不能夠再去取消相應(yīng)的權(quán)限,當(dāng)然有個(gè)別廠商自帶權(quán)限管理(安全衛(wèi)士等)。

為了更加靈活地控制權(quán)限,在Android M之后,對(duì)于某些權(quán)限,需要程序動(dòng)態(tài)向用戶申請(qǐng),靜態(tài)注冊(cè)不在起作用。如我們?cè)趹?yīng)用內(nèi)調(diào)起攝像頭時(shí),我們需要自己向系統(tǒng)發(fā)出權(quán)限申請(qǐng),系統(tǒng)會(huì)彈出對(duì)話框告訴用戶這個(gè)操作需要什么權(quán)限,用戶選擇之后,系統(tǒng)再把結(jié)果返回給應(yīng)用:

image

如果用戶選擇允許,那么我們的程序可以正常走下面的拍照邏輯,如果選擇拒絕,當(dāng)然就無權(quán)使用攝像頭,功能不可用。

1.2 權(quán)限收回

一個(gè)權(quán)限被用戶允許后,還可以被收回,收回權(quán)限的用戶操作一共有兩種:

1.在應(yīng)用信息-權(quán)限設(shè)置頁面

image

2.直接刪除所有數(shù)據(jù)

image

所以,對(duì)于需要權(quán)限的操作,在使用時(shí)每次都需要判斷是否已經(jīng)授權(quán),因?yàn)橛脩艨梢噪S時(shí)收回權(quán)限。

1.3權(quán)限分類

Android對(duì)各種權(quán)限進(jìn)行了劃分,一共三類:

正常權(quán)限(查看所有正常權(quán)限)

正常權(quán)限指對(duì)用戶隱私不敏感的信息,比如我們常用的聯(lián)網(wǎng)權(quán)限 INTERNET。上圖中包含CAMERA和INTERNET權(quán)限的APK在Android M上安裝效果如下:

image

因?yàn)镮NTERNET是正常權(quán)限,所以被系統(tǒng)直接授權(quán),當(dāng)然這里就無需展示了,而CAMERA呢?它就是下面說的危險(xiǎn)權(quán)限了。

危險(xiǎn)權(quán)限(查看所有危險(xiǎn)權(quán)限)

危險(xiǎn)權(quán)限就是我們需要適配的重點(diǎn)區(qū)域了,所有的危險(xiǎn)權(quán)限都是在運(yùn)行時(shí)(需要時(shí))才會(huì)申請(qǐng),所以當(dāng)然在安裝時(shí)也無需展示了。需要注意的是,權(quán)限進(jìn)行了分組,每一組中只要有一個(gè)權(quán)限被授予了,那么組內(nèi)其它權(quán)限也會(huì)被授予。

特殊權(quán)限

SYSTEM_ALERT_WINDOW:設(shè)置懸浮窗
WRITE_SETTINGS:修改系統(tǒng)設(shè)置
這些權(quán)限在各類安全衛(wèi)士上使用較多,大部分情況下我們都不需要?;玖鞒叹褪前l(fā)一個(gè)權(quán)限申請(qǐng)給系統(tǒng)權(quán)限設(shè)置頁面,用戶授予權(quán)限之后,在onActivityResult中獲取結(jié)果。

以上基礎(chǔ)可以在這篇文章中獲得:聊一聊Android 6.0的運(yùn)行時(shí)權(quán)限

二、適配最佳實(shí)踐

2.1 適配API介紹

在Android M的SDK中,在Activity中新增了進(jìn)行運(yùn)行時(shí)權(quán)限適配的三個(gè)API:

void requestPermissions(String[] permissions, int requestCode)//請(qǐng)求權(quán)限,參數(shù)可以是一個(gè)權(quán)限或者是多個(gè)。
void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)//請(qǐng)求權(quán)限之后的回調(diào)。
boolean shouldShowRequestPermissionRationale(String permission)//是否有必要告訴用戶我們需要這個(gè)權(quán)限的原因。

Context中添加了一個(gè)API:

int checkSelfPermission(String permission)//用來檢測(cè)當(dāng)前應(yīng)用是否具有某個(gè)權(quán)限。

由于這些API都是Android M以上版本才有,為了避免我們?cè)诖a里面引入過多的版本判斷,support包23版本中添加了個(gè)對(duì)應(yīng)的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)

2.2基本流程

2.2.1官方版本

官方training中有個(gè)例子,以應(yīng)用獲取權(quán)限READ_CONTACTS為例,在獲取權(quán)限之后,我們要讀取手機(jī)的聯(lián)系人列表操作:readContacts()。

// 檢查是否已經(jīng)具有權(quán)限
if (ContextCompat.checkSelfPermission(thisActivity,Manifest.permission.READ_CONTACTS)
    != PackageManager.PERMISSION_GRANTED) {
    // 是否需要告訴用戶我們?yōu)槭裁葱枰@個(gè)權(quán)限
    if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
     Manifest.permission.READ_CONTACTS)) {
     //彈出信息,告訴用戶我們?yōu)樯缎枰獧?quán)限

    } else {
    //直接獲取權(quán)限
    ActivityCompat.requestPermissions(thisActivity,
            new String[]{Manifest.permission.READ_CONTACTS},
            MY_PERMISSIONS_REQUEST_READ_CONTACTS);
    //用戶授權(quán)的結(jié)果會(huì)回調(diào)到FragmentActivity的onRequestPermissionsResult
    }
}else {
 //已經(jīng)擁有授權(quán)
 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 {
         //權(quán)限沒能授權(quán)通過,可以考慮彈個(gè)toast告訴用戶
        }
        return;
    }
  }
}

2.2.2 一個(gè)權(quán)限是必須的?

上面這個(gè)流程對(duì)于大部分權(quán)限來說沒有問題,但是,如果我的應(yīng)用中某個(gè)權(quán)限是必須的,上面的流程就有問題了,至于問題是什么,我們先看看系統(tǒng)的授權(quán)交互界面:
應(yīng)用在第一次請(qǐng)求某個(gè)權(quán)限時(shí),彈出的對(duì)話框如下:

image

如果用戶選擇拒絕,那么下次在請(qǐng)求時(shí),如下圖:

image

會(huì)多一個(gè) “再不提示”復(fù)選框 的對(duì)話框。

  • 如果用戶不勾選,直接拒絕,那么以后在請(qǐng)求時(shí)都會(huì)彈出這個(gè)帶有復(fù)選框的對(duì)話框;

  • 如果用戶勾選了 “不再提示”,那么以后APP在請(qǐng)求權(quán)限時(shí),并不會(huì)提示授權(quán)對(duì)話框,而是直接回調(diào)到onRequestPermissionsResult,并且結(jié)果是拒絕授權(quán)。

可悲的是API沒有提供一個(gè)接口告訴我們用戶已經(jīng)選擇了不再詢問,那么采取training中的流程時(shí),如果某一個(gè)權(quán)限是必須的而被用戶勾選不再提示,那么這個(gè)app永遠(yuǎn)不會(huì)執(zhí)行到readContacts()方法了,而且用戶也得不到任何提示,如果我開發(fā)的是一個(gè)聯(lián)系人APP,這不是坑爹么?

也許你會(huì)說不是有shouldShowRequestPermissionRationale方法用來描述是否要告訴用戶我們?yōu)槭裁葱枰@個(gè)權(quán)限么?但是這個(gè)方法是有缺陷的,下面我們來解釋一下各個(gè)操作之間這個(gè)函數(shù)返回值的變化:

[用戶操作序列][函數(shù)返回結(jié)果][用戶選擇]

  • [第一次請(qǐng)求][false][拒絕]--->第二次請(qǐng)求[true][拒絕,勾選]--->第三次請(qǐng)求[false][...]

  • [第一次請(qǐng)求][false][拒絕]--->第二次請(qǐng)求[true][拒絕,不勾選]這個(gè)操作可以重復(fù)N次--->第N+2次請(qǐng)求[true][拒絕,勾選]--->第N+3次請(qǐng)求[false][操作]

這里我們可以看到shouldShowRequestPermissionRationale方法返回false是有二義性的,既可以代表之前沒有請(qǐng)求過這個(gè)權(quán)限,也可以代表用戶選擇了不再詢問,但是這兩種情況下我們的處理邏輯肯定不一致。不過這個(gè)函數(shù)如果兩次請(qǐng)求之間值的變化是由 true-->false,那么必然是用戶點(diǎn)擊了never ask again!!

2.2.3 最佳流程

我們可以從Google自己家的APP找到一些靈感,比如相機(jī)應(yīng)用。這里我先把相機(jī)的權(quán)限去掉,然后我打開相機(jī),此時(shí)會(huì)彈出對(duì)話框,詢問權(quán)限,此時(shí)如果拒絕并勾選不再提示之后,它會(huì)直接彈出一個(gè)對(duì)話框告訴用戶去給APP添加權(quán)限,如果我們點(diǎn)擊設(shè)置,會(huì)直接到相機(jī)應(yīng)用的設(shè)置頁面,這就完成了對(duì)用戶進(jìn)行權(quán)限設(shè)置的引導(dǎo)。

需要注意的是,點(diǎn)擊去設(shè)置之后,如果用戶在設(shè)置頁面給予了相應(yīng)的權(quán)限,在返回時(shí)發(fā)現(xiàn)相機(jī)已經(jīng)關(guān)閉了,可以判斷點(diǎn)擊設(shè)置之后,相機(jī)就把自己finish()掉了。其實(shí)我們可以通過startActivityForResult啟動(dòng)設(shè)置頁面,在設(shè)置頁面返回到onActivityResult中再去判斷相應(yīng)的請(qǐng)求是否已經(jīng)授予權(quán)限。

啟動(dòng)設(shè)置頁面:

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 ,因?yàn)樵O(shè)置頁面是不會(huì)給我們?cè)O(shè)置結(jié)果的
    //設(shè)置
    if(requestCode == PERMISSIONS_REQUEST_READ_CONTACTS){
       if (ContextCompat.checkSelfPermission(thisActivity,Manifest.permission.READ_CONTACTS) {
            //用戶已經(jīng)在設(shè)置頁面授權(quán)
            readContacts();
        }
    }

}

所以問題的根本就是我們需要知道用戶點(diǎn)擊了“不再詢問”。既然shouldShowRequestPermissionRationale的false存在二義性,那么我們只能加入一個(gè)本地的標(biāo)記來輔助區(qū)分,這個(gè)標(biāo)記保存的是上一次請(qǐng)求時(shí)的shouldShowRequestPermissionRationale結(jié)果。

//設(shè)置標(biāo)記,可以存放到SP
private void setFlag() {
  boolean flag = ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,Manifest.permission.READ_CONTACTS);
  //存儲(chǔ)flag到sp
}
private boolean getFlag() {
  //從sp中讀出flag
}

//是否需要彈出對(duì)話框
private boolean needShowGuide() {
  return getFlag() 
            && ! ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,Manifest.permission.READ_CONTACTS)
}

如果這個(gè)標(biāo)記是true,而當(dāng)前的結(jié)果為false,表示這兩次請(qǐng)求之間用戶點(diǎn)擊了“不再詢問”,此時(shí),我們就可以彈出對(duì)話框

image

用戶點(diǎn)擊“設(shè)置”時(shí),直接將用戶引導(dǎo)至APP設(shè)置頁面。

最終流程如下

image

發(fā)現(xiàn)一個(gè)坑

issue戳這里
Google官方最佳實(shí)踐是這樣說的:

image

大致意思是如果我們本身不需要直接操作攝像頭,而是通過第三方SDK【如相冊(cè)】使用攝像頭,是不需要去獲取權(quán)限的。

但如果在menifest文件中申請(qǐng)了"android.permission.CAMERA"權(quán)限,那么通過Intent使用相機(jī)的時(shí)候也需要?jiǎng)討B(tài)申請(qǐng)權(quán)限,具體原因請(qǐng)戳上面的issue。 這是一個(gè)bug。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容