权限
第三方库:easypermissions
1.1 权限授予
在Android M(6.0)之前,如果应用需要某个权限,我们可以在Manifest文件中指定即可
在安装时,安装工具会弹出对话框告知用户当前安装的应用所需要的权限:
此时,用户只有两个选择,继续安装 or 直接不安装。在应用安装后,用户不能够再去取消相应的权限,当然有个别厂商自带权限管理(安全卫士等)。
为了更加灵活地控制权限,在Android M之后,对于某些权限,需要程序动态向用户申请,静态注册不在起作用。如我们在应用内调起摄像头时,我们需要自己向系统发出权限申请,系统会弹出对话框告诉用户这个操作需要什么权限,用户选择之后,系统再把结果返回给应用:
如果用户选择允许,那么我们的程序可以正常走下面的拍照逻辑,如果选择拒绝,当然就无权使用摄像头,功能不可用。
1.2 权限收回
一个权限被用户允许后,还可以被收回,收回权限的用户操作一共有两种:
1.在应用信息-权限设置页面
2.直接删除所有数据
所以,对于需要权限的操作,在使用时每次都需要判断是否已经授权,因为用户可以随时收回权限。
1.3权限分类
Android对各种权限进行了划分,一共三类:
正常权限(查看所有正常权限)
正常权限指对用户隐私不敏感的信息,比如我们常用的联网权限 INTERNET。上图中包含CAMERA和INTERNET权限的APK在Android M上安装效果如下:
因为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 一个权限是必须的?
上面这个流程对于大部分权限来说没有问题,但是,如果我的应用中某个权限是必须的,上面的流程就有问题了,至于问题是什么,我们先看看系统的授权交互界面:
应用在第一次请求某个权限时,弹出的对话框如下:
如果用户选择拒绝,那么下次在请求时,如下图:
会多一个 “再不提示”复选框 的对话框。
如果用户不勾选,直接拒绝,那么以后在请求时都会弹出这个带有复选框的对话框;
如果用户勾选了 “不再提示”,那么以后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,表示这两次请求之间用户点击了“不再询问”,此时,我们就可以弹出对话框
用户点击“设置”时,直接将用户引导至APP设置页面。
最终流程如下
发现一个坑
issue戳这里
Google官方最佳实践是这样说的:
大致意思是如果我们本身不需要直接操作摄像头,而是通过第三方SDK【如相册】使用摄像头,是不需要去获取权限的。
但如果在menifest文件中申请了"android.permission.CAMERA"权限,那么通过Intent使用相机的时候也需要动态申请权限,具体原因请戳上面的issue。 这是一个bug。