Android M 权限使用解析

权限

第三方库:easypermissions

1.1 权限授予

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



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

image

此时,用户只有两个选择,继续安装 or 直接不安装。在应用安装后,用户不能够再去取消相应的权限,当然有个别厂商自带权限管理(安全卫士等)。

为了更加灵活地控制权限,在Android M之后,对于某些权限,需要程序动态向用户申请,静态注册不在起作用。如我们在应用内调起摄像头时,我们需要自己向系统发出权限申请,系统会弹出对话框告诉用户这个操作需要什么权限,用户选择之后,系统再把结果返回给应用:

image

如果用户选择允许,那么我们的程序可以正常走下面的拍照逻辑,如果选择拒绝,当然就无权使用摄像头,功能不可用。

1.2 权限收回

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

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

image

2.直接删除所有数据

image

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

1.3权限分类

Android对各种权限进行了划分,一共三类:

正常权限(查看所有正常权限)

正常权限指对用户隐私不敏感的信息,比如我们常用的联网权限 INTERNET。上图中包含CAMERA和INTERNET权限的APK在Android M上安装效果如下:

image

因为INTERNET是正常权限,所以被系统直接授权,当然这里就无需展示了,而CAMERA呢?它就是下面说的危险权限了。

危险权限(查看所有危险权限)

危险权限就是我们需要适配的重点区域了,所有的危险权限都是在运行时(需要时)才会申请,所以当然在安装时也无需展示了。需要注意的是,权限进行了分组,每一组中只要有一个权限被授予了,那么组内其它权限也会被授予。

特殊权限

SYSTEM_ALERT_WINDOW:设置悬浮窗
WRITE_SETTINGS:修改系统设置
这些权限在各类安全卫士上使用较多,大部分情况下我们都不需要。基本流程就是发一个权限申请给系统权限设置页面,用户授予权限之后,在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 一个权限是必须的?

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

image

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

image

会多一个 “再不提示”复选框 的对话框。

  • 如果用户不勾选,直接拒绝,那么以后在请求时都会弹出这个带有复选框的对话框;

  • 如果用户勾选了 “不再提示”,那么以后APP在请求权限时,并不会提示授权对话框,而是直接回调到onRequestPermissionsResult,并且结果是拒绝授权。

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

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

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

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

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

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

2.2.3 最佳流程

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

需要注意的是,点击去设置之后,如果用户在设置页面给予了相应的权限,在返回时发现相机已经关闭了,可以判断点击设置之后,相机就把自己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();
        }
    }

}

所以问题的根本就是我们需要知道用户点击了“不再询问”。既然shouldShowRequestPermissionRationale的false存在二义性,那么我们只能加入一个本地的标记来辅助区分,这个标记保存的是上一次请求时的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,表示这两次请求之间用户点击了“不再询问”,此时,我们就可以弹出对话框

image

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

最终流程如下

image

发现一个坑

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

image

大致意思是如果我们本身不需要直接操作摄像头,而是通过第三方SDK【如相册】使用摄像头,是不需要去获取权限的。

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

你可能感兴趣的:(Android M 权限使用解析)