原文:Understanding App Permissions
—How to request the permissions you need
概述
默认地,Android应用在创建时未被授予任何权限。当应用需要使用设备中任何受保护的特性时(发送网络请求,使用照相机,发送短信等),必须先从用户手中获取对应的权限才能实现这些操作。
在Marshmallow(Android 6.0)系统之前,权限的申请在应用安装时进行,并在项目的AndroidManifest.xml
文件中指定。全部的权限列表可以查看此处。在Marshmallow系统之后,在使用权限前必须在运行时请求。有几个可用的库可让运行时的权限申请更容易些。如果你想快速了解这些,请参考我们的指南Managing Runtime Permissions with PermissionsDispatcher。
Marshmallow(Android 6.0)之前的权限申请方式
在Marshmallow(API 23)系统之前,权限申请的方式非常简单。所有的权限申请都是在应用安装时完成处理。当用户从应用商店获取一个应用并准备安装时,首先会列出应用运行所需的所有权限。用户可以选择接受所有的权限申请并继续安装应用或者决定不去安装应用。这种方式要么全都同意,要么全都不同意。没办法做到只授予应用所需的部分权限,也没办法让用户在安装应用之后只调用特定的权限。
Dropbox应用在Marshmallow版本之前的系统上申请权限的例子:
对应用开发者来说,权限申请非常简单。要请求这些权限中的一部分时,只要简单地在AndroidManifest.xml
中指定它就可以:
例如,一个应用需要读取用户的通信录,它将会添加以下内容再AndroidManifest.xml
中:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.app.myapp" >
<uses-permission android:name="android.permission.READ_CONTACTS" />
...
</manifest>
这就是它所拥有的一切,即使安装完应用程序之后,用户也无法更改权限。这使得开发人员很容易处理权限,但这不是最好的用户体验。
Marshmallow(Android 6.0)系统中的权限更新
Marshmallow版本在权限处理方面带来了很多改变。并且它引入了运行时权限的概念。有些权限是在应用运行过程中申请的(并非在应用安装之前)。这些权限可由用户决定是否授予。对于已批准的权限也能在之后进行撤销。
这意味着针对Marshmallow版本上的应用处理权限时,有一些需要注意的问题。切记,targetSdkVersion
版本必须>=
23,并且模拟器/真机必须运行Marshmallow系统,才能看到新的权限管理模块。如果不是这种情况,请参阅向后兼容部分,来了解权限在配置上的行为。
普通权限
当你需要添加一个新的权限时,可以先通过Normal Permission简介查看该权限是否是PROTECTION_NORMAL
权限。在Marshmallow系统中,Google将特定的权限设计为“安全的”,并将它们称作“普通权限”。比如ACCESS_NETWORK_STATE
, INTERNET
等,这些不会产生损害的权限。普通权限是在安装时自动进行的授权,不会对用户弹出是否授权的提示。
重点:普通权限必须被添加到AndroidManifest
中:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.app.myapp" >
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>
运行时权限
如果你要添加的权限不属于普通权限的范畴,那么你需要以“运行时权限”的方式来处理。运行时权限是指,当应用运行过程中,在用到的时候才进行请求的权限。这些权限通常会以对话框的形式向用户请求授权,如下图所示:
添加“运行时权限”的第一步是将它添加到AndroidManifest
中:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.codepath.androidpermissionsdemo" >
<uses-permission android:name="android.permission.READ_CONTACTS" />
...
</manifest>
接下来,你需要初始化权限请求并处理对应的结果。下面的代码展示了在Activity
的上下文环境中如何处理“运行时权限”的处理,但这在Fragment
中也是可行的。
// MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 在实际的应用中,你可能会在用户执行的操作需要某个权限时才去请求它
getPermissionToReadUserContacts();
}
// 权限请求的标识符
private static final int READ_CONTACTS_PERMISSIONS_REQUEST = 1;
// 当用户执行读取联系人的操作时调用
public void getPermissionToReadUserContacts() {
// 1) 使用support库的ContextCompat.checkSelfPermission(...)避免检测构建版本,因为Context.checkSelfPermission(...)只在Marshmallow中可用
// 2) 总是执行权限的检测(即使权限已经被授予),因为用户可用通过“设置”在任意时刻撤销授权
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
// 权限还未被授予时
// 检测用户是否已经被询问该权限的授予并拒绝授权。如果这样的话,应该针对为什么需要该权限做出更多解释
if (shouldShowRequestPermissionRationale(
Manifest.permission.READ_CONTACTS)) {
// 在实际需要该权限时通过自定义的UI向用户解释为什么需要读取联系人的权限
}
// 消除异步请求,实际获取权限
// 它将展示权限请求的标准对话框界面
requestPermissions(new String[]{Manifest.permission.READ_CONTACTS},
READ_CONTACTS_PERMISSIONS_REQUEST);
}
}
// 调用requestPermissions(...)的请求回调
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String permissions[],
@NonNull int[] grantResults) {
// 确保它是最初的READ_CONTACTS权限请求
if (requestCode == READ_CONTACTS_PERMISSIONS_REQUEST) {
if (grantResults.length == 1 &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "Read Contacts permission granted", Toast.LENGTH_SHORT).show();
} else {
// showRationale = false 如果用户点击了不再提示, 否则为true
boolean showRationale = shouldShowRequestPermissionRationale( this, Manifest.permission.READ_CONTACTS);
if (showRationale) {
// 处理退化模式
} else {
Toast.makeText(this, "Read Contacts permission denied", Toast.LENGTH_SHORT).show();
}
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
}
权限分组
权限分组可以防止滥用用户的权限请求,同时允许开发者只要求在任意时间点内最小数量的权限请求。
相关的权限可被分到以下的任意一个权限组中。当应用请求某个特定权限组中的权限时(例如READ_CONTACTS),Android系统会询问用户更高级别的权限请求(CONTACTS)。这样,当应用以后再需要WRITE_CONTACTS权限时,Android系统会自动完成授权而不用再询问用户。
在与权限处理API的大多数交互中,你可能处理的是单个的权限而不是权限组。但是一定要注意API想要处理什么内容,因为权限和权限组都是字符串。
向后兼容性
当提到向后兼容性的时候,主要有两个场景需要考虑:
1、应用的目标API版本低于Marshmallow (TargetSdkVersion
< 23
),但模拟器/设备是Marshmallow系统:
- 你的应用将继续使用旧版本的权限处理模块。
-
AndroidManifest
中列出的所有权限都会在安装时被询问是否授权。 - 用户只能在应用安装之后撤销权限。对于这种情景的测试非常有必要,因为没有对应权限而执行特定的动作可能会导致不可预期的结果。
2、模拟器/设备上运行的是Marshmallow之前的系统,但应用的目标API是Marshmallow(TargetSdkVersion
>= 23
):
- 你的应用仍将继续使用旧版本的权限处理模块。
-
AndroidManifest
中列出的所有权限都会在安装时被询问是否授权。
如何请求授权
Google推荐了这部视频,当说起授权问题时,这里有四种模式需要考虑:
每种模式都指明了一种权限请求的不同方式。例如当请求一个关键但不清楚的权限时,使用一个介绍页面帮助理解为什么需要请求该权限。对于关键权限,例如相机应用需要camera权限,要先查询。次要功能可在之后的上下文中请求,例如在要求location权限的地理标记应用中。对于次要和不清楚的权限,如果真的需要,应该包含解释说明。
存储权限
重新思考下你是否需要读/写存储的权限(例如android.permission.WRITE_EXTERNAL_STORAGE
or android.permission.READ_EXTERNAL_STORAGE
),这可以使你访问存储卡上的所有文件。相反地,你应该使用上下文Context中的方法访问外部存储中特定包下的目录。你的应用总是有读/写以下目录的权限,所以不必再请求授权:
// 特定应用能够调用的,不需要请求外部存储权限的有:
// Environment.DIRECTORY_PICTURES, Environment.DIRECTORY_PODCASTS, Environment.DIRECTORY_RINGTONES,
// Environment.DIRECTORY_NOTIFICATIONS, Environment.DIRECTORY_PICTURES, or Environment.MOVIES
File dir = MyActivity.this.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
使用ADB管理权限
权限也可以通过命令行的adb
,使用以下命令进行管理。
列出所有的Android权限:
$ adb shell pm list permissions -d -g
抓取应用权限的状态:
$adb shell dumpsys package com.PackageName.enterprise
授予或撤销运行时权限:
$adb shell pm grant com.PackageName.enterprise some.permission.NAME
$adb shell pm revoke com.PackageName.enterprise android.permission.READ_CONTACTS
安装应用并授权全部权限:
$adb install -g myAPP.apk