作为安卓开发者最头疼的一点,莫过于谷歌越来越快的版本更新速度。以及升级编译版本后需要面对的大量兼容性异常。尤其是今年电信终端产业协会(TAF)发布了《移动应用软件高API等级预置与分发自律公约》。https://baike.baidu.com/item/移动应用软件高API等级预置与分发自律公约/22759862
逼着你升级,想不升级都不行。
下面将根据自己实际项目中升级开发版本的经验,对每个版本的注意事项做一下总结性回顾。方便自己以后查阅方便,也可以给有这方面需求的新手提供一点借鉴。
一、动态权限兼容
Android6.0 最大的改动就是加入了动态权限,谷歌为了简化安装流程,并且方便用户控制权限,Android允许在程序运行的时候动态控制权限。对于开发而言就是将targetSdkVersion设置为23,当运行在Android 6.0 +的手机上时,就会调用6.0相关的API,达到动态控制权限的目的。但是,如果仅仅是将targetSdkVersion设置为23,而在代码层面没有针对Android 6.0做适配,就可能在申请系统服务的时候,由于权限不足,引发崩溃。
也就是说,我们将Manifest中申请的权限大致分为两类,一类是普通权限,例如网络请求权限、WIFI状态等,
- ACCESS_NETWORK_STATE
- ACCESS_WIFI_STATE
使用这些权限无需做兼容处理,另一类则是敏感权限了,除了需要在Manifest中申请以外,在程序使用的过程中,我们还需要做兼容处理,否则一旦用户拒绝授权,程序将很容易发生SecurityException这个异常。
谷歌将敏感权限分为了以下十个大类:
Permission Group | Permissions |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
也就是说,当APP首次在6.0 以上的系统中使用到涉及以上权限组的功能时,系统将会自动弹窗询问用户是否授权。根据用户的选择,返回结果有3种情况:
1.用户同意授权。程序将一直拥有该权限,除非卸载重装或者在设置里手动更改授权,不会发生授权异常;
2.用户拒绝授权,但仅仅只限本次。程序被拒绝该权限,但是下次使用还会再次弹窗询问用户是否授权;
3.用户拒绝授权,并且勾选了一直拒绝。程序被拒绝该权限,并且下次使用将自动拒绝授权,不再弹窗提示。
当发生2、3所述的情况时,就需要程序作出兼容性处理,提示用户并且禁止使用该权限的功能。
那么如何进行兼容处理呢,谷歌官方的答案是这样的:
首先,在需要调用权限相关功能之前,调用
ActivityCompat.requestPermissions(activity, permissions, requestCode);方法,请求系统授权。源码如下:
public static void requestPermissions(final @NonNull Activity activity,
final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode) {
if (Build.VERSION.SDK_INT >= 23) {
if (activity instanceof RequestPermissionsRequestCodeValidator) {
((RequestPermissionsRequestCodeValidator) activity)
.validateRequestPermissionsRequestCode(requestCode);
}
activity.requestPermissions(permissions, requestCode);
} else if (activity instanceof OnRequestPermissionsResultCallback) {
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
final int[] grantResults = new int[permissions.length];
PackageManager packageManager = activity.getPackageManager();
String packageName = activity.getPackageName();
final int permissionCount = permissions.length;
for (int i = 0; i < permissionCount; i++) {
grantResults[i] = packageManager.checkPermission(
permissions[i], packageName);
}
((OnRequestPermissionsResultCallback) activity).onRequestPermissionsResult(
requestCode, permissions, grantResults);
}
});
}
}
调用该方法后,便会出现上面说的系统授权弹窗,一般来说,permissions数组里涉及了几个敏感权限组,就会有几个授权窗。
用户在对授权弹窗全部进行处理后,则会触发Activity的
onRequestPermissionsResult(requestCode, permissions, grantResults)
方法,所以我们需要重写该方法,对返回的授权结果进行处理。代码如下
@TargetApi(23)
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
try {
if (requestCode != RC_REQUEST_PERMISSION) {
return;
}
//这里对授权结果进行处理
} catch (Exception e) {
e.printStackTrace();
}
}
说明:permissions就是我们请求的权限列表,grantResults则是系统返回的授权结果,他有两种值:
PERMISSION_GRANTED = 0;授权
PERMISSION_DENIED = -1;拒绝
注意:按照上面的说法,grantResults=-1时是有两种情况的,即用户选择一直拒绝(情况3),以及用户仅本次拒绝(情况2)。为什么要区分这两种情况呢?因为这两种情况下,程序在处理上是有区别的:用户选择一直拒绝授权(情况3),我们往往需要提示用户,你需要进入应用设置中进行权限设置,才能使用此功能,因为系统不会再弹窗提示用户授权了。用户仅本次拒绝(情况2),我们只需要告知用户申请该功能的理由即可,因为下次使用该功能系统还会再弹窗提示用户授权。
谷歌在Activity中提供了
shouldShowRequestPermissionRationale(String permission)
方法用来区分这两种情况。源码如下:
public boolean shouldShowRequestPermissionRationale(@NonNull String permission) {
return getPackageManager().shouldShowRequestPermissionRationale(permission);
}
当返回值是true时表示用户选择了本次拒绝,返回false则表示用户选择了一值拒绝。
所以我们可以对返回结果做以下遍历处理
for (int i = 0; i < permissions.length; ++i) {
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
//用户授权,可以执行下一步处理
}else if(shouldShowRequestPermissionRationale(permissions[i])){
//用户选择了本次拒绝授权,禁止使用该功能,并提示用户
}else{
//用户选择了一直拒绝授权,禁止使用该功能,并提示用户需要进入应用设置中进行权限设置
}
}
可以看出来,使用方法有点类似startActivityFroResult。都需要对相机每一个涉及到的Activity做兼容处理,无论是从代码量和代码可读性上来讲都特别不友好。对于新作的项目还好,可对于已经拥有很大体量的项目来说,无论是兼容还是后期维护都是很痛苦的。所以接下来我们要做的是把它整合成一个工具类,尽可能在不动原有程序的情况下,实现上面的授权处理。
Github 上有不少类似的工具,像PermissionsDispatcher,RxPermissions,easypermissions等,核心思想就是使用一个透明的Activity,在这个Activity中请求权限和处理回调。这样便无需在具体的类里处理这些繁琐的逻辑了。
借鉴这些项目的思想,我做出了一个十分简单的权限工具类。只需一个方法,便可以实现权限的授权,不同版本的兼容,以及回调的正确处理了。具体源码请查看附件。
下面对该工具类做详细说明:
GPermisson.with(mContext).permisson(groupPermissions).callback(new PermissionCallback() {
@Override
public void onPermissionGranted() {
//授权访问
}
@Override
public void onPermissionReject(ArrayList<String> rejects, ArrayList<String> rationals) {
if(rejects.size()>0){
//用户拒绝授权,需要去设置里面重新打开才能使用
}else{
//这里拒绝了授权但是没有选择每次都拒绝,需要告知用户不授权没法使用该功能
}
}
}).request();
调用方法,就这么简单,groupPermissions传入你需要用到的权限数组。在onPermissionGranted和onPermissionReject里做相应的操作就可以了。
核心的类就两个,GPermisson 和 PermissionActivity 。有一点需要注意,由于Android8.0系统运行时权限行为更变,8.0以前的系统,我们只需要请求权限组里的一个权限,系统会自动授予所有权限;而8.0以上系统则只会授予这一个权限,其他权限在下次使用时自动授予。所以我们要使用某一权限,最好申请该权限组的所有权限。这样就不会有问题了。
GPermisson 类便对权限组进行了统一管理,如下所示
static {
GROP_MICROPHONE = new Permisson("麦克风权限",new String[]{Manifest.permission.RECORD_AUDIO});
GROP_STORAGE = new Permisson("存储权限",new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE});
GROP_CAMERA = new Permisson("相机权限",new String[]{Manifest.permission.CAMERA});
GROP_LOCATION = new Permisson("定位权限",new String[]{Manifest.permission.ACCESS_FINE_LOCATION,Manifest.permission.ACCESS_COARSE_LOCATION});
GROP_SMS = new Permisson("短信权限",new String[]{Manifest.permission.READ_SMS,Manifest.permission.RECEIVE_WAP_PUSH,Manifest.permission.RECEIVE_MMS,Manifest.permission.RECEIVE_SMS,Manifest.permission.SEND_SMS});
GROP_PHONE = new Permisson("设备权限",new String[]{Manifest.permission.READ_PHONE_STATE});
GROP_All = new Permisson("全部权限",new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.ACCESS_FINE_LOCATION,Manifest.permission.ACCESS_COARSE_LOCATION,Manifest.permission.READ_PHONE_STATE});
}
可以根据需要自行扩展维护。PermissionActivity为具体执行授权请求,和处理回调的透明Activity.API低于23的设备会跳过授权,只有API23以上的设备才会执行对应方法。代码十分简单,已看就懂,就不细说了。
下面重点来了,关于应用设置中进行权限设置有一个巨坑。如果在APP运行时,用户在权限设置界面关闭了某一个权限。那么系统将直接将这个应用从后台杀掉。但当你返回应用时,他不会从启动界面重新启动。而是从被杀掉的那个界面直接恢复。此时,会触发该Activity的onCreate方法,并且savedInstanceState不为空。如果不做兼容处理,一旦使用到里面的全局变量。将会造成各种崩溃异常。
所以里需要在所有Activity的基类中添加如下判断:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState != null ){
Intent intent=new Intent(this, LoadingActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
overridePendingTransition(0,0);
}
}
即当发生授权关闭导致应用被杀掉时,强制APP从启动界面开始恢复。
关于动态权限的问题,我们就将到这里了。下一章将讲一讲7.0版本的兼容性问题,剧透:通过FileProvider处理应用间共享文件的问题。