作为安卓开发者最头疼的一点,莫过于谷歌越来越快的版本更新速度。以及升级编译版本后需要面对的大量兼容性异常。尤其是今年电信终端产业协会(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

CALENDAR日历权限组

  • READ_CALENDAR读日历
  • WRITE_CALENDAR日历

    CALL_LOG通话记录权限组

    • READ_CALL_LOG读通话记录
    • WRITE_CALL_LOG通话记录
    • PROCESS_OUTGOING_CALLS处理传出呼叫

    CAMERA相机权限组

    • CAMERA

    CONTACTS通讯录权限组

    • READ_CONTACTS读通讯录
    • WRITE_CONTACTS通讯录
    • GET_ACCOUNTS账号

    LOCATION位置权限组

    • ACCESS_FINE_LOCATION访问精准位置
    • ACCESS_COARSE_LOCATION访问大概位置

    MICROPHONE麦克风权限组

    • RECORD_AUDIO录制音频

    PHONE电话权限组

    • READ_PHONE_STATE读电话状态
    • READ_PHONE_NUMBERS
    • CALL_PHONE打电话 
    • ANSWER_PHONE_CALLS接听呼入电话
    • ADD_VOICEMAIL添加语音信箱
    • USE_SIP使用SIP服务

    SENSORS传感器权限组

    • BODY_SENSORS物体传感器(一般指距离,光感,重力等等这些感应接收器,摇一摇功能一般都会用到)

    SMS

    • SEND_SMS
    • RECEIVE_SMS
    • READ_SMS
    • RECEIVE_WAP_PUSH接收WAP推送
    • RECEIVE_MMS

    STORAGE存储权限组

    • READ_EXTERNAL_STORAGE读外部存储
    • WRITE_EXTERNAL_STORAGE外部存储

    也就是说,当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 上有不少类似的工具,像PermissionsDispatcherRxPermissionseasypermissions等,核心思想就是使用一个透明的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处理应用间共享文件的问题。