近段时间浏览新闻经常会看到工信部通报某某app合规检查不合格,拒不整改,勒令全部下架这些信息,尤其是金融类app。个人信息的保护对用户确实是非常重要的,相信绝大多数行业工作者也感觉到了这些年国家对互联网和app的整治风向和对于用户信息保护的决心。

对于app的专项整改中,有一项就是app权限申请的规范:权限申请前需要向用户阐明申请权限的用处,接下来就是简单地封装了权限申请提示语弹框工具类,在第一次申请权限时统一弹框阐明app申请权限地用处。

这种做法是比较提倡的,因为用户同意授权的概率会更高。

项目中用到地三方权限申请框架是uitlcode库中PermissionUtils,引入方式


    implementation'com.blankj:utilcode:1.30.6'
    

如果您想引入Androidx版本


    implementation'com.blankj:utilcodex:1.30.6'
    

其实市面上也有很多比较成熟地权限申请框架,如郭霖大神的PermissionX,高扩展的权限申请框架,它就很好的实现了我们第一次申请权限就需要弹框解释申请权限理由和用处的功能,鉴于我们项目中引入了utilcode库,里面有现成的PermissionUtils,就不额外地去引入PermissionX了。


现在讲讲PermissionUtils的用法,它可以申请单个权限,也可以申请整个权限组的权限。首先需要在AndroidManifest.xml文件中申明申请的权限,如申请相机和读写手机存储权限:


    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    

PermissionUtils申请权限简单方式,如:


        PermissionUtils.permission(PermissionConstants.STORAGE).callback(new PermissionUtils.SimpleCallback() {
            @Override
            public void onGranted() {
                UIToast.showShort("onGranted");
            }

            @Override
            public void onDenied() {
                UIToast.showShort("onDenied");

            }
        }).request();
        

这里的callback是框架中最简单的回调SimpleCallback,它不能处理拒绝并勾选不再提示的逻辑,如果要想知道这些拒绝并且不再提示的权限,需要使用SingleCallback、FullCallback,如:


        PermissionUtils.permission(PermissionConstants.STORAGE).callback(new PermissionUtils.SingleCallback() {
            @Override
            public void callback(boolean isAllGranted,
                                 @NonNull List<String> granted,
                                 @NonNull List<String> deniedForever,
                                 @NonNull List<String> denied) {

            }
        }).request();
        

所以推荐使用后两者的回调,因为我们可以拿到哪些权限是已经允许的、哪些是拒绝的和拒绝并不再提示的,在这些回调中就可以处理自己的逻辑。


PermissionUtils可以申请多个权限组内包含的权限,当然也可以直接申请单个权限,如:


     PermissionUtils.permission(Manifest.permission.CAMERA).callback(new PermissionUtils.SingleCallback() {
            @Override
            public void callback(boolean isAllGranted,
                                 @NonNull List<String> granted,
                                 @NonNull List<String> deniedForever,
                                 @NonNull List<String> denied) {

            }
        }).request();
        

因为permission方法参数是一个不定数组类型,所以可一次申请多个权限组权限,也可以申请多个权限(非权限组),PermissionUtils里面会对需要申请的权限进行归纳处理。

当然,PermissionUtils也是遵循Android原始权限申请方式,定义了几个Listener,OnRationaleListener和OnExplainListener,从其命名就可知道这两个Listener的作用了,这里不细说。


前面说了下PermissionUtils的简单用法,接下来就是我们就是完全依赖框架这些特性去封装我们的PermissionManager工具类:在申请权限前,把为什么需要权限的理由通过弹框告知用户,这样的app才会让用户感到可靠,权限通过的成功率也高。

这里为了高扩展,我们也定义自己的权限组名和其对应需要申请的权限(这里的实现也是借鉴PermissionUtils的),因为有些权限是不必要申请的,如果申请了这些不必要权限,还需要说明申请的理由和用处(比如你申请了CAMERA,你就要解释申请CAMERA去干了啥)。如:


 /**
     * 自定义权限组名
     */
    public static final String SMS = "MDroidS.permission-group.SMS";
    public static final String CONTACTS = "MDroidS.permission-group.CONTACTS";
    public static final String LOCATION = "MDroidS.permission-group.LOCATION";
    public static final String MICROPHONE = "MDroidS.permission-group.MICROPHONE";
    public static final String PHONE = "MDroidS.permission-group.PHONE";
    public static final String CAMERA = "MDroidS.permission-group.CAMERA";
    public static final String STORAGE = "MDroidS.permission-group.STORAGE";
    public static final String ALBUM = "MDroidS.permission-group.ALBUM";

    /**
     * 自定义权限组的归类,用户可自由扩展需要申请的权限
     */
    private static final String[] GROUP_CAMERA = new String[]{"android.permission.CAMERA"};
    private static final String[] GROUP_SMS = new String[]{"android.permission.SEND_SMS"};
    private static final String[] GROUP_CONTACTS = new String[]{"android.permission.READ_CONTACTS"};
    private static final String[] GROUP_LOCATION = new String[]{"android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION"};
    private static final String[] GROUP_MICROPHONE = new String[]{"android.permission.RECORD_AUDIO"};
    private static final String[] GROUP_PHONE = new String[]{"android.permission.CALL_PHONE"};
    private static final String[] GROUP_STORAGE = new String[]{"android.permission.READ_EXTERNAL_STORAGE", "android.permission.WRITE_EXTERNAL_STORAGE"};

    private static final String[] GROUP_PERMISSION = new String[]{SMS, CONTACTS, LOCATION, MICROPHONE, PHONE, ALBUM, CAMERA};

上面是我们定义的权限组别名和其对应需要的权限,如果需要权限组其它权限,直接可在权限数组添加需要的权限。如PHONE,其对应的GROUP_PHONE里只有android.permission.CALL_PHONE,我们可继续添加android.permission.READ_PHONE_STATE等。

因为权限组别名是和权限申请理由一一对应,我们使用Map类型去保存这种对应关系:key值对应我们自定义的权限组别名,value值对应我们的申请理由和用处,权限组别名对应的申请理由和用处信息如下:

    <!--权限申请理由和用处描述-->
    <string name="permission_bluetooth_des">请允许使用蓝牙,以便…。</string>
    <string name="permission_camera_des">请允许使用相机,以便…。</string>
    <string name="permission_photo_des">请允许使用相册,以便…。</string>
    <string name="permission_contacts_des">请允许使用通讯录,以便…。</string>
    <string name="permission_location_des">请允许使用定位,以便…。</string>
    <string name="permission_microphone_des">请允许使用麦克风,以便…。</string>
    <string name="permission_storage_des">请允许使用存储,以便…。</string>
    <string name="permission_phone_des">请允许使用电话,以便打电话。</string>
    <string name="permission_sms_des">请允许使用短信,以便…。</string>

    <string name="dialog_permission_allow">允许</string>
    <string name="dialog_permission_refuse">拒绝</string>
    <string name="dialog_permission_title">申请权限</string>

    <string name="dialog_permission_remind_prefix">该功能需要访问您的</string>
    <string name="dialog_permission_remind_suffix">,请通过应用设置将权限打开。</string>

    <string name="dialog_title">温馨提示</string>
    <string name="dialog_btn_cancel">取消</string>
    <string name="dialog_btn_confirm">确认</string>

这里我们使用FullCallback实现拒绝并不再提示的处理逻辑,弹框引导用户去设置将权限打开,整体实现起来并不难,调用方法简单可应对大多数情况了。如:

        PermissionManager.permission(getContext(), new PermissionManager.OnPermissionGrantCallback() {
            @Override
            public void onGranted() {
                UIToast.showShort("onGranted");
            }

            @Override
            public void onDenied() {
                UIToast.showShort("onDenied");

            }
        }, PermissionManager.CAMERA, PermissionManager.MICROPHONE, PermissionManager.ALBUM);
        

我们只管调用,PermissionManager里面自动处理上面说的这些逻辑。这里需要申请CAMERA、MICROPHONE、ALBUM,我们就需要将三个权限组对应的理由描述信息通过弹框展示给用户,如:

图一
图二

图一就是我们需要阐明权限申请的理由,图二就是当我们拒绝了相机、麦克风权限并且不再提示时,弹框提醒用户去应用设置开启权限的情况,最后贴出来这个工具类的代码:


package com.littlejerk.sample.permission;

import android.annotation.SuppressLint;
import android.app.Activity;

import com.blankj.utilcode.util.ObjectUtils;
import com.blankj.utilcode.util.PermissionUtils;
import com.blankj.utilcode.util.StringUtils;
import com.littlejerk.library.manager.log.UILog;
import com.littlejerk.sample.R;
import com.littlejerk.sample.widget.dialog.AppDialogManager;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import androidx.annotation.NonNull;
import androidx.annotation.StringDef;

/**
 * @author : HHotHeart
 * @date : 2021/8/21 00:59
 * @desc : 权限申请弹框管理类
 */
public class PermissionManager {

    private static final String TAG = "PermissionManager";
    /**
     * 获取清单文件声明的权限列表
     */
    private static final List<String> PERMISSIONS = PermissionUtils.getPermissions();

    /**
     * 自定义权限组名
     */
    public static final String SMS = "MDroidS.permission-group.SMS";
    public static final String CONTACTS = "MDroidS.permission-group.CONTACTS";
    public static final String LOCATION = "MDroidS.permission-group.LOCATION";
    public static final String MICROPHONE = "MDroidS.permission-group.MICROPHONE";
    public static final String PHONE = "MDroidS.permission-group.PHONE";
    public static final String CAMERA = "MDroidS.permission-group.CAMERA";
    public static final String STORAGE = "MDroidS.permission-group.STORAGE";
    public static final String ALBUM = "MDroidS.permission-group.ALBUM";

    /**
     * 自定义权限组的归类,用户可自由扩展需要申请的权限
     */
    private static final String[] GROUP_CAMERA = new String[]{"android.permission.CAMERA"};
    private static final String[] GROUP_SMS = new String[]{"android.permission.SEND_SMS"};
    private static final String[] GROUP_CONTACTS = new String[]{"android.permission.READ_CONTACTS"};
    private static final String[] GROUP_LOCATION = new String[]{"android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION"};
    private static final String[] GROUP_MICROPHONE = new String[]{"android.permission.RECORD_AUDIO"};
    private static final String[] GROUP_PHONE = new String[]{"android.permission.CALL_PHONE"};
    private static final String[] GROUP_STORAGE = new String[]{"android.permission.READ_EXTERNAL_STORAGE", "android.permission.WRITE_EXTERNAL_STORAGE"};

    private static final String[] GROUP_PERMISSION = new String[]{SMS, CONTACTS, LOCATION, MICROPHONE, PHONE, ALBUM, CAMERA};

    /**
     * 权限申请弹框提示信息
     *
     * @param activity
     * @param callback
     * @param permissions {@link android.Manifest.permission#CALL_PHONE} 和 {@link PermissionManager#SMS}
     * @return
     */
    public static void permission(Activity activity,
                                  OnPermissionGrantCallback callback,
                                  @PermissionType String... permissions) {

        Map<String, String> map = PermissionManager.getPermissionDesMap(permissions);

        permission(activity, map, callback, permissions);
    }

    /**
     * @param activity    上下文
     * @param map         null:表示不弹权限申请用处信息框或无效权限
     *                    key:权限名,可以是单个权限或权限组名 value:申请该权限或权限组的用处信息
     * @param callback    权限回调
     * @param permissions 自定义权限组名
     */
    public static void permission(Activity activity,
                                  Map<String, String> map,
                                  final OnPermissionGrantCallback callback,
                                  @PermissionType String... permissions) {
        //表示不弹权限申请用处信息框或无效权限
        if (null == map) {
            permissionNoExplain(activity, callback, permissions);
            return;
        }

        if (activity == null) {
            UILog.e(TAG + " activity == null");
            callback.onDenied();
            return;
        }
        //需要申请的权限(单个权限或权限组)
        Set<String> deniedPermission = map.keySet();
        if (ObjectUtils.isEmpty(deniedPermission)) {
            callback.onGranted();
            return;
        }
        UILog.e(TAG + " deniedPermission:" + deniedPermission);

        StringBuilder sb = new StringBuilder();
        int i = 0;
        Set<String> manifestPermission = new LinkedHashSet<String>();

        for (String groupType : deniedPermission) {
            i++;
            String msg = map.get(groupType);
            sb.append(i).append("、").append(msg).append("\n");
            //权限组包含的所有权限
            String[] arrayPermission = PermissionManager.getPermissions(groupType);
            Collections.addAll(manifestPermission, arrayPermission);
        }
        String remindMsg = null;
        String sbStr = sb.toString();

        int endIndex = sbStr.lastIndexOf("\n");
        if (deniedPermission.size() == 1) {
            remindMsg = sbStr.substring(2, endIndex);
        } else {
            remindMsg = sbStr.substring(0, endIndex);
        }
        //权限组所包含的权限
        String[] permissionArray = manifestPermission.toArray(new String[manifestPermission.size()]);
        permissionExplain(activity, remindMsg, callback, permissionArray, permissions);

    }


    /**
     * 权限申请弹框解释申请理由或权限用处
     *
     * @param activity        上下文
     * @param remindMsg       权限申请理由或用处信息
     * @param callback        权限回调
     * @param permissionArray 权限组包含的权限,如{@link #GROUP_SMS}
     * @param permissions     自定义的权限组名,如{@link #SMS}
     */
    private static void permissionExplain(final Activity activity, String remindMsg,
                                          final OnPermissionGrantCallback callback,
                                          final String[] permissionArray,
                                          @PermissionType String... permissions) {
        //权限申请未通过,弹框提示
        AppDialogManager.getInstance().showPermissionRemindDialog(activity, remindMsg,
                (dialogInterface, i) -> callback.onDenied(),
                (dialogInterface, i) -> PermissionUtils.permission(permissionArray).callback(new PermissionUtils.FullCallback() {
                    @Override
                    public void onGranted(@NonNull List<String> permissionsGranted) {
                        callback.onGranted();
                    }

                    @Override
                    public void onDenied(@NonNull List<String> permissionsDeniedForever, @NonNull List<String> permissionsDenied) {
                        if (!ObjectUtils.isEmpty(permissionsDeniedForever)) {
                            //权限不再提示情况处理
                            permissionsDeniedForever(activity, callback, permissionsDeniedForever, permissions);
                        } else {
                            callback.onDenied();
                        }
                    }
                }).request());
    }

    /**
     * 处理权限不再提示的逻辑
     *
     * @param activity                 上下文
     * @param callback                 权限回调
     * @param permissionsDeniedForever 权限组不再提示的某些权限
     * @param permissions              自定义的权限组名,如{@link #SMS}
     */
    private static void permissionsDeniedForever(Activity activity,
                                                 OnPermissionGrantCallback callback,
                                                 List<String> permissionsDeniedForever,
                                                 @PermissionType String... permissions) {
        //根据返回的不再提示权限获取对应的自定义权限组
        List<String> requestPermissionList = getMatchPermissionGroup(permissionsDeniedForever, permissions);
        if (ObjectUtils.isEmpty(requestPermissionList)) {
            callback.onDenied();
            return;
        }

        //拼接设置权限提示语
        StringBuilder permissionRequestMsg = new StringBuilder();
        permissionRequestMsg.append(StringUtils.getString(R.string.dialog_permission_remind_prefix));
        for (String permission : requestPermissionList) {
            String permissionName = getPermissionName(permission);
            if (!StringUtils.isEmpty(permissionName))
                permissionRequestMsg.append(permissionName).append("、");
        }
        if (permissionRequestMsg.lastIndexOf("、") > 0) {
            permissionRequestMsg.deleteCharAt(permissionRequestMsg.lastIndexOf("、"));
        } else {
            callback.onDenied();
            return;
        }
        permissionRequestMsg.append(StringUtils.getString(R.string.dialog_permission_remind_suffix));
        AppDialogManager.getInstance().showPermissionSettingRemind(activity, permissionRequestMsg.toString(),
                (dialogInterface, i) -> callback.onDenied(),
                (dialogInterface, i) -> openAppSystemSettings());
    }

    /**
     * 获取对应权限列表
     *
     * @param rawPermissionList 权限名集合
     * @param permissions       自定义权限组名
     * @return
     */
    private static List<String> getMatchPermissionGroup(List<String> rawPermissionList, @PermissionType String... permissions) {
        if (ObjectUtils.isEmpty(rawPermissionList)) return null;
        List<String> permissionList = new LinkedList<>();
        if (!ObjectUtils.isEmpty(permissions)) {
            for (String permission : permissions) {
                for (String permissionRaw : getPermissions(permission)) {
                    if (rawPermissionList.contains(permissionRaw)) {
                        permissionList.add(permission);
                        break;
                    }
                }
            }
        } else {
            for (String permission : GROUP_PERMISSION) {
                for (String permissionRaw : getPermissions(permission)) {
                    if (rawPermissionList.contains(permissionRaw)) {
                        permissionList.add(permission);
                        break;
                    }
                }
            }
        }
        return permissionList;
    }

    /**
     * 权限申请不弹理由解释框
     *
     * @param activity    上下文
     * @param callback    权限申请回调
     * @param permissions 自定义权限组名
     */
    @SuppressLint("WrongConstant")
    private static void permissionNoExplain(final Activity activity,
                                            final OnPermissionGrantCallback callback,
                                            @PermissionType final String... permissions) {

        Set<String> manifestPermission = new LinkedHashSet<String>();
        for (String groupType : permissions) {
            //权限组包含的所有权限
            String[] arrayPermission = PermissionManager.getPermissions(groupType);
            Collections.addAll(manifestPermission, arrayPermission);
        }
        String[] permissionArray = manifestPermission.toArray(new String[manifestPermission.size()]);
        PermissionUtils.permission(permissionArray).callback(new PermissionUtils.FullCallback() {

            @Override
            public void onGranted(@NonNull List<String> permissionsGranted) {
                callback.onGranted();
            }

            @Override
            public void onDenied(@NonNull List<String> permissionsDeniedForever, @NonNull List<String> permissionsDenied) {
                if (!ObjectUtils.isEmpty(permissionsDeniedForever)) {
                    if (activity == null) {
                        callback.onDenied();
                        return;
                    }
                    permissionsDeniedForever(activity, callback, permissionsDeniedForever, permissions);

                } else {
                    callback.onDenied();
                }


            }
        }).request();
    }

    /**
     * 获取权限组
     *
     * @param permission 自定义权限组名
     * @return
     */
    public static String[] getPermissions(String permission) {

        switch (permission) {
            case CONTACTS:
                return GROUP_CONTACTS;
            case SMS:
                return GROUP_SMS;
            case STORAGE:
            case ALBUM:
                return GROUP_STORAGE;
            case LOCATION:
                return GROUP_LOCATION;
            case PHONE:
                return GROUP_PHONE;
            case MICROPHONE:
                return GROUP_MICROPHONE;
            case CAMERA:
                return GROUP_CAMERA;
            default:
                return new String[]{permission};
        }

    }


    /**
     * 获取请求权限中需要申请的权限和其相应用处信息
     *
     * @param permissions
     * @return key:权限名,可以是单个权限或权限组名 value:申请该权限或权限组的用处信息
     */
    private static Map<String, String> getPermissionDesMap(@NonNull String... permissions) {

        UILog.e(TAG + " permissions.length:" + permissions.length);
        //无效权限组
        if (ObjectUtils.isEmpty(permissions)) {
            UILog.e("无效权限,请确认权限是否正确或是否已在清单文件声明");
            return null;
        }

        Set<String> deniedPermissions = PermissionManager.getDeniedPermissionGroup(permissions);
        //无效权限组,即不在清单文件中
        if (null == deniedPermissions) {
            UILog.e("无效权限,请确认权限是否正确或是否已在清单文件声明");
            return null;
        }

        Map<String, String> map = new HashMap<String, String>();
        for (String permission : deniedPermissions) {
            String remindMsg = PermissionManager.getPermissionDes(permission);
            map.put(permission, remindMsg);
        }

        return map;

    }

    /**
     * 获取未通过的权限列表
     *
     * @param permissions
     * @return
     */
    private static Set<String> getDeniedPermissionGroup(String... permissions) {
        //无效权限(不在清单文件中)
        boolean isInvalid = true;
        LinkedHashSet<String> deniedPermissions = new LinkedHashSet<String>();

        for (int length = permissions.length, i = 0; i < length; ++i) {
            String permission = permissions[i];
            //权限组或单个权限
            String[] groupPermission = PermissionManager.getPermissions(permission);
            for (String aPermission : groupPermission) {
                //检查清单文件声明权限
                if (!PERMISSIONS.contains(aPermission)) {
                    UILog.e(TAG + "无效权限,请确认权限是否正确或是否已在清单文件声明:" + aPermission);
                    continue;
                }
                isInvalid = false;
                //权限未通过
                if (!PermissionUtils.isGranted(aPermission)) {
                    UILog.e(TAG + " denied:" + permission);
                    deniedPermissions.add(permission);
                    break;
                }
                UILog.e(TAG + " granted:" + permission);
            }
        }
        //无效权限(不在清单文件中)
        if (isInvalid) {
            return null;
        }

        return deniedPermissions;
    }

    /**
     * 获取请求权限相应的提示信息
     *
     * @param permission
     * @return
     */
    private static String getPermissionDes(String permission) {

        String remindMsg = null;
        switch (permission) {
            case STORAGE:
                remindMsg = StringUtils.getString(R.string.permission_storage_des);
                break;
            case ALBUM:
                remindMsg = StringUtils.getString(R.string.permission_photo_des);
                break;
            case CAMERA:
                remindMsg = StringUtils.getString(R.string.permission_camera_des);
                break;
            case LOCATION:
                remindMsg = StringUtils.getString(R.string.permission_location_des);
                break;
            case MICROPHONE:
                remindMsg = StringUtils.getString(R.string.permission_microphone_des);
                break;
            case CONTACTS:
                remindMsg = StringUtils.getString(R.string.permission_contacts_des);
                break;
            case PHONE:
                remindMsg = StringUtils.getString(R.string.permission_phone_des);
                break;
            case SMS:
                remindMsg = StringUtils.getString(R.string.permission_sms_des);
                break;
            default:
                //单个权限申请扩展
                break;
        }

        return remindMsg;
    }

    /**
     * 获取请求权限名
     *
     * @param permission 自定义权限组名
     * @return
     */
    private static String getPermissionName(String permission) {

        String name = null;
        switch (permission) {
            case STORAGE:
                name = "存储";
                break;
            case ALBUM:
                name = "相册";
                break;
            case CAMERA:
                name = "相机";
                break;
            case LOCATION:
                name = "定位";
                break;
            case MICROPHONE:
                name = "麦克风";
                break;
            case CONTACTS:
                name = "通讯录";
                break;
            case PHONE:
                name = "电话";
                break;
            case SMS:
                name = "短信";
                break;
            default:
                //单个权限申请扩展
                break;
        }

        return name;
    }

    /**
     * 判断权限是否已申请过
     * 特别说明:
     * 使用PermissionUtils三方框架权限申请时,如果要调用PermissionUtils.isGranted(permissionGroup)
     * 要确保申请的权限或权限组已经在清单文件声明过,不然会恒为false
     *
     * @param permissions
     * @return
     */
    public static boolean isGranted(String... permissions) {
        if (ObjectUtils.isEmpty(permissions)) {
            return false;
        }
        Set<String> deniedPermissions = PermissionManager.getDeniedPermissionGroup(permissions);
        if (null == deniedPermissions) return false;

        return ObjectUtils.isEmpty(deniedPermissions);
    }

    /**
     * 打开应用详情页
     */
    public static void openAppSystemSettings() {
        PermissionUtils.launchAppDetailsSettings();
    }


    /**
     * 自定义权限组类型
     */
    @StringDef({SMS, MICROPHONE, STORAGE, ALBUM, CONTACTS, PHONE, LOCATION, CAMERA})
    @Retention(RetentionPolicy.SOURCE)
    public @interface PermissionType {
    }

    /**
     * 权限申请回调
     */
    public interface OnPermissionGrantCallback {
        void onGranted();

        void onDenied();
    }

}

其中UILog和AppDialogManager替换成自己的log和dialog就好,这里就不贴出来了,只要处理权限组、权限组内权限和权限组对应描述信息就好了,只管增减权限。

最后自己整理了下完整的demo: PermissionDemo,有需要可以去看看。

APP合规检查系列文章:
Android 组件导出风险及防范
Android Activity防劫持方案
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐