Android6.0中的运行时请求权限

问题来源于最近的一个项目中,原本好好的程序,在一台Nexus 6(Android6.0)上测试发现所有需要保存图片到本地的都不行,看报错:

其实不仅仅是保存图片这里报这个错,还有打开相机的时候也会报错。好了问题就这么开始了!

看到这里都知道是权限的问题所引起的原因,对,确实是因为权限的问题引起的,但是程序明明已经在Manifest中声明了以上操作需要的权限,而且在以前测试的系统机型中都是没问题的,为什么!

突然间想到了Marshmallow发布的时候提及到的Requesting Permissions at Run Time这东西,对就是他的原因!

Requesting Permissions at Run Time究竟是啥,官方对他是如下介绍滴:

从Android6.0(API >= 23)开始,用户在APP运行的时候授予其权限而不是像以前安装的时候就通通授予了(以前等待方式没什么卵用)。由于不在需要在安装或更新APP的时候授予相关权限,这就简化了APP的安装过程。这也提高了用户对APP功能的控制,比如:用户可以选择让一个Camera APP使用Camera,用户可以在任何时候在设置面板撤销这个权限。。。

看完是不是有点像我们在国产ROM中常见到的每个应用运行时权限授予。

系统权限也被分成 normaldangerous两类:

• Normal类的权限不会直接涉及到用户隐私风险。如果APP在Manifest文件中声明了Normal类的权限,系统会自动授予这些权限。
• Dangerous类的权限可能会让APP涉及到用户机密的数据。如果APP在Manifest文件中声明了Normal类的权限,系统会自动授予这些权限。如果在Manifest文件中添加了Dangerous类的权限,用户必须明确的授予对应的权限后APP才具有这些权限。

关于哪些权限属于Normal类,哪些属于Dangerous类,如下图:

更详细请看normal-dangerous-permissions文档。

现在我们知道了在APP中normal和dangerous类的权限都需要Manifest文件中声明,但是在不同的版本系统和target SDK效果是不一样的,有如下几点需要注意:

  1. 系统Android 5.1以下或target SDK 22以下,只要在Manifest文件中声明了需要的权限,用户在安装APP的时候就会授予相应的权限;如果不授予当然APP也无法安装。
  2. 如果设备运行在6.0以上并且你的应用的target SDK版本>=23,APP除了需要在Manifest文件中声明相应的权限之外,还要在APP运行时向用户进行请求每个dangerous类的权限。用户可以选择授予或不授予该权限,即使用户不授予该权限APP也可以继续运行,但是相关的需要权限的操作是没法进行的。

使用系统提供的API检查并请求权限

a. 检查权限

以为用户可以随时撤销对APP的授权,所以在每次准备进行需要dangerous类权限操作的时候,需要检查APP是否具有对应的权限。使用ContextCompat.checkSelfPermission())检查权限,代码如下:

1
2
3
// 检查activity是否有写日程的权限
// Assume thisActivity is the current activity
int permissionCheck = ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.WRITE_CALENDAR);

上面代码,如果APP有该权限返回PackageManager.PERMISSION_GRANTED,APP接着可以进行对应操作;如果没有权限,以上方法返回PERMISSION_DENIED,APP需要明确的向用户请求授权。

b. 请求权限

如何使用系统的API向用户请求dangerous类的权限,也很简单,support-library都基本上帮我们做好了。首先这个权限已经在Manifest文件中声明(废话么),然后向用户请求该权限,同意了你就用,否则想都别想(用户是上帝)。Google是这么解释why the app needs permissions的:

在某些情况下,你可能想让用户理解为啥你的APP需要这个权限。比如用户打开了一个拍摄APP(好吧,又是Camera),用户不会对这个APP需要使用Camera感到奇怪,但是用户可能不能理解为什么这破APP还需要使用我的位置和联系人数据(不能忍了)!所以在你请求某个权限之前,你应该考虑提供一段解释性的话给用户。请记住你不是要靠这点解释性的话就让用户彻底的明白你为什么需要这个权限,如果你解释过多,用户觉得没卵用直接卸了APP。只有当用户拒绝了你权限请求之后才是使用那段解释性的话最好的时候,因为如果用户一直尝试使用某个需要权限的功能,但是却又一直不授予该权限,这就表明用户真不知道为什么这个功能需要这个这个权限,在这种情况下,你的解释性的话就派上用场了。

Android提供shouldShowRequestPermissionRationale())方法求向用户展示为啥你需要这个权限,当用户之前已经请求过该权限并且拒绝了授权这个方法返回true。

注意:如果用户拒绝权限请求的时候选择了Don’t ask again选项,上面的方法返回false,当然如果设备本身就不允许有这个权限也是返回false。

看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(thisActivity,Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
Manifest.permission.READ_CONTACTS)) {
// Show an expanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
} else {
// No explanation needed, we can request the permission.
ActivityCompat.requestPermissions(thisActivity,
new String[]{Manifest.permission.READ_CONTACTS},
MY_PERMISSIONS_REQUEST_READ_CONTACTS);
// MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
// app-defined int constant. The callback method gets the
// result of the request.
}
}

其中requestPermissions())就是请求权限方法,异步方法。看看这个方法需要三个参数:

@param activity The target activity
@param permissions The requested permissions(这里就是你需要申请的权限,在Manifest类中可以找到你需要的权限)
@param requestCode Application specific request code to match with a result reported to onRequestPermissionsResult(int, String[], int[]).Calling these methods brings up a standard Android dialog, which you cannot customize(标志这次授权的唯一请求码,当用户进行授权操作后在回调方法中可以根据这个标识符进行区分不同的授权操作)

有个缺点就是使用系统请求授权API你不能自定义样式,请求授权弹出来的是标准的Android Dialog(如下图,遵循Android标准蛮好的),如果你希望提供一些信息之类的应该在 requestPermissions()) 之前进行操作。

c. 处理授权请求回调,onRequestPermissionsResult(int ,String , int[])方法

上面说到了根据requestPermissions())方法中的requestCode,我们就可以在回调方法中区分授权请求,看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted, yay! Do the
// contacts-related task you need to do.
} else {
// permission denied, boo! Disable the
// functionality that depends on this permission.
}
return;
}
// other 'case' lines to check for other
// permissions this app might request
}
}

最后注意以下两点:

  1. 当在Manifest文件中声明了同一个 permission group 中的权限后,请求权限时不会列出具体的权限,只是将这个permission group权限进行说明。对于permission group中的所有权限,用户只需要授权一次,以后里面的所有其他权限都不需要在进行授权(撤销后等操作除外),系统会自动认为是授权并以 PERMISSION_GRANTED 为参数调用onRequestPermissionsResult方法,和弹出系统请求授权对话框点击授权是一样的效果。虽然这样,对于每个授权还是需要进行单独请求授权操作。
  2. Requesting Permissions at Run Time是当target API >= 23并且系统为Android 6.0 (API level 23)及以上才有的,如不属于这种情况APP还是像以前一样在安装的时候回提示所有需要的权限并授予这些权限。

看看简单的demo效果;

代码:https://gitlab.com/lujun/RuntimePermissionDemo

参考文档: