一、 官方推荐的权限最佳实践
如果没有节制地频繁请求权限很容易使用户反感,如果用户发现app需要大量的敏感权限,很可能会拒绝使用甚至直接卸载。以下几点可以有效地提升用户的使用体验。
1. 考虑使用Intent
在很多情况下,你可以有两种选择实现你的操作,一种是直接app中请求比如摄像头这样的权限,然后调用摄像头APIs去控制摄像头并获取照片。这种方式可以使你对摄像头有全部的控制权,并且可以自定义相关的UI。
然而,如果你不需要完全控制,那么你只需要使用ACTION_IMAGE_CAPTURE intent来请求图片。当你发送这个intent,系统会自动询问用户打开哪个照相app(加入手机上安装不止一个照相app)。用户从选中的照相app中选择照片后,你的app就会从onActivityResult()方法中得到需要的照片。
类似的,如果你需要打电话,访问用户的通讯录等等,你也可以发送相应的intent,或者直接请求相应的权限。以下是两种方式的优缺点:
如果自己申请权限:
- 你将可以完全控制想要的权限,但是同时也增加了复杂度,例如你要设计相应的UI界面
- 一旦用户同意了你的权限申请,不管是在使用时还是安装阶段(根据用户手机的系统版本),你将可以一直使用该权限。但是一旦用户拒绝了你的权限申请(或者随后撤回了权限),你的app将无法使用相关的权限和功能
如果使用Intent:
- 首先你不需要自己设计UI界面,拥有该权限的app会提供UI,那么同时也意味着你将不能控制用户的使用体验,用户将会被一个你甚至完全不知道的app所影响。
- 如果用户有不止一个相关的app,系统将会提示用户做出选择。如果用户不勾选默认的操作,那么每次调用该权限的时候都会弹出提示框。
2. 不要同时申请大量的权限
如果用户使用的Android 6.0(API为23)及以上版本,用户需要在使用app时选择是否允许使用某权限。如果你一次性向用户申请大量的权限,用户会很反感甚至直接退出app。所以,你应该在只有用到某权限时才询问用户。
在有些情况下,你可能需要不止一个权限。你应该在启动app时请求权限,例如,你要做一个照片相关的app,你需要申请摄像头权限。当用户第一次打开app时,他们不会惊讶app询问使用摄像头的权限。同时app也需要分享照片给通讯录中好友,你不应该在app首次启动时申请READ_CONTACTS权限,而是当用户分享照片再去申请。
3. 解释为什么你需要权限
当你调用requestPermissions()方法时,系统会自动弹出权限对话框展示相应的权限描述,但是不会显示申请的原因。这会在某种程度上给用户造成困惑,所以在调用requestPermissions()前解释一下申请的原因会比较合适。
例如,一个图片应用想要地址服务以给图片标出地理标签,一个普通用户可能不明白为什么照片需要地理信息,甚至困惑app为什么要申请地址权限。因此,在调用requestPermissions()前告诉用户申请的原因就显得很有必要了。
至于如何在代码中实现显示申请原因下面的代码分析中会提及。
4. 测试权限
从Android 6.0(API为23)开始,用户可以在任意时刻同意和拒绝权限,而不是像之前版本安装时做一次决定。在Android 6.0之前,你可以假定app所有在manifest文件声明的权限是已经通过了。但是在Android 6.0及更高版本,你不能再有这样的假定。
以下这些建议将会在Android 6.0及更高版本帮你识别权限相关的代码问题:
- 确认你的app当前需要的权限和相关的代码
- 测试用户可以通过权限保护服务
- 测试各种同意和拒绝的权限组合,例如,一个照相app可能会在manifest文件中罗列CAMERA, READ_CONTACTS, and ACCESS_FINE_LOCATION权限,你应该测试每个权限同意或拒绝,并且保证app可以有相应的处理。
- 在命令行中使用adb工具管理权限:
按组罗列权限和状态:$ adb shell pm list permissions -d -g
同意和拒绝一个或者多个权限:
$ adb shell pm [grant | revoke] …
例如:
打开READ_CONTACTS权限
adb shell pm grant com.name.app android.permission.READ_CONTACTS
关闭READ_CONTACTS权限
adb shell pm revoke com.name.app android.permission.READ_CONTACTS
二、 权限代码分析
以申请摄像头为例:
首先需要判断是否要申请摄像头权限:
public void showCamera(View view) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
requestCameraPermission();
} else {
showCameraPreview();
}
}
正如代码中注释那样,需要先判断摄像头权限是否有限,如果权限还未有效,需要调用申请权限方法,如果已经有效,则直接显示摄像头预览界面。
打开摄像头预览界面不是本文的重点,所以相关的代码就不关注了。那么接下来看一下requestCameraPermission方法。
private void requestCameraPermission() {
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.CAMERA)) {
Snackbar.make(mLayout, R.string.permission_camera_rationale,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.ok, new View.OnClickListener() {
@Override
public void onClick(View view) {
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.CAMERA},
REQUEST_CAMERA);
}
})
.show();
} else {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA},
REQUEST_CAMERA);
}
}
上述代码先是判断权限是否已经被拒绝,如果被拒绝则通过Snackbar展示权限申请的原因,如果用户同意将会再次申请权限。权限申请的结果是在onRequestPermissionsResult返回的,下面来看这部分代码:
* Callback received when a permissions request has been completed. */
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == REQUEST_CAMERA) {
if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Snackbar.make(mLayout, R.string.permision_available_camera,
Snackbar.LENGTH_SHORT).show();
showCameraPreview();
} else {
Snackbar.make(mLayout, R.string.permissions_not_granted,
Snackbar.LENGTH_SHORT).show();
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
三、第三方SDK:EasyPermissions
Github上有一个比较火且简单易懂的第三方SDK可以简化权限管理,EasyPermissions可以详见github地址。接下来会大致介绍一下其用法,最后会发现其实和系统提供的权限管理很相似,理解起来也很简单。
1. 简单用法
首先需要在build.gradle文件中引入包,操作如下:
dependencies {
compile 'pub.devrel:easypermissions:0.1.7'
}
还以申请摄像头权限为例:
public class MainActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
@Override
public void onPermissionsGranted(int requestCode, List
list) {
}
@Override
public void onPermissionsDenied(int requestCode, List
list) {
}
}
首先需要在Activity实现EasyPermissions.PermissionCallbacks接口,该接口提供了onPermissionsGranted和onPermissionsDenied两个方法,也即权限申请成功和失败的回调方法,而EasyPermissions.PermissionCallbacks又实现了ActivityCompat.OnRequestPermissionsResultCallback,该接口提供了onRequestPermissionsResult方法,相当于EasyPermissions将系统的权限申请结果回调方法又进行了二次封装,同时提供了权限申请成功和失败的回调方法。
同时触发摄像头权限申请方法如下:
@AfterPermissionGranted(RC_CAMERA_PERM)
public void cameraTask() {
if (EasyPermissions.hasPermissions(this, Manifest.permission.CAMERA)) {
Toast.makeText(this, "TODO: Camera things", Toast.LENGTH_LONG).show();
} else {
EasyPermissions.requestPermissions(this, getString(R.string.rationale_camera),
RC_CAMERA_PERM, Manifest.permission.CAMERA);
}
}
此处会先调用EasyPermissions.hasPermissions方法判断是否允许使用该权限,如果返回值为ture表示已经申请成功过该权限,则直接使用即可,如果返回值为false表示还没有申请过该权限,那么可以通过EasyPermissions.requestPermissions方法进行申请,同时给出申请原因文案。
通过查看EasyPermissions.hasPermissions的源码,可以看到该方法可以接收多个参数,即可以同时检查多个权限。
AfterPermissionGranted注解是可选的,如果有该注解的话,那么当request值对应的权限申请通过的话会自动调用该方法。
需要特别说明的是,当用户在系统弹出的权限申请对话框中拒绝权限并且勾选不再询问,那么下次系统讲不会自动尝试申请,但是可以在onPermissionsDenied方法中通过弹框的方式解释app需要该权限的理由,如果用户同意的话会再次尝试请求。
2. 源码分析
首先来看EasyPermissions.hasPermissions方法,可以看到先是有一个版本检查,因为Android 6.0之前是不需要在运行时检查权限的,然后就是调用系统提供的ContextCompat.checkSelfPermission方法,所以这个方法好理解的。
public static boolean hasPermissions(Context context, String... perms) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
Log.w(TAG, "hasPermissions: API version < M, returning true by default");
return true;
}
for (String perm : perms) {
boolean hasPerm = (ContextCompat.checkSelfPermission(context, perm) ==
PackageManager.PERMISSION_GRANTED);
if (!hasPerm) {
return false;
}
}
return true;
}
然后是权限申请requestPermissions方法,首先是通过checkCallingObjectSuitability判断版本号和是否是Acticity或Fragment调用,然后是根据入参拼接权限申请解释文案并通过对话框显示给用户,如果用户点击同意将会调用系统提供的requestPermissions方法,如果用户点击取消,只直接返回权限申请拒绝的回调方法onPermissionsDenied。
public static void requestPermissions(final Object object, String rationale,
@StringRes int positiveButton, @StringRes int negativeButton, final int requestCode, final String... perms) {
checkCallingObjectSuitability(object);
final PermissionCallbacks callbacks = (PermissionCallbacks) object;
boolean shouldShowRationale = false;
for (String perm : perms) {
shouldShowRationale =
shouldShowRationale || shouldShowRequestPermissionRationale(object, perm);
}
if (shouldShowRationale) {
Activity activity = getActivity(object);
if (null == activity) {
return;
}
AlertDialog dialog = new AlertDialog.Builder(activity)
.setMessage(rationale)
.setPositiveButton(positiveButton, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
executePermissionsRequest(object, perms, requestCode);
}
})
.setNegativeButton(negativeButton, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
callbacks.onPermissionsDenied(requestCode, Arrays.asList(perms));
}
}).create();
dialog.show();
} else {
executePermissionsRequest(object, perms, requestCode);
}
}
最后是onRequestPermissionsResult方法,该方法接收系统的权限申请结果方法,并做统一的处理。可以看到也是先通过checkCallingObjectSuitability判断版本号和是否是Acticity或Fragment调用,然后根据不同权限申请结果分别放置到通过和拒绝列表,可以看到如果拒绝列表不为空直接返回申请失败的回调,当成功列表不为空调用之前包含AfterPermissionGranted注解的方法,完成后续的业务动作。
public static void onRequestPermissionsResult(int requestCode, String[] permissions,
int[] grantResults, Object object) {
checkCallingObjectSuitability(object);
PermissionCallbacks callbacks = (PermissionCallbacks) object;
ArrayList
granted = new ArrayList<>();
ArrayList
denied = new ArrayList<>();
for (int i = 0; i < permissions.length; i++) {
String perm = permissions[i];
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
granted.add(perm);
} else {
denied.add(perm);
}
}
if (!granted.isEmpty()) {
callbacks.onPermissionsGranted(requestCode, granted);
}
if (!denied.isEmpty()) {
callbacks.onPermissionsDenied(requestCode, denied);
}
if (!granted.isEmpty() && denied.isEmpty()) {
runAnnotatedMethods(object, requestCode);
}
}
结语
至此算是讲完了Android权限管理最佳实践的所有内容,第一部分是官方的操作建议,第二部分是系统提供的权限管理方案,第三部分是Github上比较成熟的SDK的简单用法和源码分析,希望对各位读者有帮助。