Android 6.0权限管理及其封装

前言

Android M出来很久了,N都快发布了,尽管如此还是要整理一下这块。6.0的权限机制变更为Runtime Permissions,运行时要请求权限,如果用户不授权的话,可能会导致崩溃。
本篇文章分三部分,一是权限的分类,二是运行时请求的基本代码,三是权限申请的封装,尽管现有的封装有很多。

  • 本文代码示例在BlogDemo中的 MPermission。
  • 自己的封装实现在 EasyPermission,fork的EasyPermission,去掉了注解,融合了PermissionGen,使代码能更好的书写,更专注于业务逻辑。

Attention

Future versions of the Android SDK might move a particular permission from one group to another. Therefore, don’t base your app’s logic on the structure of these permission groups.

For example, if your app requests the READ_CONTACTS permission, then the WRITE_CONTACTS permission, you shouldn’t assume that the system can automatically grant the WRITE_CONTACTS permission, even though it’s in the same permission group as READ_CONTACTS as of Android 8.0 (API level 26).

1. 权限分类

1.1 危险权限Dangerous Permission

Dangerous permissions cover areas where the app wants data or resources that involve the user’s private information, or could potentially affect the user’s stored data or the operation of other apps.
涉及用户隐私信息的权限、能修改用户存储数据的权限、影响其他app的权限等就是危险权限。

列张表,方便之后查阅:
Android 6.0权限管理及其封装_第1张图片

也可通过adb命令来查看:
adb shell pm list permissions -d -g

1.2 正常权限 Normal Permission

Normal permissions cover areas where your app needs to access data or resources outside the app’s sandbox, but where there’s very little risk to the user’s privacy or the operation of other apps.
在官网上查看:see Normal Permissions。基本上全是与用户信息无关的权限。

1.3 特殊权限 Particular Permission

  • SYSTEM_ALERT_WINDOW
  • WRITE_SETTINGS

这两个权限通过启动授权界面来开启。详细请参考 技术小黑屋的 聊一聊Android 6.0的运行时权限 。

了解了权限分类,然后再看请求的基本代码。其中会介绍系统API的含义。

2. 申请权限的基本代码

这块可以查看官方文档 Requesting Permissions at Run Time 先了解一些,此处给出在Demo中的一段代码,请求CAMERA权限。

1.判断权限是否已经授予,没有则申请


 int checkCameraPermission =  ContextCompat.checkSelfPermission(mContext, Manifest.permission.CAMERA);

    if (checkCameraPermission != PackageManager.PERMISSION_GRANTED) {
    /*
        用户未授权,谷歌推荐标准流程
    */
    if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(),Manifest.permission.CAMERA)) {
        //Show an expanation to the user asynchronously.说明请求原因,之前被用户拒绝过
        showRequestPermissionRationale("请授权", new DialogInterface.OnClickListener(){
            @Override 
            public void onClick(DialogInterface dialog, int which) {
                //点击确定时再次申请权限
                requestPermissions(new String[] { Manifest.permission.CAMERA},REQUEST_PERMISSION_CAMERA);
                    }
                });
    } else {
        //第一次申请时,请求授权
        requestPermissions(new String[] { Manifest.permission.CAMERA },REQUEST_PERMISSION_CAMERA);
    }
}

2.然后在onRequestPermissionsResult方法中接受返回值并进行判断:

@Override 
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,@NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    switch (requestCode) {
        case REQUEST_PERMISSION_CAMERA:
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // permission was granted,
                LogUtil.d("Camera Granted");
            } else {
                // permission denied
                LogUtil.d("Camera Denied");
                Toast.makeText(mContext, "未授权", Toast.LENGTH_SHORT).show();
                }
            }
            break;
    }
}

这里解释一下shouldShowRequestPermissionRationale()方法:

This method returns true if the app has requested this permission previously and the user denied the request.
用户在设置里取消了授权, 或之前拒绝请求过 时返回true。

If the user turned down the permission request in the past and chose the Don’t ask again option in the permission request system dialog, this method returns false.
The method also returns false if a device policy prohibits the app from having that permission.
第一次询问 , check了不再询问 时返回false。

另外要注意:
Activity中使用ActivityCompat.requestPermissions()方法申请权限,而在Fragment中直接使用requestPermissions()方法就可以了。但是在Fragment中系统在回调onRequestPermissionsResult()方法时会同时回调Activity中的onRequestPermissionsResult(),这点在使用要注意。

因为在权限申请时是固定的步骤,大量的重复代码,固定的流程,所以就会考虑封装。下面就介绍一下封装,我综合了GitHub上几个框架,做了自己的封装。

3. 封装权限申请

GitHub上有几个很出名,我融合了EasyPermission和PermissionGen,删去了注解,使用PermissionGen的请求方式,并利用回调来处理权限申请后的逻辑。不得不说,EasyPermission真的是很好的封装,考虑的很全面,尤其是支持跳到Settings界面打开权限申请。

修改后的EasyPermission的特点:
1. 使用PermissionGen的方式申请权限
2. 去掉了注解,只使用回调,代码更清晰,更专注于业务逻辑
3. 必须让权限申请类实现EasyPermission.PermissionCallback接口,否则抛出异常

因为权限的封装并不复杂,你也可以按照自家的业务需求再修改再封装。

使用示例:

public class MainFragment extends Fragment implements EasyPermission.PermissionCallback {
      @Override 
      public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        /*
            像PermissionGen那样申请权限,清晰明了
        */
        EasyPermission.with(this)//Activity或Fragment
            .addRequestCode(RC_SMS_PERM)//RequestCode
            .permissions(Manifest.permission.READ_SMS)//请求的权限
            //showRequestPermissionRationale时的对话框的提示信息
            .rationale(getString(R.string.rationale_sms))
            .request();
    }

    @Override 
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
            @NonNull int[] grantResults) {
        /*
            系统回调方法,传递给EasyPermission
        */
        EasyPermission.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
    }

    @Override 
    public void onPermissionGranted(int requestCode, List perms) {
        /*
            用户授予权限,做业务逻辑
        */
        Toast.makeText(getActivity(), "TODO: SMS Granted", Toast.LENGTH_SHORT).show();
    }

    @Override 
    public void onPermissionDenied(int requestCode, List perms) {
        /*
            用于拒绝授予权限,提示信息
        */
        Toast.makeText(getActivity(), "TODO: SMS Denied", Toast.LENGTH_SHORT).show();

        //用户点击了不再询问,弹出对话框去Settings界面开启,这段代码根据业务需求可以添加,也可删去
        EasyPermission.checkDeniedPermissionsNeverAskAgain(this, "授权啊,不授权没法用啊," + "去设置里授权大哥", perms);
    }

    @Override 
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        /*
            从Settings界面跳转回来,标准代码,就这么写
        */
        if (requestCode == EasyPermission.SETTINGS_REQ_CODE) {
            if(EasyPermission.hasPermissions(getContext(), Manifest.permission.READ_SMS)){
                //已授权,处理业务逻辑
                //...
            }else{
               Toast.makeText(getContext(),"没有权限,无法工作",Toast.LENGTH_SHORT).show();
            }
        }
    }
}

修改后的代码要精简的多,申请多个权限时只有全部授予才回调onPermissionGranted(),其他情况回调onPermissionDenied(),去掉注解后代码更加清晰。同时适配23以下的机器,不用关心。代码就按照6.0写法就OK。

4. 权限申请再封装

12.20日更新

第三步中抽出了EasyPermission在单个页面的使用,在多个页面写时,会发现其有大量的重复代码,所以我们就可以再进行封装。抽出父类BasePermissionFragmentBasePermissionActivity。以Fragment举例。

public abstract class BasePermissionFragment extends Fragment implements EasyPermission.PermissionCallback {
    protected Context mContext;
    private int mRequestCode;
    private String[] mPermissions;
    private PermissionCallBackM mPermissionCallBack;

    //rationale: 申请授权理由,在子类调用时使用该方法就OK
    protected void requestPermission(int requestCode, String[] permissions, String rationale,
            PermissionCallBackM permissionCallback) {
        this.mRequestCode = requestCode;
        this.mPermissionCallBack = permissionCallback;
        this.mPermissions = permissions;

        EasyPermission.with(this)
                .addRequestCode(requestCode)
                .permissions(permissions)
                //.nagativeButtonText(android.R.string.ok)
                //.positveButtonText(android.R.string.cancel)
                .rationale(rationale)
                .request();
    }


    @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
            @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        EasyPermission.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
    }

    @Override public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        /*
            从Settings界面跳转回来,标准代码,就这么写
        */
        if (requestCode == EasyPermission.SETTINGS_REQ_CODE) {
            if (EasyPermission.hasPermissions(mContext, mPermissions)) {
                //已授权,处理业务逻辑
                onEasyPermissionGranted(mRequestCode, mPermissions);
            } else {
                onEasyPermissionDenied(mRequestCode, mPermissions);
            }
        }
    }

    @Override public void onEasyPermissionGranted(int requestCode, String... perms) {
        if (mPermissionCallBack != null) {
            mPermissionCallBack.onPermissionGrantedM(requestCode, perms);
        }
    }

    @Override public void onEasyPermissionDenied(final int requestCode, final String... perms) {
        //rationale: Never Ask Again后的提示信息
        if (EasyPermission.checkDeniedPermissionsNeverAskAgain(this, "授权啊,不授权没法用啊," + "去设置里授权大哥", android.R.string.ok,
                                                               android.R.string.cancel,
                                                               new DialogInterface.OnClickListener() {
                                                                   @Override public void onClick(DialogInterface dialog,
                                                                           int which) {
                                                                       if (mPermissionCallBack != null) {
                                                                           mPermissionCallBack.onPermissionDeniedM(
                                                                                   requestCode, perms);
                                                                       }
                                                                   }
                                                               }, perms)) {
            return;
        }

        if (mPermissionCallBack != null) {
            mPermissionCallBack.onPermissionDeniedM(requestCode, perms);
        }
    }
}

父类抽出了公有方法:onRequestPermissionsResultonActivityResultonEasyPermissionGrantedonEasyPermissionDenied 这些方法,暴露给子类的回调方法PermissionCallBackM 就更加简洁,如下所示:

        // 子类中
        requestPermission(
                RC_SMS_PERM,
                new String[] { Manifest.permission.READ_SMS },
                getString(R.string.rationale_sms),
                new PermissionCallBackM() {
                    @Override public void onPermissionGrantedM(int requestCode, String... perms) {
                        Toast.makeText(getActivity(), "TODO: SMS Granted", Toast.LENGTH_SHORT).show();
                    }

                    @Override public void onPermissionDeniedM(int requestCode, String... perms) {
                        Toast.makeText(getActivity(), "TODO: SMS Denied", Toast.LENGTH_SHORT).show();
                    }
                });

封装后的类在GitHub上 easypermissions,一些基础示例在samples中。同样也可查看BlogDemo中的 MPermission。

结语:

6.0运行时权限的出现确实加大了代码的复杂度,需要考虑的东西多了,产品也得出文案-_-!,封装成这样也算可以更多的专注于业务逻辑了,方便写代码。


参考:

  • 官方文档:
    Android 6.0 Changes
    Working with System Permissions
    Normal and Dangerous Permissions
    requesting permissions

  • 一篇很全面的文章
    Everything every Android Developer must know about new Android’s Runtime Permission

  • 好文章
    聊一聊Android 6.0的运行时权限

  • 这篇文章的处理方式一刀切,全都跳去Settings界面,也不错
    Android 6.0: 动态权限管理的解决方案

你可能感兴趣的:(Android,Java等基础知识)