摘要:
为什么系统禁用录音权限后,在Android 6.0以上版本手机运行崩溃?为什么清单文件声明了录音权限,Android 6.0以下版本仅第一次提示权限授予窗口?为什么使用运行时权限请求,返回权限以授予?怎么让Android应用程序在每次操作时识别系统是否禁用对应权限?如果你和TeachCourse一样存在很多很多的疑问,说明你还没明白传统在manifest清单文件声明权限和运行时权限请求之间的区别。
一、对比传统权限声明和运行时权限请求的区别
传统权限声明针对Android 6.0及其以下版本使用,Android 6.0对应的API版本23,声明的方式直接将所有应用程序用到的权限同一在manifest
清单文件中定义,使用标签
,应用程序点击安装的过程,罗列清单文件声明的所有权限,安装完成后用户可以选择是否授予应用程序某个隐私的权限,Android系统提供:允许、提示和禁止三种选择,下面看一组演示:
-
build.gradle
选择编译版本、目标版本都是API 19,运行在Android 4.4.2系统(华为)效果1:
-
build.gradle
选择编译版本、目标版本都是API 19,运行在Android 6.0.1系统(小米)效果2:
-
build.gradle
选择编译版本、目标版本都是API 23,运行在Android 4.4.2系统(华为)效果3:
-
build.gradle
选择编译版本、目标版本都是API 23,运行在Android 6.0.1系统(小米)效果4:
效果1演示传统权限授予过程,在完成安装的过程中可以选择某个权限是否允许、提示和禁止状态;效果2演示低版本应用程序在Android 6.0以上系统安装过程,默认授予应用程序清单文件声明的所有权限,小米手机测试无法修改权限状态;效果3和效果4演示API版本23开发的应用程序分别安装在低版本和高版本系统权限授予过程,安装在低版本时授予权限过程和传统的方式一样,用户可以修改权限的状态;安装到高版本时授予权限过程发生了很大变化,用户安装过程无法修改权限状态,最后运行应用程序的录音功能,出现闪退、崩溃现象。到这里,你是不是和TeachCourse一样,有一点点明白传统权限声明和运行时权限请求之间的区别吗?
二、深入理解运行时权限请求过程
是不是我们可以大致认为:使用API 23及其以上版本开发的应用程序安装在Android 6.0系统以下的手机,默认授予应用程序清单文件所有的权限,安装在Android 6.0系统以上的手机默认禁止清单文件声明的所有权限?应用程序在无法获取权限的过程,调用Android开发库提供的一些方法,某个方法返回null或属性为null,就可能导致使用部分功能时应用程序崩溃,而部分被禁止权限的功能虽然不会导致程序崩溃,但也无法获取正确的数值。
运行时权限的出现,一改传统清单文件一键授权的不足,防止用户安装过程的惯性操作,获取了用户某些隐私权限,这些权限包括:收集位置信息,读取短信内容,记录用户数据等,然后进行一些非法操作:发送短信订阅资费套餐,扣取手机话费等,为了用户隐私信息的安全,API 23开发的应用程序统一在运行时提醒用户授予权限,仅授予针对当前功能使用到的权限,未使用到的权限默认禁止。
那么如何兼容低版本的应用程序呢?以及如何让高版本的应用程序也能在Android 6.0以下系统正常运行?那可能就像文章开头演示的四种效果图
运行时权限涉及的几个过程:第一检查权限是否被授予,使用方法checkPermission()
;第二请求获取权限,使用方法requestPermissions()
;第三用户是否授予应用程序权限,监听回调方法onRequestPermissionsResult()
,为了防止应用API 23开发的应用程序在Android 6.0以上系统正常安装,在代码中添加权限检查,如下:
int flag = getPackageManager().checkPermission(android.Manifest.permission.RECORD_AUDIO,getPackageName());
if (PackageManager.PERMISSION_GRANTED==flag){
/**执行录音操作**/
...
}else {
/**提示用户操作**/
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, REQUEST_CODE_GET_RECORDER_PERMISSION);
}
针对效果图4,运行时请求获取录音权限,然后点击禁止后回调方法onRequestPermissionsResult()
,如下图:
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == REQUEST_CODE_GET_RECORDER_PERMISSION) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
...
} else {
...
ToastUtil.getInstance(this).showToast(Toast.LENGTH_SHORT, "录音权限被禁用,请在权限管理修改");
}
return;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
应用程序请求授予权限后,如果用户点击禁止,以后每次权限检查不再出现选择提示窗口,onRequestPermissionsResult()
方法返回权限的状态是PackageManager.PERMISSION_DENIED
,防止反复弹出要求用户授予权限弹窗,如果开发者仍然期待在用户没有禁止权限状态后,再次提醒用户授予权限,需要调用方法shouldShowRequestPermissionRationale()
,该方法的目的显示系统UI说明提示用户重新授予应用程序权限,Nexus 5测试运行效果,如下:
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == REQUEST_CODE_GET_RECORDER_PERMISSION) {
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
boolean isSecondRequest = ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[0]);
if (isSecondRequest)
/**重新请求授予权限,显示权限说明(该说明属于系统UI内容,区别第一次弹窗)**/
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, 0x11);
else
Toast.makeText(this, "录音权限被禁用,请在权限管理修改", Toast.LENGTH_SHORT).show();
}
return;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
查看shouldShowRequestPermissionRationale()
源码说明,详细了解该方法的使用:获取是否你应该通过显示UI说明请求授权的原因,只有当你没有获得该权限,同时当前上下文环境需要的权限没有明确和用户沟通——对于获取该权限有什么用处,这时候你应该调用该方法。比如说,如果你写了一个拍照功能的APP,请求了用户可能需要的拍照权限,而没有解释为什么请求的权限是必须的,可能用户没觉得不正常;然而如果当前APP在拍照时请求获取位置的权限,这时对于一个不精通技术的用户来说可能想知道定位和拍照是怎样的一种联系。在这个情景之下,你大概会选择通过一个显示UI说明请求授权的原因
/**
* Gets whether you should show UI with rationale for requesting a permission.
* You should do this only if you do not have the permission and the context in
* which the permission is requested does not clearly communicate to the user
* what would be the benefit from granting this permission.
*
* For example, if you write a camera app, requesting the camera permission
* would be expected by the user and no rationale for why it is requested is
* needed. If however, the app needs location for tagging photos then a non-tech
* savvy user may wonder how location is related to taking photos. In this case
* you may choose to show UI with rationale of requesting this permission.
*
*
* @param activity The target activity.
* @param permission A permission your app wants to request.
* @return Whether you can show permission rationale UI.
*
* @see #checkSelfPermission(android.content.Context, String)
* @see #requestPermissions(android.app.Activity, String[], int)
*/
public static boolean shouldShowRequestPermissionRationale(@NonNull Activity activity,
@NonNull String permission) {
if (Build.VERSION.SDK_INT >= 23) {
return ActivityCompatApi23.shouldShowRequestPermissionRationale(activity, permission);
}
return false;
}
说明:
在Android 6.0.1系统的小米手机测试,在用户禁止后,调用shouldShowRequestPermissionRationale
该方法没有显示说明;使用Android Studio内置的Nexus 5模拟器测试用户第一次请求权限弹窗只有DENY和ALLOW选项,在用户选择DENY后再次调用requestPermissions
方法,弹窗除了DENY和ALLOW选项外,还多了一个Never ask again复选框
三、关于ActivityCompat
的说明
在上面检查授予权限的代码中,TeachCourse使用了getPackageManager().checkPermission()
这个方法检查,考虑到谦容高低版本API的问题,还是推荐使用v4包下的ActivityCompat.checkSelfPermission()
这个静态方法或者父类ContextCompat.checkSelfPermission()
;请求权限推荐使用ActivityCompat.requestPermissions()
这个静态方法,如果第一次禁止后,重新弹窗显示UI说明,调用静态方法ActivityCompat.shouldShowRequestPermissionRationale()
后重新授权,具体可以查看ActivityCompat
源码理解它们之间的关系。
在API 23的版本中,查看ActivityCompat
的源码,上述的三个方法最终来自受保护的类ActivityCompatApi23
,在源码中检查了应用程序的API版本。
public static void requestPermissions(final @NonNull Activity activity,
final @NonNull String[] permissions, final int requestCode) {
if (Build.VERSION.SDK_INT >= 23) {
ActivityCompatApi23.requestPermissions(activity, permissions, requestCode);
}
...
}
响应用户授权状态的回调方法onRequestPermissionsResult()
属于ActivityCompat
内部的一个接口,如果没有猜错的话,仅在API 23以后的版本中,实现了ActivityCompat.OnRequestPermissionsResultCallback
接口的Activity子类,才能回调onRequestPermissionsResult()
方法,同时也会看到FragmentActivity、AppCompatActivity源码实现了上述接口。
public class ActivityCompat extends ContextCompat {
/**
* This interface is the contract for receiving the results for permission requests.
*/
public interface OnRequestPermissionsResultCallback {
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults);
}
...
四、运行时权限策略
提出了运行时权限,在运行应用程序的时候,每使用应用程序的一个功能开发者就需要请求授权一次,那必然会加大了开发者的工作量,请求权限的代码会变得很多,同时本来运行时权限的申请方式本来就比传统权限请求方式复杂,如果再让开发者一次次请求授权那肯定非常反感。为了解决权限反复多次请求的问题,Google采用了权限分组的策略:同一组的多个权限,只要获得了用户授予的一个权限,同时可以使用同组的其他权限,权限的分组情况如下图:
4.1 Permission-Group,具体可以查看源码android.Manifest.permission_group
- android.permission-group.CALENDAR
android.permission.READ_CALENDAR
android.permission.WRITE_CALENDAR
- android.permission-group.CAMERA
android.permission.CAMERA
- android.permission-group.CONTACTS
android.permission.READ_CONTACTS
android.permission.WRITE_CONTACTS
android.permission.GET_GET_ACCOUNTS
- android.permission-group.LOCATION
android.permission.ACCESS_COARSE_LOCATION
android.permission.ACCESS_FINE_LOCATION
- android.permission-group.MICROPHONE
android.permission.RECORD_AUDIO
- android.permission-group.PHONE
android.permission.READ_PHONE_STATE
android.permission.CALL_PHONE
android.permission.READ_CALL_LOG
android.permission.WRITE_CALL_LOG
android.permission.ADD_VOICEMAIL
android.permission.USE_SIP
android.permission.PROCESS_OUTGOING_CALLS
- android.permission-group.SENSORS
android.permission.BODY_SENSORS
- android.permission-group.SMS
android.permission.SEND_SMS
android.permission.RECEIVE_SMS
android.permission.READ_SMS
android.permission.RECEIVE_WAP_PUSH
android.permission.RECEIVE_MMS
- android.permission-group.STORAGE
android.permission.READ_EXTERNAL_STORAGE
android.permission.WRITE_EXTERNAL_STORAGE
参考资料:https://developer.android.google.cn/guide/topics/security/permissions.html#perm-groups