Android — 运行时权限检查

运行时权限检查

概括

简单粗暴的来说一下,在 Android 6.0 即 API 23 之前,我们用到的权限只需要在 AndroidManifest 文件中统一进行申请就可以了,而且用户不可取消!

但是在 6.0 及以上,出于安全性考虑,开始由用户掌握了部分主动权!

Android 中的系统权限可以分为 normal 和 dangerous 两种。

normal 这类的权限不太会直接威胁到用户的隐私,只需要在 Manifest 文件中注册即可;

dnagerous 这类的权限使得 app 可以直接访问用户的一些隐私数据,从 Android 6.0 开始,这些权限就不仅仅只需要在 Manifest 文件中声明,而且需要在运行且适当的时候进行动态的申请,由用户决定是否授权,并且用户可以随时取消这些权限(在设置里的应用详细信息界面取消)。如果不进行权限检查而贸然执行一些“危险”的动作的话,应用可能是会崩溃的。

Dangerous Permissions List

这里只给出 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

如上表,权限被分组了,值得一提的是 同一组中的任何一个权限被授权了,其他权限也就自动被授权了

最基本的使用步骤

申请授权一般需要三步:

  1. 检查权限是否已经授权
  2. 申请用户授权
  3. 处理授权结果

好在 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);
}

修改后的代码应该可以满足我能想到所有的情况:

  1. 如果第一次申请授权,系统申请授权对话框会出现,如果用户拒绝,那么可以什么都不做

  2. 接着第二次申请授权,系统授权对话框仍会出现,而且有 “不再提示” 选项,

    如果用户只拒绝授权,不选择 “不再提示” ,那么还是可以什么都不做

    如果用户既拒绝授权,又选择 “不再提示” ,那么就可弹出自定义提示框,引导用户去设置开启权限

  3. 用户选择 “不再提示” 后再申请授权,系统授权对话框不会再出现,这时自定义提示框会就派上用场了,可引导用户去开启权限

  4. 如果已授权,那么就可直接进行操作了

  5. 如果已授权,但是用户在应用设置界面手动取消权限,这时再申请授权,系统对话框会提示,剩下的就和情况 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

总结

动态权限申请对于用户而言是友好的,但受伤的总是程序猿…

总的来说,动态申请权限在以后的开发中是逃避不了的,还是趁机好好攻略一下,毕竟研究了一下发现,也不是特别难开发的东西

你可能感兴趣的:(Android)