引言
Android系統(tǒng)雖然開源平绩,但是相對(duì)還是比較安全的臼勉,尤其是高版本的系統(tǒng),這得益于Android系統(tǒng)自身的安全機(jī)制晤斩,其中權(quán)限管理機(jī)制一直是首要的安全概念焕檬,Android 動(dòng)態(tài)權(quán)限又叫運(yùn)行時(shí)權(quán)限已經(jīng)面世很久了,網(wǎng)上很多文章都是只寫了用法澳泵,不客氣地說只是告訴了怎么用实愚,具體的機(jī)制并沒有很完整,讓一些初學(xué)者只知其然而不知其所然兔辅,對(duì)于動(dòng)態(tài)權(quán)限并沒有完全掌握腊敲,于是我就想結(jié)合自己的項(xiàng)目經(jīng)驗(yàn)和官方的文檔,一篇文章把重要關(guān)于動(dòng)態(tài)的知識(shí)點(diǎn)都總結(jié)出來幢妄,當(dāng)然不是所有的兔仰,比如說權(quán)限組的實(shí)際操作等等。
一蕉鸳、Android系統(tǒng)權(quán)限機(jī)制概述
我們知道在Android的權(quán)限系統(tǒng)一直是首要的安全概念乎赴,因?yàn)檫@些權(quán)限在Android M(6.0)之前在AndroidManifest文件中聲明之后忍法,僅App在安裝的時(shí)候被詢問一次,安裝成功之后運(yùn)行榕吼,就可以在用戶毫不知曉的情況下訪問權(quán)限內(nèi)的內(nèi)容饿序,毫無顧忌地收集信息(雖然現(xiàn)在也還是可以在一次申請(qǐng)之后無顧忌的使用)。而在Android M之后羹蚣,app將不會(huì)在安裝的時(shí)候授予權(quán)限原探。App不得不在運(yùn)行時(shí)一個(gè)一個(gè)詢問用戶授予權(quán)限,系統(tǒng)權(quán)限被按敏感級(jí)別分組(normal顽素、signature咽弦、dangerous、privileged胁出、signature|privileged)并且敏感權(quán)限必須在運(yùn)行的時(shí)候動(dòng)態(tài)申請(qǐng)型型,并且隨時(shí)可以在設(shè)置了取消已經(jīng)授權(quán)的權(quán)限,又可以為兩大類:普通權(quán)限和危險(xiǎn)權(quán)限全蝶,在M手機(jī)上闹蒜,對(duì)于敏感權(quán)限,需要在程序運(yùn)行時(shí)進(jìn)行動(dòng)態(tài)申請(qǐng)抑淫。對(duì)于非敏感權(quán)限绷落,即Normal Permissions,和M之前的使用相同始苇。如果APP是在Android 5.1 或更低版本的設(shè)備上運(yùn)行砌烁,或者APP的targetSdkVersion為 22 或更低時(shí),在清單中列出了危險(xiǎn)權(quán)限埂蕊,則用戶必須在安裝應(yīng)用時(shí)授予此權(quán)限往弓,若不授予此權(quán)限,系統(tǒng)則不會(huì)安裝蓄氧。而APP運(yùn)行在 Android 6.0 或更高版本的設(shè)備上,或者應(yīng)用的目標(biāo) SDK 為 23 或更高槐脏,應(yīng)用必須在清單中列出所有權(quán)限喉童,并且它必須在運(yùn)行時(shí)動(dòng)態(tài)申請(qǐng)危險(xiǎn)權(quán)限,用戶隨時(shí)可以授予或拒絕每項(xiàng)權(quán)限顿天。值得注意的是:從 Android 6.0(API 級(jí)別 23)開始堂氯,用戶可以隨時(shí)從任意應(yīng)用調(diào)用權(quán)限,即使應(yīng)用面向較低的 API 級(jí)別也可以調(diào)用牌废,所以無論APP面向哪個(gè) API 級(jí)別咽白,都應(yīng)對(duì)應(yīng)用進(jìn)行測(cè)試,以驗(yàn)證它在缺少需要的權(quán)限時(shí)行為是否正常鸟缕。
二晶框、Android M權(quán)限機(jī)制
1排抬、Normal級(jí)別的權(quán)限只需要在AndroidManifest中聲明就好,安裝時(shí)就授權(quán)授段,不需要每次使用時(shí)都檢查權(quán)限蹲蒲,而且用戶不能取消以上授權(quán)
2、其他級(jí)別的權(quán)限在編譯在Android M(即targetSdkVersion大于等于23時(shí)候)版本時(shí)候侵贵,不僅需要在AndroidManifest中聲明届搁,還得在運(yùn)行的時(shí)候需要?jiǎng)討B(tài)申請(qǐng),而且還可以隨時(shí)取消授權(quán)窍育。
- 先在AndroidManifest中聲明
- 再在運(yùn)行的時(shí)候動(dòng)態(tài)申請(qǐng)
3卡睦、Android6.0之后權(quán)限組管理
同一組的任何一個(gè)權(quán)限被授權(quán)了,其他權(quán)限也自動(dòng)被授權(quán)漱抓。比如說一旦WRITE_CONTACTS被授權(quán)了表锻,app也有READ_CONTACTS和GET_ACCOUNTS了。在api23中通過Activity的checkSelfPermission和requestPermissions來檢查和請(qǐng)求權(quán)限的方法辽旋。(或者可以使用( compile 'com.android.support:support-v4:25.2.0')v4庫中的ContextCompat.checkSelfPermission()浩嫌、ActivityCompat.requestPermissions())
危險(xiǎn)權(quán)限實(shí)際上才是運(yùn)行時(shí)權(quán)限主要處理的對(duì)象,這些權(quán)限可能引起隱私問題或者影響其他程序運(yùn)行补胚。Android中的危險(xiǎn)權(quán)限可以歸為以下幾個(gè)分組:
三码耐、運(yùn)行時(shí)申請(qǐng)權(quán)限
因?yàn)?.0權(quán)限授權(quán)的改變,即使你在Manifest中加入溶其,有些權(quán)限依然需要獲得用戶的手動(dòng)授權(quán)骚腥,但是這一機(jī)制——運(yùn)行時(shí)權(quán)限僅僅是在我們我們?cè)O(shè)置targetSdkVersion 大于等于23時(shí)且運(yùn)行在M系統(tǒng)以上的設(shè)備上才起作用(即你想要你的app支持這一新特性你得設(shè)置compileSdkVersion 和targetSdkVersion 設(shè)為大于等于23),所以如果希望app在6.0之前的設(shè)備依然使用舊的權(quán)限系統(tǒng)瓶逃,只需要把targetSdkVersion設(shè)置為23以下即可束铭,還有一點(diǎn)使用Android studio新建項(xiàng)目,targetSdkVersion 會(huì)自動(dòng)設(shè)置為 23厢绝,如果你還沒支持新運(yùn)行時(shí)權(quán)限契沫,個(gè)人建議先把targetSdkVersion 降級(jí)到22,在M以后昔汉,敏感權(quán)限默認(rèn)的值是每次詢問懈万,而且shouldShowRequestPermissionRationale()返回值機(jī)制手機(jī)系統(tǒng)不同。
- 第一靶病、在AndroidManifest聲明所需的權(quán)限
- 第二会通、使用相應(yīng)的api方法動(dòng)態(tài)申請(qǐng)
1、使用系統(tǒng)提供的API
動(dòng)態(tài)權(quán)限的核心工作流程:checkSelfPermission檢查是否已被授予——>requestPermissions申請(qǐng)權(quán)限——>自動(dòng)回調(diào)onRequestPermissionsResult——shouldShowRequestPermissionRationale娄周。無論什么框架變出花來都離不開這個(gè)基本的流程涕侈。
| API方法| 說明|
|: ------------- |:------------|
| int checkSelfPermission(@NonNull Context context, @NonNull String permission) |可用activity.checkSelfPermission或者v4包下的 ContextCompat.checkSelfPermission來檢查權(quán)限是否已授權(quán) |
| void requestPermissions(final @NonNull Activity activity,final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode) | 可用activity.requestPermissions或者v4包下的 ContextCompat.requestPermissions來進(jìn)行權(quán)限申請(qǐng)。需要為每一個(gè)權(quán)限指定一個(gè)id煤辨,當(dāng)系統(tǒng)返回授權(quán)結(jié)果時(shí)裳涛,應(yīng)用根據(jù)id拿到授權(quán)結(jié)果木张。|
| void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,@NonNull int[] grantResults) | 由系統(tǒng)自動(dòng)觸發(fā),當(dāng)應(yīng)用申請(qǐng)權(quán)限后调违,Activity將觸發(fā)這個(gè)回調(diào)窟哺,告訴應(yīng)用用戶的授權(quán)結(jié)果。 |
|boolean shouldShowRequestPermissionRationale(@NonNull Activity activity, @NonNull String permission) |當(dāng)應(yīng)用首次申請(qǐng)權(quán)限時(shí)技肩,如果用戶點(diǎn)擊拒絕且轨,下次再申請(qǐng)權(quán)限,Android允許你提示用戶虚婿,你為什么需要這個(gè)權(quán)限旋奢,更好引導(dǎo)用戶是否授權(quán),其中在Android原生系統(tǒng)中:如果應(yīng)用之前請(qǐng)求過此權(quán)限但用戶拒絕了請(qǐng)求然痊,此方法將返回true至朗;如果用戶在過去拒絕了權(quán)限請(qǐng)求且在權(quán)限請(qǐng)求系統(tǒng)對(duì)話框中選擇了 Don't ask again 選項(xiàng)將返回 false;如果第一次申請(qǐng)權(quán)限也返回false剧浸;如果設(shè)備規(guī)范禁止應(yīng)用具有該權(quán)限锹引,此方法也會(huì)返回 false,返回false則不在顯示提示對(duì)話框唆香,返回true則回顯示對(duì)話框(但并不一定任何系統(tǒng)的機(jī)制都是如此嫌变,不同廠商不同的Rom機(jī)制有可能不同,以HTC 6.0和聯(lián)想的系統(tǒng)為例躬它,在HTC上就不一定是返回true腾啥,目前測(cè)試結(jié)果一直都是false,而聯(lián)想手機(jī)上則和原生的機(jī)制一樣)|
public class MainActivity extends Activity implements View.OnClickListener {
public static final int REQUEST_PERMISSION_CALL = 100;
private static final String CALL_PHONE = Manifest.permission.CALL_PHONE;
private static final String TAG = "Permission";
private Button btnCheck, btnShow, btnCall;
private Intent callIntent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
private void init() {
initViews();
callIntent = new Intent(Intent.ACTION_CALL);
Uri uri = Uri.parse("tel:" + "10086");
callIntent.setData(uri);
}
private void initViews() {
btnCheck = (Button) findViewById(R.id.btn_check);
btnShow = (Button) findViewById(R.id.btn_showtip);
btnCall = (Button) findViewById(R.id.btn_call);
btnCheck.setOnClickListener(this);
btnShow.setOnClickListener(this);
btnCall.setOnClickListener(this);
}
private boolean checkPermission(String permission) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this, CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
Log.e("checkPermission", "PERMISSION_GRANTED" + ContextCompat.checkSelfPermission(this, CALL_PHONE));
return true;
} else {
Log.e("checkPermission", "PERMISSION_DENIED" + ContextCompat.checkSelfPermission(this, CALL_PHONE));
return false;
}
} else {
Log.e("checkPermission", "M以下" + ContextCompat.checkSelfPermission(this, CALL_PHONE));
return true;
}
}
private void startRequestPermision(String permission) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{android.Manifest.permission.CALL_PHONE}, REQUEST_PERMISSION_CALL);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == REQUEST_PERMISSION_CALL) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startActivity(callIntent);
} else {
//如果拒絕授予權(quán)限,且勾選了再也不提醒
if (!shouldShowRequestPermissionRationale(permissions[0])) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("說明")
.setMessage("需要使用電話權(quán)限冯吓,進(jìn)行電話測(cè)試")
.setPositiveButton("確定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
showTipGoSetting();
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
return;
}
})
.create()
.show();
} else {
showTipGoSetting();
}
}
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
public void onClick(View v) {
int viewId = v.getId();
switch (viewId) {
case R.id.btn_check:
int isGrantd = ContextCompat.checkSelfPermission(getApplicationContext(), CALL_PHONE);
Toast.makeText(MainActivity.this, "isGrantd" + isGrantd, Toast.LENGTH_SHORT).show();
break;
case R.id.btn_showtip:
boolean isShow = ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, CALL_PHONE);
Toast.makeText(MainActivity.this, "isShow" + isShow, Toast.LENGTH_SHORT).show();
break;
case R.id.btn_call:
call();
break;
default:
break;
}
}
private void call() {
if (checkPermission(CALL_PHONE)) {
startActivity(callIntent);
} else {
startRequestPermision(CALL_PHONE);
}
}
/**
* 用于在用戶勾選“不再提示”并且拒絕時(shí)倘待,再次提示用戶
*/
private void showTipGoSetting() {
new AlertDialog.Builder(this)
.setTitle("電話權(quán)限不可用")
.setMessage("請(qǐng)?jiān)?應(yīng)用設(shè)置-權(quán)限-中,允許APP使用電話權(quán)限來測(cè)試")
.setPositiveButton("立即開啟", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 跳轉(zhuǎn)到應(yīng)用設(shè)置界面
goToAppSetting();
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
}).setCancelable(false).show();
}
/**
* 打開Setting
*/
private void goToAppSetting() {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
startActivityForResult(intent, 123);
}
}
2组贺、使用第三方開源框架
目前用得比較多的動(dòng)態(tài)權(quán)限第三方庫PermissionsDispatcher凸舵,核心也是依然離不開系統(tǒng)的API,PermissionsDispatcher采用注解失尖,源碼很簡(jiǎn)單就不單獨(dú)分析了贞间,主要就是在編譯時(shí)生成代理類(代理類名稱格式為XxxxPermissionsDispatcher,其中Xxxx為@RuntimePermissions標(biāo)注的Activity或Fragment類名稱)雹仿,在Activity/Fragment中通過代理類去完成權(quán)限的檢查工作,并且把系統(tǒng)的權(quán)限處理回調(diào)回傳到代理類內(nèi)部整以,進(jìn)而完成觸發(fā)內(nèi)部的回調(diào)胧辽,在效率上和官方差不多,唯一的區(qū)別在于調(diào)用的形式:由于采用代理的形式公黑,不是直接調(diào)用系統(tǒng)API的checkSelfPermission來檢查權(quán)限邑商,取而代之的是代理類里的XxxWithCheck的方法(其中Xxx代表被@NeedsPermission標(biāo)注的方法名)摄咆;處理權(quán)限申請(qǐng)的結(jié)果依然是在系統(tǒng)的onRequestPermissionsResult方法內(nèi),但是我們必須把回調(diào)結(jié)果回傳到代理類人断,所以還必須用代理類調(diào)用他對(duì)應(yīng)的onRequestPermissionsResult方法
注解名稱 | 是否必須 | 說明 |
---|---|---|
@RuntimePermissions | ? | 用于表面該Activity or Fragment 使用動(dòng)態(tài)代理來管理權(quán)限 |
@NeedsPermission | ? | 用于表明在什么時(shí)候需要權(quán)限吭从,一般用在方法聲明上 |
@OnShowRationale | 應(yīng)用首次申請(qǐng)權(quán)限被拒絕,再次申請(qǐng)權(quán)限時(shí)恶迈,給出提示信息涩金,自動(dòng)回調(diào)標(biāo)注有@OnShowRationale的方法 | |
@OnPermissionDenied | 應(yīng)用首次申請(qǐng)權(quán)限,用戶拒絕暇仲,使用@OnPermissionDenied標(biāo)識(shí)的方法將作為回調(diào): | |
@OnNeverAskAgain | 應(yīng)用非首次申請(qǐng)權(quán)限時(shí)步做,授權(quán)對(duì)話框會(huì)多出一個(gè)復(fù)選框不再詢問,系統(tǒng)自動(dòng)回調(diào)標(biāo)注有@OnNeverAskAgain注解的方法 |
- 在Module下build.gradle引入依賴
dependencies {
compile 'com.github.hotchemi:permissionsdispatcher:2.3.2'
annotationProcessor 'com.github.hotchemi:permissionsdispatcher-processor:2.3.2'//java8不能使用apt
}
- 在需要?jiǎng)討B(tài)權(quán)限的Activity或者Fragment加上注解RuntimePermissions
在涉及到動(dòng)態(tài)權(quán)限的方法加上注解@NeedsPermission
在需要申請(qǐng)動(dòng)態(tài)權(quán)限的方法處奈附,使用代理類的XxxWithCheck方法開啟動(dòng)態(tài)權(quán)限申請(qǐng)的第一步
重寫權(quán)限處理回調(diào)方法onRequestPermissionsResult全度,并且通過回傳到代理類內(nèi)部的onRequestPermissionsResult方法
然后根據(jù)各自的業(yè)務(wù)需求,使用@OnShowRationale斥滤、@OnPermissionDenied将鸵、@OnNeverAskAgain來標(biāo)注對(duì)應(yīng)的回調(diào)方法并處理