(三) 安卓通用代码库 之 android6.0权限申请封装

通用核心代码库-code: 适合入门不久的同学学习借鉴编程思想,也可以直接拿来使用。

它包含了我从业几年以来积累的、最常用的封装类和基础框架等等,几乎每个项目都可以引入它,实现快速开发。

code的github地址:https://github.com/chenyugui/code

好了,开始介绍今天的主角: PermissionBaseActivity

1. 目标

虽然android6.0时代已经过去很久了,但是动态权限申请作为每个APP都需要用的功能,所以把它纳入「核心代码库」里了,今天还是继续讲一讲吧,主要是让新手同学学习学习其封装思想,不用依赖第三方框架,学会自己封装,也可以实现'一句话申请权限'

2. 前言

在android6.0之前,开发者无需在java代码里进行危险权限的申请,只需在AndroidManifest.xml文件里进行权限声明即可,但从android6.0开始,google为了用户的安全考虑,加入了动态权限机制,危险权限必须在代码里申请,在手机界面上会弹窗提示用户是否允许相应权限,当然,AndroidManifest.xml文件里一样还是要进行声明,别忘了这一点哦。(ps:加入了动态权限,虽然对开发者而言是麻烦了一些,但是对整体的android环境有一定的改善,流氓软件不能像以前那样流氓了。)

3. 什么是危险权限

危险权限,也可以理解成较敏感权限,有哪些呢,请看下表:

group:android.permission-group.CONTACTS
  permission:android.permission.WRITE_CONTACTS
  permission:android.permission.GET_ACCOUNTS
  permission:android.permission.READ_CONTACTS

group:android.permission-group.PHONE
  permission:android.permission.READ_CALL_LOG
  permission:android.permission.READ_PHONE_STATE
  permission:android.permission.CALL_PHONE
  permission:android.permission.WRITE_CALL_LOG
  permission:android.permission.USE_SIP
  permission:android.permission.PROCESS_OUTGOING_CALLS
  permission:com.android.voicemail.permission.ADD_VOICEMAIL

group:android.permission-group.CALENDAR
  permission:android.permission.READ_CALENDAR
  permission:android.permission.WRITE_CALENDAR

group:android.permission-group.CAMERA
  permission:android.permission.CAMERA

group:android.permission-group.SENSORS
  permission:android.permission.BODY_SENSORS

group:android.permission-group.LOCATION
  permission:android.permission.ACCESS_FINE_LOCATION
  permission:android.permission.ACCESS_COARSE_LOCATION

group:android.permission-group.STORAGE
  permission:android.permission.READ_EXTERNAL_STORAGE
  permission:android.permission.WRITE_EXTERNAL_STORAGE

group:android.permission-group.MICROPHONE
  permission:android.permission.RECORD_AUDIO

group:android.permission-group.SMS
  permission:android.permission.READ_SMS
  permission:android.permission.RECEIVE_WAP_PUSH
  permission:android.permission.RECEIVE_MMS
  permission:android.permission.RECEIVE_SMS
  permission:android.permission.SEND_SMS
  permission:android.permission.READ_CELL_BROADCASTS

很明显可以看到,一个group下面带有一个或多个permission,这是什么情况呢?
其实是这样的,google把相类似的权限都分到了对应的组里,这样有什么好处? 仅仅是为了好看、方便记忆?

不不,好处是:属于同一组的权限不用重复授权。例如,如果用户之前已经授权过WRITE_CONTACTS权限的话,当你申请获取READ_CONTACTS的时候,不需要等待用户同意授权,而是直接返回授权成功。

当然,既然是以分组的形式进行授权,那么系统的授权dialog上面提示的也会是具体的组名,而不是单个权限名。

4. 怎么申请危险权限

4.1 先看看相关的API:

// 第二个参数可以从`Manifest.permission`里取值
// 第三个参数会在`onRequestPermissionsResult`方法里回调回来
ActivityCompat.requestPermissions(final Activity activity,
            final String[] permissions, final int requestCode)

调用此方法后,如果系统版本版本小于android6.0、或者用户已经授权过对应组的权限、或者用户已经拒绝过该组权限并且选择了“不再提示”,都会直接回调activity的onRequestPermissionsResult方法,否则会弹出系统对话框询问用户是否允许该权限组,当用户点了拒绝或者同意权限,也都同样会回调activity的onRequestPermissionsResult方法。

4.2 那么,重点逻辑处理似乎就是在onRequestPermissionsResult方法里了。

  • 先来看看该方法的参数:
// 1. requestCode即调用ActivityCompat.requestPermissions时填的requestCode。
// 2. permissions即调用ActivityCompat.requestPermissions时填的permissions。
// 注意如果targetSdkVersion版本小于26,则permissions是空数组
// 3. grantResults即存储着权限授权结果。
// 注意如果系统版本小于android6.0,则grantResults是空数组
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
  • 所以,只要grantResults数组的size > 0 并且 其中一个为“拒绝授权”,则为有权限被拒绝了
for (int i = 0; i < grantResults.length; i++) {
    if (grantResults[i] == PackageManager.PERMISSION_DENIED) {// 拒绝权限
        // 有权限被决绝,执行拒绝权限后的逻辑,例如退出界面
        // xxx
        return;
    }
}
// 执行同意权限后的逻辑
// xxx
  • 应用权限设置界面跳转提醒
    权限拒绝有两种情况:1. 不勾选“不再提醒”; 2.勾选“不再提醒”。
    如果用户勾选了“不再提醒”,那么再次申请权限的时候,系统不会弹出授权dialog,此时我们的app应该给予提示,否则用户很有可能会感觉莫名其妙:怎么进来这个界面后又退出来了。

那么问题来了,怎么判断有没有勾选“不再提醒”,android提供了一个API:

// 1:用户拒绝了该权限,没有勾选"不再提醒",此方法将返回true。
// 2:用户拒绝了该权限,有勾选"不再提醒",此方法将返回 false。
// 3:如果用户同意了权限,此方法返回false
boolean flag = ActivityCompat.shouldShowRequestPermissionRationale(Activity , String[]])

所以,只要在判断出是拒绝权限的地方,判断出shouldShowRequestPermissionRationale返回false,即为需要提醒跳转。下面贴下代码:

// 循环判断权限,只要有一个拒绝了,则回调onReject()。 全部允许时才回调onAllow()
for (int i = 0; i < grantResults.length; i++) {
    if (grantResults[i] == PackageManager.PERMISSION_DENIED) {// 拒绝权限
        // 对于 ActivityCompat.shouldShowRequestPermissionRationale
        // 1:用户拒绝了该权限,没有勾选"不再提醒",此方法将返回true。
        // 2:用户拒绝了该权限,有勾选"不再提醒",此方法将返回 false。
        // 3:如果用户同意了权限,此方法返回false
        if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[i])) {
            // 拒绝选了"不再提醒",一般提示跳转到权限设置页面,
            // 并在dialog cancel的时候,执行onReject();
            showTipDialog(permissions[i]);
        } else {
            // 有权限被决绝,执行拒绝权限后的逻辑
            onReject();
        }
        return;
     }
}
// 执行同意权限后的逻辑
onAllow();

5. 动态请求封装,实现'一句话申请权限'

其实原理很简单,就是用一个PermissionBaseActivity父类来处理权限的申请和判断,然后利用‘延迟实现’的思想,通过回调的方式,把具体的同意或拒绝授权后的业务逻辑交给子类来实现。还是直接贴代码吧:

public class PermissionBaseActivity extends AppCompatActivity implements CancelAble {
    protected final String TAG = getClass().getSimpleName().replace("Activity", "Act");
    private SparseArray listenerMap;
    protected TipDialogCreator tipDialogCreator;
    private int requestCode = 1;

    @Override
    public void toCancel() {
        finish();
    }

    /**
     * 权限请求结果监听者
     */
    public interface OnPermissionResultListener {
        /**
         * 权限被允许
         */
        void onAllow();

        /**
         * 权限被拒绝
         */
        void onReject();
    }


    /**
     * 镜像权限申请
     *
     * @param onPermissionResultListener 申请权限结果回调
     */
    public void checkPermissions(final String[] permissions, OnPermissionResultListener onPermissionResultListener) {
        if (Build.VERSION.SDK_INT < 23 || permissions.length == 0) {
            // android6.0以下不需要申请,直接为"同意"
            if (onPermissionResultListener != null) {
                onPermissionResultListener.onAllow();
            }
        } else {
            if (onPermissionResultListener != null) {
                if (listenerMap == null) {
                    listenerMap = new SparseArray<>();
                }
                listenerMap.put(requestCode, onPermissionResultListener);
            }
            ActivityCompat.requestPermissions(this, permissions, requestCode);
            requestCode++;
        }
    }

    /**
     * 跳转系统的App应用详情页
     */
    protected void toAppDetailSetting() {
        Intent localIntent = new Intent();
        localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
        localIntent.setData(Uri.fromParts("package", getPackageName(), null));
        startActivity(localIntent);
    }


    @Override
    protected void onDestroy() {
        if (listenerMap != null) {
            listenerMap.clear();
        }
        listenerMap = null;
        super.onDestroy();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (listenerMap == null) {
            return;
        }
        final OnPermissionResultListener onPermissionResultListener = listenerMap.get(requestCode);
        if (onPermissionResultListener == null) {
            return;
        }
        listenerMap.remove(requestCode);
        // 循环判断权限,只要有一个拒绝了,则回调onReject()。 全部允许时才回调onAllow()
        for (int i = 0; i < grantResults.length; i++) {
            if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
                // 拒绝被拒绝权限
                // 对于 ActivityCompat.shouldShowRequestPermissionRationale
                // 1:用户拒绝了该权限,没有勾选"不再提醒",此方法将返回true。
                // 2:用户拒绝了该权限,有勾选"不再提醒",此方法将返回 false。
                // 3:如果用户同意了权限,此方法返回false
                if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[i])) {
                    // 拒绝选了"不再提醒",一般提示跳转到权限设置页面
                    if (tipDialogCreator == null) {
                        tipDialogCreator = new TipDialogCreator(this, this);
                    }
                    tipDialogCreator.showTipDialog(PermissionUtil.getTip(permissions[i]), new TipDialog.TipClickCallBack() {
                        @Override
                        public void onConfirm() {
                            toAppDetailSetting();
                            onPermissionResultListener.onReject();
                        }

                        @Override
                        public void onCancel() {
                            onPermissionResultListener.onReject();
                        }
                    });
                } else {
                    onPermissionResultListener.onReject();
                }
                return;
            }
        }
        onPermissionResultListener.onAllow();
    }
}

其中用了一个listenerMap来维护多个请求,避免如果同时有多个权限申请请求,onRequestPermissionsResult会乱套的。
处理完授权判断后,从map里移除listener,减少内存占用。

如果是要在Fragment界面进行申请权限,咱们也封装了一个PermissionBaseFragment,代码很简单:

public class PermissionBaseFragment extends Fragment {
    public void checkPermissions(final String[] permissions, PermissionBaseActivity.OnPermissionResultListener onPermissionResultListener) {
        Activity activity = getActivity();
        if (activity instanceof PermissionBaseActivity) {
            ((PermissionBaseActivity) activity).checkPermissions(permissions, onPermissionResultListener);
        } else {
            throw new ClassCastException("Want to use checkPermissions, The fragment's Activity must extends PermissionBaseActivity");
        }
    }
}
  • 子类使用:
checkPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, new OnPermissionResultListener() {
    @Override
    public void onAllow() {
        Log.d(TAG, "onAllow: ");
    }

    @Override
    public void onReject() {
        Log.d(TAG, "onReject: ");
    }
});

是不是很简洁咧,理解原理后,稍微封装下就好,也不用去引用什么第三方框架了。

6. 额外说一下,责任单一原则

大家都知道,写android app的时候,基本上都有一个BaseActivity,但是有些同学会把很多逻辑都一起写在BaseActivity这个类里,感觉像大杂烩一样,很不美观,后期有问题了或者需要优化了,定位不好定位。

建议,遵循责任单一原则:
权限相关的逻辑就写在PermissionBaseActivity里;
Mvp相关的绑定逻辑,就写在MvpBaseActivity里,等等。
最后再去处理继承关系就好了。

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

PS: 分享不易,觉得有用的同学,点个赞鼓励一下呗。可以关注公众号「Grade桂」,持续分享技术信息和职场经验

你可能感兴趣的:((三) 安卓通用代码库 之 android6.0权限申请封装)