在Android M(6.0)以前,若是应用须要某个权限,咱们能够在Manifest文件中指定便可html
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET" />
复制代码
在安装时,安装工具会弹出对话框告知用户当前安装的应用所须要的权限:android
为了更加灵活地控制权限,在Android M以后,对于某些权限,须要程序动态向用户申请,静态注册不在起做用。如咱们在应用内调起摄像头时,咱们须要本身向系统发出权限申请,系统会弹出对话框告诉用户这个操做须要什么权限,用户选择以后,系统再把结果返回给应用:安全
一个权限被用户容许后,还能够被收回,收回权限的用户操做一共有两种:bash
因此,对于须要权限的操做,在使用时每次都须要判断是否已经受权,由于用户能够随时收回权限。app
Android对各类权限进行了划分,一共三类:ide
正常权限指对用户隐私不敏感的信息,好比咱们经常使用的联网权限 INTERNET。上图中包含CAMERA和INTERNET权限的APK在Android M上安装效果以下:函数
危险权限就是咱们须要适配的重点区域了,全部的危险权限都是在运行时(须要时)才会申请,因此固然在安装时也无需展现了。须要注意的是,权限进行了分组,每一组中只要有一个权限被授予了,那么组内其它权限也会被授予。工具
SYSTEM_ALERT_WINDOW:设置悬浮窗 WRITE_SETTINGS:修改系统设置 这些权限在各种安全卫士上使用较多,大部分状况下咱们都不须要。基本流程就是发一个权限申请给系统权限设置页面,用户授予权限以后,在onActivityResult中获取结果。ui
以上基础能够在这篇文章中得到:聊一聊Android 6.0的运行时权限this
在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)
复制代码
官方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;
}
}
}
复制代码
上面这个流程对于大部分权限来讲没有问题,可是,若是个人应用中某个权限是必须的,上面的流程就有问题了,至于问题是什么,咱们先看看系统的受权交互界面: 应用在第一次请求某个权限时,弹出的对话框以下:
若是用户选择拒绝,那么下次在请求时,以下图:
若是用户不勾选,直接拒绝,那么之后在请求时都会弹出这个带有复选框的对话框;
若是用户勾选了 “再也不提示”,那么之后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!!
咱们能够从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,表示这两次请求之间用户点击了“再也不询问”,此时,咱们就能够弹出对话框
issue戳这里
Google官方最佳实践是这样说的:
但若是在menifest文件中申请了"android.permission.CAMERA"权限,那么经过Intent使用相机的时候也须要动态申请权限,具体缘由请戳上面的issue。 这是一个bug。