Android M 权限使用解析

权限

第三方库:easypermissions

1.1 权限授予

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

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

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

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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,527评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,314评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,535评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,006评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,961评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,220评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,664评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,351评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,481评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,397评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,443评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,123评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,713评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,801评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,010评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,494评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,075评论 2 341

推荐阅读更多精彩内容