android6.0动态权限与封装

1. 目标

此文章的最终目标:让童鞋们了解6.0动态权限的逻辑,以及学会封装动态权限申请,实现'一句话申请权限'

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)

调用此方法后,如果targetSdkVersion版本小于26,或者用户已经授权过对应组的权限,会直接回调activity的onRequestPermissionsResult方法,否则会弹出系统对话框询问用户是否允许该权限组,当用户点了拒绝或者同意权限,也都同意会回调activity的onRequestPermissionsResult方法。

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

  • 先来看看该方法的参数:
// 1. requestCode即调用ActivityCompat.requestPermissions时填的requestCode。
// 2. permissions即调用ActivityCompat.requestPermissions时填的permissions。
// 注意如果targetSdkVersion版本小于26,则permissions是空数组
// 3. grantResults即权限授权结果。
// 注意如果targetSdkVersion版本小于26,则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 {
    protected final String TAG = getClass().getSimpleName().replace("Activity", "Act");
    protected TipDialog tipDialog;
    private SparseArray listenerMap = new SparseArray<>();

    /**
     * 权限请求结果监听者
     */
    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 {
            int size = listenerMap.size();
            if (onPermissionResultListener != null) {
                listenerMap.put(size, onPermissionResultListener);
            }
            ActivityCompat.requestPermissions(this, permissions, size);
        }
    }

    /**
     * 显示提示"跳转到应用权限设置界面"的dialog
     * @param permission 具体的某个权限,用于展示dialog的内容文字。
     */
    private void showTipDialog(String permission, final OnPermissionResultListener onPermissionResultListener) {
        if (tipDialog == null) {
            tipDialog = new TipDialog(this);
        }
        // 确定按钮
        tipDialog.findViewById(R.id.btn_confirm).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                tipDialog.cancel();
                toAppDetailSetting();
            }
        });
        // 取消按钮
        tipDialog.findViewById(R.id.btn_cancel).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                tipDialog.cancel();
            }
        });

        tipDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
            @Override
            public void onCancel(DialogInterface dialog) {
                onPermissionResultListener.onReject();
            }
        });
        tipDialog.setTipText(PermissionUtil.getTip(permission));
        tipDialog.show();
    }

    /**
     * 跳转系统的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 (tipDialog != null) {
            tipDialog.cancel();
            tipDialog = null;
        }
        listenerMap.clear();
        listenerMap = null;
        super.onDestroy();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        OnPermissionResultListener onPermissionResultListener = listenerMap.get(requestCode);
        if (onPermissionResultListener != null) {
            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])) {
                        // 拒绝选了"不再提醒",一般提示跳转到权限设置页面
                        showTipDialog(permissions[i], onPermissionResultListener);
                    } else {
                        onPermissionResultListener.onReject();
                    }
                    return;
                }
            }
            onPermissionResultListener.onAllow();
        }
    }
}

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

  • 子类使用:
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里。等等
最后再去处理继承关系就好了。


觉得有用同学,点赞鼓励下呗。 个人公众号『Grade桂』,欢迎关注

你可能感兴趣的:(android6.0动态权限与封装)