Android运行时权限整理

3,972 阅读8分钟

目录

  • 简介
  • 查看危险权限
  • 请求方式
  • 权限的演变
  • 项目地址
  • 优秀权限申请库

简介

device-2021-03-18-104824.png 不知道你对上面的安装场景是否还有印象,想当年智能机刚刚流行起来的时候,安装应用时,总会有一个界面上展示着安装应用所需要的权限,点击继续就代表你已经同意了以上所有权限(此界面跟以前稍微有所不同,以前在Android6.0(API级别23)之前,在安装界面权限不能操作,界面大概就是这样的,懂我意思就行😊),可能一个功能很单一的App,也要请求一屏都滑不完的权限列表,用来收集一些用户的隐私信息,所以在Android6.0(API级别23)以上,一些危险权限就不能在安装应用的一股脑全部允许了,需要在App的场景用到的地方动态的去申请,区别于之前的安装完成就拥有所有权限,动态申请可以让用户的隐身得到更好的保护;因为用户是参与其中的,并有权选择同意或拒绝。对用户而言是交互上的变化,对Android开发来说,是一种新的代码逻辑处理,下面我们就说一下如何在代码中实现运行时权限的申请。

查看危险权限

在实现运行时权限之前,我们还要做一件事,就是要先知道,哪些权限是危险权限,需要动态的申请。在官方的开发者文档中有告诉我们如何查看危险权限;查看危险权限

连接上手机,在终端输入以下命令,就可以看到以组列出的危险权限

adb shell pm list permissions -d -g

➜  ~ adb shell pm list permissions -d -g
Dangerous Permissions:

group:com.google.android.gms.permission.CAR_INFORMATION
  permission:com.google.android.gms.permission.CAR_VENDOR_EXTENSION
  permission:com.google.android.gms.permission.CAR_MILEAGE
  permission:com.google.android.gms.permission.CAR_FUEL

group:android.permission-group.CONTACTS

group:android.permission-group.PHONE

group:android.permission-group.CALENDAR

group:android.permission-group.CALL_LOG

group:android.permission-group.CAMERA

...

上面列出的是一组一组的危险权限,为什么是组,先按下不表,在后面的演变中会再说;这样就知道哪些是危险权限了,在应用中使用到时,就要想着使用运行时权限了。

请求方式

先说之前的请求方式

知道了危险权限有哪些,接下来就是请求了;请求方式很简单,在清单文件注册是少不了的。接着在调用功能之前,进行运行时权限的请求和判断,我们以打电话为例,来写一段代码:

//在Mainfest文件中添加需要用到的权限
 <uses-permission android:name="android.permission.CALL_PHONE" />
public void callPhone(View view) {
        //请求权限
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
        //权限第一次被拒绝后,第二次申请时shouldShowRequestPermissionRationale方法会返回true
            if (ActivityCompat.shouldShowRequestPermissionRationale(this,Manifest.permission.CALL_PHONE)) {
                    DialogUtils.showDialog(this, new DialogClickListener() {
                        @Override
                        public void ok() {
                            ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.CALL_PHONE},CALL_PHONE_REQUEST_CODE);
                        }
                    });
            }else {
                ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.CALL_PHONE},CALL_PHONE_REQUEST_CODE);
            }
        }else {
            call();
        }
    }
    
    private void call() {
        Intent intent = new Intent(Intent.ACTION_CALL);
        intent.setData(Uri.parse("tel://18568655367"));
        startActivity(intent);
    }
    
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode){
            case CALL_PHONE_REQUEST_CODE:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
                    call();
                }else {
                    Toast.makeText(MainActivity.this,"电话权限被拒绝",Toast.LENGTH_SHORT).show();
                }
                break;
            default:
                break;
        }
    }

申请权限

在点击事件中先调用ContextCompat.checkSelfPermission()方法检查是否有电话权限,该方法的返回值是PERMISSION_GRANTEDPERMISSION_DENIED分别表示已授权和未授权;根据返回值判断如果没有授权就调用ActivityCompat.requestPermissions()方法发起权限请求,其中的参数CALL_PHONE_REQUEST_CODE是自定义的一个请求码,在下面的onRequestPermissionsResult回调方法中会用到。作用和startActivityForResult方法中的requestCode类似。如果判断有权限就直接打电话。

接着就会弹出一个弹框,显示应用要请求的权限,用户可以选择允许拒绝,选择的结果会在onRequestPermissionsResult回调方法中处理。

处理权限

onRequestPermissionsResult回调方法中刚才提到的requestCode就要用上了,因为考虑到在一个页面中可能会有不同权限的请和逻辑处理,所以可以用requestCode很好的区分;在方法中判断如果code相等,判断grantResults数组的长度,grantResults数组就是我们上面请求传入new String[]{Manifest.permission.CALL_PHONE}数组,因为我们传入的只有一个权限,所以获取第一个,判断是否已经授权,如果授权调用call()方法拨打电话,其他情况就是权限被拒绝,给出相应的提示。

以上就是请求运行时权限的过程,上面只是请求一个权限,如果请求多个权限,只需在数组中继续添加权限,对应的在onRequestPermissionsResult回调方法处理就行。

ActivityCompat.shouldShowRequestPermissionRationale()方法

请求权限代码中,在判断是否有某一权限之后,接着调用了ActivityCompat.shouldShowRequestPermissionRationale()方法的判断,这个方法在用户拒绝了一次权限以后,再次申请此权限,该方法会返回true,用意是当用户不理解为什么申请此权限而拒绝时,应用可以给出一个解释,让用户知道,我要申请此权限的用途,在用户明白之后再次申请权限可以提升同意此权限的概率。

再说说AndroidX包提供的请求方式

接着我们再说说另一种方式,这种方式是AndroidX包中提供的一种请求方式,和上面的方法基本上差不多,少了回调部分,下面还是以打电话为例:

private ActivityResultLauncher<String> requestPermissionLauncher;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);

        requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
            if (isGranted) {
                call();
            } else {
                Toast.makeText(this,"电话权限被拒绝",Toast.LENGTH_SHORT).show();
            }
        });
    }

    @RequiresApi(api = Build.VERSION_CODES.M)
    public void secondCall(View view) {

        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
            call();
        } else if (shouldShowRequestPermissionRationale(Manifest.permission.CALL_PHONE)) {
            DialogUtils.showDialog(this, new DialogClickListener() {
                @Override
                public void ok() {
                    requestPermissionLauncher.launch(Manifest.permission.CALL_PHONE);
                }
            });

        } else {
            requestPermissionLauncher.launch(Manifest.permission.CALL_PHONE);
        }
    }


    private void call() {
        Intent intent = new Intent(Intent.ACTION_CALL);
        intent.setData(Uri.parse("tel://12345678"));
        startActivity(intent);
    }

创建registerForActivityResult并处理

和前面的不同,AndroidX中提供的请求方式,不需要重写onRequestPermissionsResult方法,而是将处理结果放在了registerForActivityResult方法中处理,根据isGranted字段判断权限是否已授权。

申请权限

申请权限和前面判断的类似,registerForActivityResult方法返回一个ActivityResultLauncher对象,利用ActivityResultLauncher.launch()方法,进行权限的申请。

多权限申请

上面只是一个权限的申请,如果有多个权限怎么办呢,需要替换一下类型

    // 1、将String替换成String[]
    private ActivityResultLauncher<String[]> requestPermissionLauncher;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        // 2、将RequestPermission替换成RequestMultiplePermissions
        requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), map -> {
            // 3、isGranted的类型由boolean变成map,map的键值对是<String,Boolean>
            //String对应的是权限,Boolean对应的是是否授权,需要判断处理
            if (map.size() > 0
                    && map.get(Manifest.permission.CALL_PHONE)
                    && map.get(Manifest.permission.CAMERA)
            ) {
                call();
            } else {
                Toast.makeText(this,"电话权限被拒绝",Toast.LENGTH_SHORT).show();
            }
        });
    }

    @RequiresApi(api = Build.VERSION_CODES.M)
    public void secondCall(View view) {
    //4、检测权限也需要判断多个,用&&符号
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED
            && ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
        ) {
            call();
        } else if (shouldShowRequestPermissionRationale(Manifest.permission.CALL_PHONE)) {
            DialogUtils.showDialog(this, new DialogClickListener() {
                @Override
                public void ok() {
                    // 5、launch方法中参数由String变成String[]
                    requestPermissionLauncher.launch(new String[]{Manifest.permission.CALL_PHONE,Manifest.permission.CAMERA});
                }
            });

        } else {
            requestPermissionLauncher.launch(new String[]{Manifest.permission.CALL_PHONE,Manifest.permission.CAMERA});
        }
    }

替换部分有5个点

  1. 将String替换成String[]
  2. 将RequestPermission替换成RequestMultiplePermissions
  3. isGranted的类型由boolean变成mapmap的键值对是<String,Boolean>;String对应的是权限,Boolean对应的是是否授权,对判断也需要变更。
  4. 检测权限也需要判断多个,用&&符号
  5. launch方法中参数由String变成String[]

注意

在使用AndroidX包申请运行时权限时,可能会遇到这样一个bug java.lang.IllegalArgumentException: Can only use lower 16 bits for requestCode 查了一下这个错误说是因为requestCode不能为负数或者大于65536;讲道理,我们这种方式在代码中并没有用到requestCode,那为什么有这种问题呢?坦白的说:我不知道。我在网上找到了一个答案解决了这个问题。

build.gradle文件里面添加以下两个依赖:

    implementation 'androidx.activity:activity-ktx:1.2.0-alpha08'
    implementation 'androidx.fragment:fragment:1.3.0-alpha08'

添加这两个依赖就能解决问题了。如果有哪位小伙伴知道问题的原因,还请告知,感谢😊

权限的演变

知道怎么请求权限以后,再聊一聊Android6.0到Android11这几个版本中针对运行时权限做了哪些修改,也让我们在开发中更好的去做适配。

如果tragetSdkVersion<=21

build.gradle文件里面targetSdkVersion表示应用最高兼容到的Android版本,就是使用API最高的版本。上面说了在Android6.0开始才有的运行时权限,6.0对应的API是23,那如果我们把项目的targetSdkVersion改成21,这时候还没有运行时权限,如果在手机上运行会是什么样呢?

文章开头你看到的图片就是targetSdkVersion改成21的场景,所有的权限都在安装时自动授权,安装好即获得清单文件中的全部权限,但是在继续安装的时候系统会有提示框:

旧版本提示框

而且页面在手机上也不是全屏,下方会有全屏展示的的按钮,点击以后可能会全屏,也可能会闪退(不同品牌手机上安装,展示的界面可能不一样,上图仅做参考)。

如果这时候有了升级包,新包的版本是6.0以上的API,你把app升级了,调用用到危险权限的功能,也是可以调用的,因为在5.0的时候,已经把权限全部都给了,所以不需要重新申请。但是如果卸载重新安装或把权限在应用信息页面关闭,如果使用到这一危险权限程序就会报异常,需要进行运行时权限处理。

权限弹框的变化

首先申请权限的弹框框,在开发中不可自定义,由系统提供。在官方的文档中也有说明 在Android6.0、7.0时候的权限弹框,只要请求就可以弹出,不管拒绝多少次;在弹框上有一个“不再提示”的小的选择框,只有在拒绝权限时选中了此选项,下次再请求就不会再弹出权限弹框了。

在Android8.0、9.0、10的系统上,差别不大,也是只要请求就会展示权限申请弹框;不同点在于弹框的选择上,在这些系统版本上,第一次请求权限时,没有“不在提示”的选项,只有“允许”和“拒绝”,但是如果第一请求该运行时权限,用户拒绝了,那么从第二次申请开始,在弹框中就会显示三个选项,分别是“允许”、“拒绝”、“拒绝并不再提示”;如果用户选择了第三个,之后这个权限就会一直不被授权,也无法调起权限弹框,只能在应用的详情页面,手动授权。

在Android11系统上,又有了新的变化;在权限弹框中同样没有“不再提示”的小的选择框,但是在权限被拒绝后,也不会有“拒绝并不再提示”这样的选项,如果连续拒绝两次之后,就不会再显示权限弹框,权限会返回被拒绝,同样后续也只能在应用的详情页面,手动授权。相当于此刻系统帮我们勾选了“不再提示”的选项;所以可以利用好上面提到的shouldShowRequestPermissionRationale方法,在用户第一次拒绝了申请后,要向用户解释明白为什么要申请此权限。

Tips:因为手上只有6.0、7.0和10.0系统的手机,所以弹框第二次变化的情况,不是很确定,如果描述有误,还请大佬指正,我会在第一时间修改。另外国内手机厂商众多,定制的UI系统也各有千秋,在展示形式和文字描述上可能有所不同,但大致就是上面说的这些情况,不影响我们的适配和代码逻辑判断。

权限组

在上面命令行中查看危险权限的时候,我们知道危险权限都是以组的形式的存在的,比如READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE两者同属于STORAGE组,在Android8.0以前(Api < 25),如果我们申请READ_EXTERNAL_STORAGE权限,系统则会将STORAGE组的权限都给我们,也就是说我们可以同时得到READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE两个权限(两个权限在清单文件中都有声明),拿到权限后可以进行读写操作;但是从Android8.0开始(Api >= 25)有变化了,当我们申请其中一个权限时,系统只会给当前申请的权限,并不会将整个组都给到我们;如果后续再申请组中的另一个权限,系统会立马同意,中间不会通知用户,也就是说不会有弹框提示。

那这情况对我们开发有什么影响呢?举个例子,如果我们在代码中申请了READ_EXTERNAL_STORAGE权限,在权限同意之后,对内存进行读写操作,如果是运行在8.0系统之前的手机上,那么是没有问题,可以正常的使用。如果是在8.0或更高系统的手机上,就行不通了。因为申请哪个权限就只给哪一个,不会将权限组都给。

解决办法:在申请危险权限时,可以将整个权限组的权限一起申请,这样用户体验方面也只是给出一次弹框,同时也可以兼容到所有的Android版本。最后别忘记在清单文件中声明要申请的权限。

项目地址

项目中有上面示例的所有代码,还有一个根据郭霖大佬的视频简单封装的一个BaseActivity,可以简化调用的流程。

项目地址

优秀权限申请库

Google可能自己也感觉,权限申请的步骤有些繁琐,所以就封装了一个类,大家感兴趣可以看看 EasyPermissions

郭霖大佬的PermissionX

参考

郭霖大佬B站关于运行时权限的直播视频

Android8.0运行时权限适配方案