1. 目標(biāo)
此文章的最終目標(biāo):讓童鞋們了解6.0動態(tài)權(quán)限的邏輯耕拷,以及學(xué)會封裝動態(tài)權(quán)限申請,實現(xiàn)'一句話申請權(quán)限'
。
2. 前言
在android6.0之前嗦随,開發(fā)者無需在java代碼里進(jìn)行危險權(quán)限
的申請,只需在AndroidManifest.xml文件里進(jìn)行權(quán)限聲明即可敬尺,但從android6.0開始枚尼,google為了用戶的安全考慮,加入了動態(tài)權(quán)限機制砂吞,危險權(quán)限
必須在代碼里申請署恍,當(dāng)然,AndroidManifest.xml文件里一樣還是要進(jìn)行聲明蜻直,別忘了這一點哦盯质。(ps:加入了動態(tài)權(quán)限,雖然對開發(fā)者而言是麻煩了一些概而,但是對整體的android環(huán)境有一定的改善呼巷,流氓軟件不能像以前那樣流氓了。)
3. 什么是危險權(quán)限
危險權(quán)限赎瑰,也可以理解成較敏感權(quán)限王悍,有哪些呢,請看下表:
group:android.permission-group.CONTACTS
permission:android.permission.WRITE_CONTACTS
permission:android.permission.GET_ACCOUNTS
permission:android.permission.READ_CONTACTS
group:android.permission-group.PHONE
permission:android.permission.READ_CALL_LOG
permission:android.permission.READ_PHONE_STATE
permission:android.permission.CALL_PHONE
permission:android.permission.WRITE_CALL_LOG
permission:android.permission.USE_SIP
permission:android.permission.PROCESS_OUTGOING_CALLS
permission:com.android.voicemail.permission.ADD_VOICEMAIL
group:android.permission-group.CALENDAR
permission:android.permission.READ_CALENDAR
permission:android.permission.WRITE_CALENDAR
group:android.permission-group.CAMERA
permission:android.permission.CAMERA
group:android.permission-group.SENSORS
permission:android.permission.BODY_SENSORS
group:android.permission-group.LOCATION
permission:android.permission.ACCESS_FINE_LOCATION
permission:android.permission.ACCESS_COARSE_LOCATION
group:android.permission-group.STORAGE
permission:android.permission.READ_EXTERNAL_STORAGE
permission:android.permission.WRITE_EXTERNAL_STORAGE
group:android.permission-group.MICROPHONE
permission:android.permission.RECORD_AUDIO
group:android.permission-group.SMS
permission:android.permission.READ_SMS
permission:android.permission.RECEIVE_WAP_PUSH
permission:android.permission.RECEIVE_MMS
permission:android.permission.RECEIVE_SMS
permission:android.permission.SEND_SMS
permission:android.permission.READ_CELL_BROADCASTS
很明顯可以看到餐曼,一個group下面帶有一個或多個permission压储,這是什么情況呢?
其實是這樣的晋辆,google把相類似的權(quán)限都分到了對應(yīng)的組里渠脉,這樣有什么好處? 僅僅是為了好看、方便記憶瓶佳?
不不芋膘,好處是:屬于同一組的權(quán)限不用重復(fù)授權(quán)。例如霸饲,如果用戶之前已經(jīng)授權(quán)過WRITE_CONTACTS
權(quán)限的話为朋,當(dāng)你申請獲取READ_CONTACTS
的時候,不需要等待用戶同意授權(quán)厚脉,而是直接返回授權(quán)成功习寸。
當(dāng)然,既然是以分組的形式進(jìn)行授權(quán)傻工,那么系統(tǒng)的授權(quán)dialog
上面提示的也會是具體的組名霞溪,而不是單個權(quán)限名孵滞。
4. 怎么申請危險權(quán)限
4.1 先看看相關(guān)的API:
// 第二個參數(shù)可以從`Manifest.permission`里取值
// 第三個參數(shù)會在`onRequestPermissionsResult`方法里回調(diào)回來
ActivityCompat.requestPermissions(final Activity activity,
final String[] permissions, final int requestCode)
調(diào)用此方法后,如果targetSdkVersion版本小于26鸯匹,或者用戶已經(jīng)授權(quán)過對應(yīng)組的權(quán)限坊饶,會直接回調(diào)activity的onRequestPermissionsResult
方法,否則會彈出系統(tǒng)對話框詢問用戶是否允許該權(quán)限組殴蓬,當(dāng)用戶點了拒絕或者同意權(quán)限匿级,也都同意會回調(diào)activity的onRequestPermissionsResult
方法。
4.2 那么染厅,重點邏輯處理似乎就是在onRequestPermissionsResult
方法里了痘绎。
- 先來看看該方法的參數(shù):
// 1. requestCode即調(diào)用ActivityCompat.requestPermissions時填的requestCode。
// 2. permissions即調(diào)用ActivityCompat.requestPermissions時填的permissions肖粮。
// 注意如果targetSdkVersion版本小于26孤页,則permissions是空數(shù)組
// 3. grantResults即權(quán)限授權(quán)結(jié)果。
// 注意如果targetSdkVersion版本小于26尿赚,則grantResults是空數(shù)組
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
- 所以散庶,只要grantResults數(shù)組的size > 0 并且 其中一個為“拒絕授權(quán)”蕉堰,則為有權(quán)限被拒絕了
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {// 拒絕權(quán)限
// 有權(quán)限被決絕凌净,執(zhí)行拒絕權(quán)限后的邏輯,例如退出界面
// xxx
return;
}
}
// 執(zhí)行同意權(quán)限后的邏輯
// xxx
- 應(yīng)用權(quán)限設(shè)置界面跳轉(zhuǎn)提醒
權(quán)限拒絕有兩種情況:1. 不勾選“不再提醒”屋讶; 2.勾選“不再提醒”冰寻。
如果用戶勾選了“不再提醒”,那么再次申請權(quán)限的時候皿渗,系統(tǒng)不會彈出授權(quán)dialog斩芭,此時我們的app應(yīng)該給予提示,否則用戶很有可能會感覺莫名其妙:怎么進(jìn)來這個界面后又退出來了乐疆。
那么問題來了划乖,怎么判斷有沒有勾選“不再提醒”
,android提供了一個API:
// 1:用戶拒絕了該權(quán)限挤土,沒有勾選"不再提醒"琴庵,此方法將返回true。
// 2:用戶拒絕了該權(quán)限仰美,有勾選"不再提醒"迷殿,此方法將返回 false。
// 3:如果用戶同意了權(quán)限咖杂,此方法返回false
boolean flag = ActivityCompat.shouldShowRequestPermissionRationale(Activity , String[]])
所以庆寺,只要在判斷出是拒絕權(quán)限的地方,判斷出shouldShowRequestPermissionRationale返回false诉字,即為需要提醒跳轉(zhuǎn)懦尝。下面貼下代碼:
// 循環(huán)判斷權(quán)限知纷,只要有一個拒絕了,則回調(diào)onReject()陵霉。 全部允許時才回調(diào)onAllow()
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {// 拒絕權(quán)限
// 對于 ActivityCompat.shouldShowRequestPermissionRationale
// 1:用戶拒絕了該權(quán)限屈扎,沒有勾選"不再提醒",此方法將返回true撩匕。
// 2:用戶拒絕了該權(quán)限鹰晨,有勾選"不再提醒",此方法將返回 false止毕。
// 3:如果用戶同意了權(quán)限模蜡,此方法返回false
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[i])) {
// 拒絕選了"不再提醒",一般提示跳轉(zhuǎn)到權(quán)限設(shè)置頁面扁凛,
// 并在dialog cancel的時候忍疾,執(zhí)行onReject();
showTipDialog(permissions[i]);
} else {
// 有權(quán)限被決絕,執(zhí)行拒絕權(quán)限后的邏輯
onReject();
}
return;
}
}
// 執(zhí)行同意權(quán)限后的邏輯
onAllow();
5. 動態(tài)請求封裝谨朝,實現(xiàn)'一句話申請權(quán)限'
其實原理很簡單卤妒,就是用一個PermissionBaseActivity父類來處理權(quán)限的申請和判斷,然后利用‘延遲實現(xiàn)’的思想字币,通過回調(diào)的方式则披,把具體的同意或拒絕授權(quán)后的業(yè)務(wù)邏輯交給子類來實現(xiàn)。還是直接貼代碼吧:
public class PermissionBaseActivity extends AppCompatActivity {
protected final String TAG = getClass().getSimpleName().replace("Activity", "Act");
protected TipDialog tipDialog;
private SparseArray<OnPermissionResultListener> listenerMap = new SparseArray<>();
/**
* 權(quán)限請求結(jié)果監(jiān)聽者
*/
public interface OnPermissionResultListener {
/**
* 權(quán)限被允許
*/
void onAllow();
/**
* 權(quán)限被拒絕
*/
void onReject();
}
/**
* 鏡像權(quán)限申請
* @param onPermissionResultListener 申請權(quán)限結(jié)果回調(diào)
*/
public void checkPermissions(final String[] permissions, OnPermissionResultListener onPermissionResultListener) {
if (Build.VERSION.SDK_INT < 23 || permissions.length == 0) {// android6.0已下不需要申請洗出,直接為"同意"
if (onPermissionResultListener != null)
onPermissionResultListener.onAllow();
} else {
int size = listenerMap.size();
if (onPermissionResultListener != null) {
listenerMap.put(size, onPermissionResultListener);
}
ActivityCompat.requestPermissions(this, permissions, size);
}
}
/**
* 顯示提示"跳轉(zhuǎn)到應(yīng)用權(quán)限設(shè)置界面"的dialog
* @param permission 具體的某個權(quán)限士复,用于展示dialog的內(nèi)容文字。
*/
private void showTipDialog(String permission, final OnPermissionResultListener onPermissionResultListener) {
if (tipDialog == null) {
tipDialog = new TipDialog(this);
}
// 確定按鈕
tipDialog.findViewById(R.id.btn_confirm).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
tipDialog.cancel();
toAppDetailSetting();
}
});
// 取消按鈕
tipDialog.findViewById(R.id.btn_cancel).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
tipDialog.cancel();
}
});
tipDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
onPermissionResultListener.onReject();
}
});
tipDialog.setTipText(PermissionUtil.getTip(permission));
tipDialog.show();
}
/**
* 跳轉(zhuǎn)系統(tǒng)的App應(yīng)用詳情頁
*/
protected void toAppDetailSetting() {
Intent localIntent = new Intent();
localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
localIntent.setData(Uri.fromParts("package", getPackageName(), null));
startActivity(localIntent);
}
@Override
protected void onDestroy() {
if (tipDialog != null) {
tipDialog.cancel();
tipDialog = null;
}
listenerMap.clear();
listenerMap = null;
super.onDestroy();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
OnPermissionResultListener onPermissionResultListener = listenerMap.get(requestCode);
if (onPermissionResultListener != null) {
listenerMap.remove(requestCode);
// 循環(huán)判斷權(quán)限翩活,只要有一個拒絕了阱洪,則回調(diào)onReject()。 全部允許時才回調(diào)onAllow()
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {// 拒絕權(quán)限
// 對于 ActivityCompat.shouldShowRequestPermissionRationale
// 1:用戶拒絕了該權(quán)限菠镇,沒有勾選"不再提醒"冗荸,此方法將返回true。
// 2:用戶拒絕了該權(quán)限利耍,有勾選"不再提醒"蚌本,此方法將返回 false。
// 3:如果用戶同意了權(quán)限堂竟,此方法返回false
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[i])) {
// 拒絕選了"不再提醒"魂毁,一般提示跳轉(zhuǎn)到權(quán)限設(shè)置頁面
showTipDialog(permissions[i], onPermissionResultListener);
} else {
onPermissionResultListener.onReject();
}
return;
}
}
onPermissionResultListener.onAllow();
}
}
}
其中用了一個listenerMap
來維護(hù)多個請求,避免如果同時有多個權(quán)限申請請求出嘹,onRequestPermissionsResult會亂套的席楚。
同時用此SparseArray的size作為requestCode,方便處理完授權(quán)判斷后税稼,從map里移除listener烦秩,減少內(nèi)存占用垮斯。
- 子類使用:
checkPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, new OnPermissionResultListener() {
@Override
public void onAllow() {
Log.d(TAG, "onAllow: ");
}
@Override
public void onReject() {
Log.d(TAG, "onReject: ");
}
});
是不是很簡潔咧,理解原理后只祠,稍微封裝下就好兜蠕,也不用去引用什么第三方框架了。
6. 額外說一下抛寝,責(zé)任單一原則
大家都知道熊杨,寫android app的時候,基本上都有一個BaseActivity盗舰,但是有些童鞋會把很多邏輯都一起寫在BaseActivity這個類里晶府,感覺像大雜燴一樣,很不美觀钻趋,后期有問題了或者需要優(yōu)化了川陆,定位不好定位。
建議蛮位,遵循責(zé)任單一原則:
權(quán)限相關(guān)的邏輯就寫在PermissionBaseActivity里较沪。
Mvp相關(guān)的綁定邏輯,就寫在MvpBaseActivity里失仁。等等
最后再去處理繼承關(guān)系就好了尸曼。
覺得有用同學(xué),點贊鼓勵下唄陶因。 個人公眾號『Grade桂』骡苞,歡迎關(guān)注