简单粗暴的来说一下,在 Android 6.0 即 API 23 之前,我们用到的权限只需要在 AndroidManifest 文件中统一进行申请就可以了,而且用户不可取消!
但是在 6.0 及以上,出于安全性考虑,开始由用户掌握了部分主动权!
Android 中的系统权限可以分为 normal 和 dangerous 两种。
normal 这类的权限不太会直接威胁到用户的隐私,只需要在 Manifest 文件中注册即可;
dnagerous 这类的权限使得 app 可以直接访问用户的一些隐私数据,从 Android 6.0 开始,这些权限就不仅仅只需要在 Manifest 文件中声明,而且需要在运行且适当的时候进行动态的申请,由用户决定是否授权,并且用户可以随时取消这些权限(在设置里的应用详细信息界面取消)。如果不进行权限检查而贸然执行一些“危险”的动作的话,应用可能是会崩溃的。
这里只给出 dangerous 类的权限列表:
权限组 | 组内权限 |
---|---|
CALENDAR | READ_CALENDAR WRITE_CALENDAR |
CAMERA | CAMERA |
CONTACTS | READ_CONTACTS WRITE_CONTACTS GET_ACCOUNTS |
LOCATION | ACCESS_FINE_LOCATION ACCESS_COARSE_LOCATION |
MICROPHONE | RECORD_AUDIO |
PHONE | READ_PHONE_STATE CALL_PHONE READ_CALL_LOG WRITE_CALL_LOG ADD_VOICEMAIL USE_SIP PROCESS_OUTGOING_CALLS |
SENSORS | BODY_SENSORS |
SMS | SEND_SMS RECEIVE_SMS READ_SMS RECEIVE_WAP_PUSH RECEIVE_MMS |
STORAGE | READ_EXTERNAL_STORAGE WRITE_EXTERNAL_STORAGE |
如上表,权限被分组了,值得一提的是 同一组中的任何一个权限被授权了,其他权限也就自动被授权了。
申请授权一般需要三步:
好在 Android 提供了相应的 API 使得我们开发便利许多,下面以申请 “读取联系人权限” 为例展开这三个步骤。
第一步,检查权限是否已经授权:
//读取联系人的权限
String permission = Manifest.permission.READ_CONTACTS;
//检查权限是否已授权
int hasPermission = checkSelfPermission(permission);
只需要调用 checkSelfPermission(String permission) 方法,参数为 权限字符串。
如果是已授权的权限,该方法返回结果是 PackageManager.PERMISSION_GRANTED 常量为 0,
如果是未授权的权限,该方法返回结果是 PackageManager.PERMISSION_DENIED 常量为 -1。
第二步,对未授权权限 申请用户授权
//读取联系人的权限
String permission = Manifest.permission.READ_CONTACTS;
//检查权限是否已授权
int hasPermission = checkSelfPermission(permission);
//如果没有授权
if (hasPermission != PackageManager.PERMISSION_GRANTED) {
//请求权限,此方法会弹出权限请求对话框,让用户授权,并回调 onRequestPermissionsResult 来告知授权结果
requestPermissions(new String[]{permission}, REQUEST_CODE_ASK_SINGLE_PERMISSION);
}else {//已经授权过
//做一些你想做的事情,即原来不需要动态授权时做的操作
doSomething();
}
需要调用 requestPermissions(@NonNull String[] permissions, int requestCode) 方法,该方法需要传入两个参数:
参数1:String[] permissions 是一个字符串数组,即要申请的权限的字符串数组,同时也说明了,可以一次性申请多个权限,用户会一个一个进行授权
参数2:int requestCode 是一个 int 常量,此处为代表请求码,在第三步中可能会用到
注意: requestPermissions 这个方法是异步执行的
第三步,处理授权结果
//回调方法
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {//根据请求码判断是哪一次申请的权限
case REQUEST_CODE_ASK_SINGLE_PERMISSION:
if (grantResults.length > 0) {//grantResults 数组中存放的是授权结果
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {//同意授权
//授权后做一些你想做的事情,即原来不需要动态授权时做的操作
doSomething();
}else {//用户拒绝授权
//可以简单提示用户
Toast.makeText(RuntimePermissionDemo.this, "没有授权继续操作", Toast.LENGTH_SHORT).show();
}
}
break;
case REQUEST_CODE_ASK_MUTI_PERMISSIONS:
break;
default: super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
焦点访谈 用动图说话
通过上图,我们至少可以看到以下几点:
第一次点击检查权限,会弹出权限申请对话框,告知用户申请什么权限,用户可以选择拒绝(DENY)或者同意(ALLOW),第一次先拒绝,会弹吐司提示;
第二次点击检查权限,也会弹出权限申请对话框,但是多了一个 “不再询问(Never ask again)”,这一次还是点拒绝,还是会弹吐司提示;
第三次点击检查权限,对话框同上,但这次选择同意,同意后我的操作只是在界面上显示一句话。
第四次点击检查权限,就不会再弹出对话框了,因为已经授权过了。
但是,如果在第二次点击的时候,选了 “不再询问”,并且 “拒绝” 授权,那么情况会怎样?
上图可以看到不再询问后,就不再弹窗授权,而直接未授权转到回调方法中,这样就不能保证用户体验了
所以,我们总得想办法,在系统不提示弹窗授权的情况下,为用户提供别的授权渠道 —— 跳转到应用设置界面,手动打开某些权限
但是,问题是我们怎么判断用户选了 “不再提醒” 呢?这时候可能就需要使用 shouldShowRequestPermissionRationale(String permission)
这个方法了,需要注意的是这个方法在不同的情况下,返回值是不同的:
在第一次请求权限前 即 第一次调用 requestPermissions(…) 方法前,调用会返回 false
第一次请求权限时,用户拒绝了后,再调用返回 true
第二次请求权限时,用户拒绝了,并选择了 “不再提醒” 的选项,再调用返回 false,
如果不选择 “不再提醒” ,再调用还是返回 true
手动在应用设置界面取消权限, 再调用返回 true
如果已经授权再调用返回 false
对于这个方法什么时候使用,返回值怎么使用,网上也是说法不一,也确实需要仔细的推敲各种复杂情况,但我认为终归的目的还是:在没有权限操作的时候,要为用户提供开启权限的方法。
这里我只给出个人认为比较合适的使用方式:我认为在请求权限回调方法 即 onRequestPermissionsResult(…) 中使用比较合理,下面是修改后的回调方法
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_ASK_SINGLE_PERMISSION:
if (grantResults.length > 0) {//grantResults 数组中存放的是授权结果
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {//同意授权
//授权后做一些你想做的事情,即原来不需要动态授权时做的操作
doSomething();
}else {//用户拒绝授权
//可以简单提示用户
Toast.makeText(RuntimePermissionDemo.this, "没有授权继续操作", Toast.LENGTH_SHORT).show();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
//调用 shouldShowRequestPermissionRationale() 方法
boolean shouldShow = shouldShowRequestPermissionRationale(permissions[0]);
if (!shouldShow) {// shouldShow = false
//需要弹出自定义对话框,引导用户去应用的设置界面手动开启权限
showMissingPermissionDialog();
}
}
}
}
break;
case REQUEST_CODE_ASK_MUTI_PERMISSIONS:
break;
default: super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
自定义对话框,引导用户去应用设置界面进行授权
/**
* 需要手动开启缺失的权限对话框
*/
private void showMissingPermissionDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("提示");
builder.setMessage("当前应用权限不足。\n\n可点击\"设置\"-\"权限\"-打开所需权限。\n\n最后点击两次后退按钮,即可返回。");
builder.setNegativeButton("知道了", null);
builder.setPositiveButton("设置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startAppSettings();
}
});
builder.setCancelable(false);
builder.show();
}
/**
* 启动应用的设置 来手动开启权限
*/
private void startAppSettings() {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
}
修改后的代码应该可以满足我能想到所有的情况:
如果第一次申请授权,系统申请授权对话框会出现,如果用户拒绝,那么可以什么都不做
接着第二次申请授权,系统授权对话框仍会出现,而且有 “不再提示” 选项,
如果用户只拒绝授权,不选择 “不再提示” ,那么还是可以什么都不做
如果用户既拒绝授权,又选择 “不再提示” ,那么就可弹出自定义提示框,引导用户去设置开启权限
用户选择 “不再提示” 后再申请授权,系统授权对话框不会再出现,这时自定义提示框会就派上用场了,可引导用户去开启权限
如果已授权,那么就可直接进行操作了
如果已授权,但是用户在应用设置界面手动取消权限,这时再申请授权,系统对话框会提示,剩下的就和情况 2 是一样的了
效果图:
p.s. 顺带补充一下调用 requestPermissions(…) 弹出系统授权对话框的情况
即使已经授权,再调用也会弹出对话框
手动在设置界面取消权限,再调用也会弹对话框
只要没有选择 “不再提醒” 就会弹出对话框,如果选了,那么不弹窗,回调结果是拒绝权限
同时申请多个权限和申请单个权限的步骤是一样的,可先检查去除某些已授权过的,不重复申请,然后系统会在弹窗中依次展示要申请的权限,用户都选择后,结果也可在回调方法中处理
申请授权代码如下:
//申请 读联系人、读短信、获取定位的权限
String[] mutiPermissions = new String[]{Manifest.permission.READ_CONTACTS,Manifest.permission.READ_SMS,Manifest.permission.ACCESS_FINE_LOCATION};
//需要请求授权的权限
ArrayList<String> needRequest = new ArrayList<>();
//遍历 过滤已授权的权限,防止重复申请
for (String permission : mutiPermissions) {
int check = checkSelfPermission(permission);
if (check != PackageManager.PERMISSION_GRANTED) {
needRequest.add(permission);
Log.d("RuntimePermissionDemo","needCheck: " + permission);
}
}
//如果没有全部授权
if (needRequest.size() > 0) {
//请求权限,此方法异步执行,会弹出权限请求对话框,让用户授权,并回调 onRequestPermissionsResult 来告知授权结果
requestPermissions(needRequest.toArray(new String[needRequest.size()]), REQUEST_CODE_ASK_MUTI_PERMISSIONS);
}else {//已经全部授权过
//做一些你想做的事情,即原来不需要动态授权时做的操作
doSomething();
}
回调处理代码:
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_ASK_SINGLE_PERMISSION://请求单个权限
//...
break;
case REQUEST_CODE_ASK_MUTI_PERMISSIONS://请求多个权限
if (grantResults.length > 0) {
//被拒绝的权限列表
ArrayList deniedPermissions = new ArrayList<>();
for (int i=0; iif (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
deniedPermissions.add(permissions[i]);
Log.d("RuntimePermissionDemo", "Denied Permission: " + permissions[i]);
}
}
if (deniedPermissions.size() <= 0) {//已全部授权
doSomething();
}else {//没有全部授权
Toast.makeText(RuntimePermissionDemo.this, "缺少部分权限", Toast.LENGTH_SHORT).show();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
//需要引导用户手动开启的权限列表
ArrayList needShow = new ArrayList<>();
//从没有授权的权限中判断是否需要引导用户
for (int i=0; iif (!shouldShowRequestPermissionRationale(permission)) {
needShow.add(permission);
Log.d("RuntimePermissionDemo", "needShow: " + permission);
}
}
//需要引导用户
if (needShow.size() > 0) {
//需要弹出自定义对话框,引导用户去应用的设置界面手动开启权限
showMissingPermissionDialog();
}
}
}
}
break;
default: super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
需要说明的是,可能你一次申请的多个权限,如果只有部分满足,也不影响应用的使用,这个就需要结合具体应用需求由开发者自行判断了,我这里给出的是都授权才能继续执行的示例。
效果图:
既然申请权限是这么繁琐的的工作,那么肯定是需要好好封装一下的,然而我这种水平… 啧~ 封装的也就我自己能用罢了,我始终相信,肯定有大牛已开源了封装好了的第三方库,不信你在 GitHub 上 搜一下 “android permission” 试试!
虽然 GitHub 上已有了第三方库方便开发,但我依然还认为,自己得知道最原始的方法怎么使用才是最重要的。
RuntimePermissionDemo.java
动态权限申请对于用户而言是友好的,但受伤的总是程序猿…
总的来说,动态申请权限在以后的开发中是逃避不了的,还是趁机好好攻略一下,毕竟研究了一下发现,也不是特别难开发的东西