Android M 运行时权限实践全解析

作者: 楚云之南 | 来源:发表于2016-03-14 16:52 被阅读2614次

    今天早上看到新闻说Android N的preview版本已经出来了,据说已经支持Java 8 的语言特性,想想函数式编程还有点小激动呢,让我缓缓。

    然而我们今天讨论的话题是Android M的运行时适配,也许你要问新版本都出来了再来炒冷饭还有意义么?由于Android M在市场占有率一直较低,Google统计截至到今天,Android M占有率为2.3%,天朝的数据肯定比这还低,友盟的版本统计数据中压根就看不到Android M的身影,这么低的市场占有率导致很多公司没有动力去推动进行运行时权限适配。我司作为国内著名二三线互联网公司也是最近一个月才完成适配,所以很多小伙伴都还没开始做这件事情。

    如果你对运行时权限基本知识已经非常熟悉,请跳过第一节“什么是运行时权限”。

    一 、什么是运行时权限

    1.1 权限授予

    在Android M之前,如果应用需要某个权限,我们可以在Manifest文件中指定即可:

    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.INTERNET" />
    

    在安装时,安装工具会弹出对话框告知用户当前安装的应用所需要的权限:

    Paste_Image.png

    此时,用户只有两个选择,继续安装 or 直接不安装。在应用安装后,用户不能够再去取消相应的权限,总结起来一句话:Android M之前权限管理就是一杆子买卖

    为了更加灵活地控制权限,在Android M之后,对于某些权限,只有在APP真正使用时,才会去申请相应的权限。注意:这里的申请是我们代码自己的行为,这也就是适配的全部工作。如我们在应用内调起摄像头时,我们需要自己向系统发出权限申请,系统会弹出对话框告诉用户这个操作需要什么权限,用户选择之后,系统再把结果返回给应用:

    Paste_Image.png

    如果用户选择允许,那么我们的程序可以正常走下面的拍照逻辑,如果选择拒绝,我们只能不使用摄像头了,一般的操作就是取消当次操作,当然在某些特殊场景下我们有一些特殊处理,详见后文。

    1.2 权限收回

    一个权限被用户允许后,还可以被收回,收回权限的用户操作一共有两种:

    1.在应用信息-权限设置页面

    Paste_Image.png

    2.直接删除所有数据

    Paste_Image.png

    对于需要权限的操作,在使用时每次都需要判断是否已经授权,因为用户可以随时收回权限。

    1.3权限分类

    Android对各种权限进行了划分,一共三类:
    正常权限(查看所有正常权限)
    正常权限指对用户隐私不敏感的信息,比如我们常用的联网权限 INTERNET。上图中包含CAMERAINTERNET权限的APK在Android M上安装效果如下:

    Paste_Image.png

    因为INTERNET是正常权限,所以被系统直接授权,当然这里就无需展示了,而CAMERA呢?它就是下面说的危险权限了。
    危险权限(查看所有危险权限)
    危险权限就是我们需要适配的重点区域了,所有的危险权限都是在运行时(需要时)才会申请,所以当然在安装时也无需展示了。需要注意的是,权限进行了分组,每一组中只要有一个权限被授予了,那么组内其它权限也会被授予。

    特殊权限
    SYSTEM_ALERT_WINDOW:设置悬浮窗
    WRITE_SETTINGS:修改系统设置
    这些权限在各类安全卫士上使用较多,大部分情况下我们都不需要,所以这里不再展开将,基本流程就是发一个Intent给系统权限设置页面,用户授予权限之后,在onActivityResult中获取结果。
    以上基础可以在这篇文章中获得:聊一聊Android 6.0的运行时权限

    二、适配最佳实践

    2.1 适配API介绍

    在Android M的SDK中,在Activity中新增了进行运行时权限适配的三个API:

    void requestPermissions(String[] permissions, int requestCode)//请求权限,参数可以是一个权限或者是多个。
    void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)//请求权限之后的回调。
    boolean shouldShowRequestPermissionRationale(String permission)//是否有必要告诉用户我们需要这个权限的原因。
    

    Context中添加了一个API:

    int checkSelfPermission(String permission)//用来检测当前应用是否具有某个权限。
    

    由于这些API都是Android M以上版本才有,为了避免我们在代码里面引入过多的版本判断,support包23版本中添加了个对应的API:

    ActivityCompat.requestPermissions(Activity activity,String[] permissions,int requestCode)
    FragmentActivity.onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
    boolean ActivityCompat.shouldShowRequestPermissionRationale(Activity,  String permission)
    ContextCompat.checkSelfPermission(String permission)
    

    2.2基本流程

    2.2.1官方版本

    官方training中有个例子,以应用获取权限READ_CONTACTS为例,在获取权限之后,我们要读取手机的联系人列表操作:readContacts()

    // 检查是否已经具有权限
    if (ContextCompat.checkSelfPermission(thisActivity,Manifest.permission.READ_CONTACTS)
        != PackageManager.PERMISSION_GRANTED) {
        // 是否需要告诉用户我们为什么需要这个权限
        if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
         Manifest.permission.READ_CONTACTS)) {
         //弹出信息,告诉用户我们为啥需要权限
    
        } else {
        //直接获取权限
        ActivityCompat.requestPermissions(thisActivity,
                new String[]{Manifest.permission.READ_CONTACTS},
                MY_PERMISSIONS_REQUEST_READ_CONTACTS);
        //用户授权的结果会回调到FragmentActivity的onRequestPermissionsResult
        }
    }else {
     //已经拥有授权
     readContacts();
    }
    

    在onRequestPermissionsResult中:

    public void onRequestPermissionsResult(int requestCode,
        String permissions[], int[] grantResults) {
      switch (requestCode) {
        case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
            if (grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                readContacts();
            } else {
             //权限没能授权通过,可以考虑弹个toast告诉用户
            }
            return;
        }
      }
    }
    

    2.2.2 一个权限是必须的?

    上面这个流程对于大部分权限来说没有问题,但是,如果我的应用中某个权限是必须的,上面的流程就有问题了,至于问题是什么,我们先看看系统的授权交互界面:
    应用在第一次请求某个权限时,弹出的对话框如下:

    Paste_Image.png

    如果用户选择拒绝,那么下次在请求时,如下图:

    Paste_Image.png

    会多一个 “再不提示”复选框 的对话框,如果用户不勾选,直接拒绝,那么以后在请求时都会弹出这个带有复选框的对话框;
    如果用户勾选了 “不再提示”,那么以后APP在请求权限时,并不会提示授权对话框,而是直接回调到onRequestPermissionsResult,并且结果是拒绝授权。

    可悲的是API被没有提供一个接口告诉我们用户已经选择了不再询问,那么采取training中的流程时,如果某一个权限是必须的而被用户勾选不再提示,那么这个app永远不会执行到readContacts()方法了,而且用户也得不到任何提示,如果我开发的是一个联系人APP,这不是坑爹么?

    也许你会说不是有shouldShowRequestPermissionRationale方法用来描述是否要告诉用户我们为什么需要这个权限么?但是这个方法是有缺陷的,下面我们来解释一下各个操作之间这个函数返回值的变化:

    [用户操作序列][函数返回结果][用户选择]

    [第一次请求][false][拒绝]--->第二次请求[true][拒绝,勾选]--->第三次请求[false][...]

    [第一次请求][false][拒绝]--->第二次请求[true][拒绝,不勾选]这个操作可以重复N次--->第N+2次请求[true][拒绝,勾选]--->第N+3次请求[false][操作]

    这里我们可以看到false是有二义性的,既可以代表之前没有请求过这个权限,也可以代表用户选择了不再询问,但是这两种情况下我们的处理逻辑肯定不一致。不过这个函数如果两次请求之间值的变化是由 true-->false,那么必然是用户点击了never ask again!!

    2.2.3 最佳流程

    我们可以从Google自己家的APP找到一些灵感,比如相机应用。这里我先把相机的权限去掉,然后我打开相机,此时会弹出对话框,询问权限,此时如果拒绝并勾选不再提示之后,它会直接弹出一个对话框告诉用户去给APP添加权限,如果我们点击设置,会直接到相机应用的设置页面,这就完成了对用户进行权限设置的引导。

    ezgif-3763241376.gif

    需要注意的是,点击去设置之后,如果用户在设置页面给予了相应的权限,在返回时发现相机已经关闭了,可以判断点击设置之后,相机就把自己finish()掉了。其实我们可以通过startActivityForResult启动设置页面,在设置页面返回到onActivityResult中再去判断相应的请求是否已经授予权限。

    启动设置页面:

    private void startAppSetting() {
      Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
      Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
      intent.setData(uri);
      activity.startActivityForResult(intent, PERMISSIONS_REQUEST_READ_CONTACTS);
    }
    
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        //注意,这里不需要判断 resultCode == Activity.RESULT_OK ,因为设置页面是不会给我们设置结果的
        //设置
        if(requestCode == PERMISSIONS_REQUEST_READ_CONTACTS){
           if (ContextCompat.checkSelfPermission(thisActivity,Manifest.permission.READ_CONTACTS) {
                //用户已经在设置页面授权
                readContacts();
            }
        }
    
    }
    

    所以问题的根本就是我们需要知道用户点击了“不再询问”。既然shouldShowRequestPermissionRationalefalse存在二义性,那么我们只能加入一个本地的标记来辅助区分,这个标记保存的是上一次请求时的shouldShowRequestPermissionRationale结果。

    //设置标记,可以存放到SP
    private void setFlag() {
      boolean flag = ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,Manifest.permission.READ_CONTACTS);
      //存储flag到sp
    }
    private boolean getFlag() {
      //从sp中读出flag
    }
    
    //是否需要弹出对话框
    private boolean needShowGuide() {
      return getFlag() 
                && ! ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,Manifest.permission.READ_CONTACTS)
    }
    

    如果这个标记是true,而当前的结果为false,表示这两次请求之间用户点击了“不再询问”,此时,我们就可以弹出对话框

    Paste_Image.png

    用户点击“设置”时,直接将用户引导至APP设置页面。

    最终流程如下:

    Paste_Image.png

    发现一个坑

    issue戳这里
    Google官方最佳实践是这样曰的:

    Paste_Image.png

    大致意思是如果我们本身不需要直接操作摄像头,是不需要去获取权限的,然而,我们的项目中存在这样的场景:在某一个第三方的SDK里面需要去扫二维码,需要操作摄像头,所以我们在manifest文件中配置了如下权限:

    <uses-permission android:name="android.permission.CAMERA" />
    

    然而在应用另外一个地方我只需要调用系统相机即可,然而按照文档所说,我不需要进行任何权限申请,然后这儿的bug是:

    如果在menifest文件中申请了"android.permission.CAMERA"权限,那么通过Intent使用相机的时候也需要动态申请权限,具体原因请戳上面的issue。

    相关文章

      网友评论

      • Android轮子哥:推荐一个简单易用的框架,一句代码搞定权限请求,从未如此简单:https://www.jianshu.com/p/c69ff8a445ed
      • rockan007:如果用户拒绝 并选择 donot ask again ,怎么判断是否要显示设置权限对话框?
        把数据保存到本地????
      • 499691d089d0:这个写的是真详细。用户拒绝弹窗的这些细节都介绍到了
      • e67b99386af0:我做升级时候需要有读写SD卡权限,之前手机好好的,换了个手机就不行了,原来是因为新手机系统版本太高。真坑爹。

      本文标题:Android M 运行时权限实践全解析

      本文链接:https://www.haomeiwen.com/subject/anqzkttx.html