權(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)限:
此時(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)用:
如果用戶選擇允許,那么我們的程序可以正常走下面的拍照邏輯,如果選擇拒絕,當(dāng)然就無權(quán)使用攝像頭,功能不可用。
1.2 權(quán)限收回
一個(gè)權(quán)限被用戶允許后,還可以被收回,收回權(quán)限的用戶操作一共有兩種:
1.在應(yīng)用信息-權(quán)限設(shè)置頁面
2.直接刪除所有數(shù)據(jù)
所以,對(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上安裝效果如下:
因?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ì)話框如下:
如果用戶選擇拒絕,那么下次在請(qǐng)求時(shí),如下圖:
會(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ì)話框
用戶點(diǎn)擊“設(shè)置”時(shí),直接將用戶引導(dǎo)至APP設(shè)置頁面。
最終流程如下
發(fā)現(xiàn)一個(gè)坑
issue戳這里
Google官方最佳實(shí)踐是這樣說的:
大致意思是如果我們本身不需要直接操作攝像頭,而是通過第三方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。