Android 动态权限设计 (权限的申请与处理)


Demo下载地址:https://pan.baidu.com/s/1dnaugm


需求:

最近把APP的TargetSdk从21提高至25,梳理了一下,APP中有多个地方用到了动态权限。
所以,需要把动态权限的申请与处理统一设计。

动态权限的基础知识,不再累述,请自行查询资料。
安卓端动态权限请求与处理的过程,通常如下:

  1. 检测权限
    ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED
  2. 在Activity中请求权限
    ActivityCompat.requestPermissions(activity, permissions, resultCode);
  3. APP弹出系统的授权提示框,让用户授权(用户可授权、也可取消、也可选择不再提醒)
  4. 在Activity的onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)方法中,返回用户授权的信息(请求码,请求的权限集合、每个权限授权的情况集合)
    a) 如果已都已授权,可以继续操作,也可以是什么也不做,等待用户重新操作;
    b) 如果未授权,引导用户去授权;

常见问题分析 与 真机实验:

总结安卓动态权限时,在网上也看了不少资料,反应最多的问题就是:
无法绝对检测权限的状态(授予、拒绝、禁止(不再提醒))!!!
原因如:各大手机厂商的定制或自身的权限体系与谷歌不一致、checkSelfPermission无效、shouldShowRequestPermissionRationale无效等等,对应的解释也是千奇百怪。
与其那么纠结,不如自己动手实验一下,看一看动态权限的判断是否如传说中的那么艰难。

  1. 实验目的:测试检测权限状态的方法
  2. 实验机器:小米7.1.2 三星6.0.1
  3. 查询官方资料,查找对应的API
用户权限状态检测的 主要方法有3种,谷歌官方大概释义如下:
//方法1:检测是否授予某权限,如果是返回0,反之,返回-1
ContextCompat.checkSelfPermission(activity, permission);
//方法2:是否应该向用户显示请求权限的原因说明(true/false)
ActivityCompat.shouldShowRequestPermissionRationale(activity, permission);
//方法3:判断程序的某操作是被允许的还是拒绝的还是忽略的
AppOpsManagerCompat.noteProxyOp(activity, op, activity.getPackageName());
  1. Google将权限进行了分组,申请属于normal permissions的权限在manifest中声明即可,对于dangerous permissions 权限必须运行时动态申请。
    normal permissions权限如下:
android.permission.ACCESS_LOCATION_EXTRA_COMMANDS
android.permission.ACCESS_NETWORK_STATE
android.permission.ACCESS_NOTIFICATION_POLICY
android.permission.ACCESS_WIFI_STATE
android.permission.ACCESS_WIMAX_STATE
android.permission.BLUETOOTH
android.permission.BLUETOOTH_ADMIN
android.permission.BROADCAST_STICKY
android.permission.CHANGE_NETWORK_STATE
android.permission.CHANGE_WIFI_MULTICAST_STATE
android.permission.CHANGE_WIFI_STATE
android.permission.CHANGE_WIMAX_STATE
android.permission.DISABLE_KEYGUARD
android.permission.EXPAND_STATUS_BAR
android.permission.FLASHLIGHT
android.permission.GET_ACCOUNTS
android.permission.GET_PACKAGE_SIZE
android.permission.INTERNET
android.permission.KILL_BACKGROUND_PROCESSES
android.permission.MODIFY_AUDIO_SETTINGS
android.permission.NFC
android.permission.READ_SYNC_SETTINGS
android.permission.READ_SYNC_STATS
android.permission.RECEIVE_BOOT_COMPLETED
android.permission.REORDER_TASKS
android.permission.REQUEST_INSTALL_PACKAGES
android.permission.SET_TIME_ZONE
android.permission.SET_WALLPAPER
android.permission.SET_WALLPAPER_HINTS
android.permission.SUBSCRIBED_FEEDS_READ
android.permission.TRANSMIT_IR
android.permission.USE_FINGERPRINT
android.permission.VIBRATE
android.permission.WAKE_LOCK
android.permission.WRITE_SYNC_SETTINGS
com.android.alarm.permission.SET_ALARM
com.android.launcher.permission.INSTALL_SHORTCUT
com.android.launcher.permission.UNINSTALL_SHORTCUT

我们先来看看将normal 和 dangerous的权限 同时在6.0+手机上的效果吧:
我们准备测试的权限如下,共6个:
前面的4个为危险权限,分为3类(SD卡读写、相机、短信);
后面的2个为标准权限,分为2类(音频调节、桌面快捷方式);

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.READ_SMS"/>
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
    <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
三星6.0.1.jpg
小米7.1.2.jpg

通过上面两图可以看到,申请同样的权限,在不同手机上显示的效果是不同的:
三星只显示了危险的权限,但是位置权限我们没有申请,它也给默认显示了出来;
小米将危险权限和标准权限都显示了出来,不过标准权限默认都显示着禁用的图标,清单中倒是没有位置权限,却多了后台弹出等权限项。
推测:不同厂商对于授权页面都有自己的定制规则。

  1. 测试方法1:检测是否授予某权限,如果是,返回0,反之,返回-1
    ContextCompat.checkSelfPermission(activity, permission);
//动态请求的权限数组
String[] permissions =new String[]{
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.CAMERA,
                Manifest.permission.READ_SMS,
                Manifest.permission.INSTALL_SHORTCUT,
                Manifest.permission.MODIFY_AUDIO_SETTINGS};
//测试
Log.i("checkSelfPermission", "--------------------------------"+permissions.length);
        for (String permission : permissions) {           
            int isGranted = ContextCompat.checkSelfPermission(activity, permission);
            if (isGranted == PackageManager.PERMISSION_GRANTED) {
                //已授权
                Log.i("checkSelfPermission", PermissionUtils.getInstance().getPermissionName(permission)+ ":   已授权");
            }else if(isGranted == PackageManager.PERMISSION_DENIED){
                //未授权的
                Log.i("checkSelfPermission", PermissionUtils.getInstance().getPermissionName(permission)+ ":   未授权");
            }
        }
        Log.i("checkSelfPermission", "--------------------------------"+permissions.length);

运行两个手机,输出的结果如下:

三星:
02-26 15:52:07.376 3247-3247/iwangzhe.paizhaocaiqie I/checkSelfPermission: --------------------------------5
02-26 15:52:07.396 3247-3247/iwangzhe.paizhaocaiqie I/checkSelfPermission: --文件存储:   未授权
02-26 15:52:07.396 3247-3247/iwangzhe.paizhaocaiqie I/checkSelfPermission: --相机/拍照:   未授权
02-26 15:52:07.396 3247-3247/iwangzhe.paizhaocaiqie I/checkSelfPermission: --短信:   未授权
02-26 15:52:07.396 3247-3247/iwangzhe.paizhaocaiqie I/checkSelfPermission: com.android.launcher.permission.INSTALL_SHORTCUT:   已授权
02-26 15:52:07.396 3247-3247/iwangzhe.paizhaocaiqie I/checkSelfPermission: android.permission.MODIFY_AUDIO_SETTINGS:   已授权
02-26 15:52:07.396 3247-3247/iwangzhe.paizhaocaiqie I/checkSelfPermission: --------------------------------5
小米:
02-26 15:58:09.128 7306-7306/iwangzhe.paizhaocaiqie I/checkSelfPermission: --------------------------------5
02-26 15:58:09.131 7306-7306/iwangzhe.paizhaocaiqie I/checkSelfPermission: --文件存储:   未授权
02-26 15:58:09.131 7306-7306/iwangzhe.paizhaocaiqie I/checkSelfPermission: --相机/拍照:   未授权
02-26 15:58:09.132 7306-7306/iwangzhe.paizhaocaiqie I/checkSelfPermission: --短信:   未授权
02-26 15:58:09.132 7306-7306/iwangzhe.paizhaocaiqie I/checkSelfPermission: com.android.launcher.permission.INSTALL_SHORTCUT:   已授权
02-26 15:58:09.132 7306-7306/iwangzhe.paizhaocaiqie I/checkSelfPermission: android.permission.MODIFY_AUDIO_SETTINGS:   已授权
02-26 15:58:09.132 7306-7306/iwangzhe.paizhaocaiqie I/checkSelfPermission: --------------------------------5

通过上面两图可以看出,两种手机输出的结果都是一样的,也都是正确的。
只是小米的手机显示的状态与输出的结果有些歧义,对于标准权限(音频调节、快捷方式),明明标识着拒绝的图标(红×),输出的结果却是已授权(已授权是正确的结果)。这可能是小米手机本身的小问题吧,这也可能也是迷惑很多开发者的主要原因。

  1. 测试方法2:是否应该向用户显示请求权限的原因说明(true/false)
    ActivityCompat.shouldShowRequestPermissionRationale(activity, permission);
String[] permissions =new String[]{
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.CAMERA,
                Manifest.permission.READ_SMS,
                Manifest.permission.INSTALL_SHORTCUT,
                Manifest.permission.MODIFY_AUDIO_SETTINGS};
//测试
Log.i("checkSelfPermission", "--------------------------------"+permissions.length);
        for (String permission : permissions) {
            //拒绝且不再提醒、系统默认禁用的权限、用户未点过拒绝的权限均返回false, 只有用户点击拒绝后,且没有勾选不再提醒返回true
            if(!ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)){
                Log.i("checkSelfPermission @@", PermissionUtils.getInstance().getPermissionName(permission)+ ":   false");
            }else{
                Log.i("checkSelfPermission @@", PermissionUtils.getInstance().getPermissionName(permission)+ ":   true");
            }
        }
        Log.i("checkSelfPermission", "--------------------------------"+permissions.length);

运行两个手机,直接运行,默认输出的结果均一致

小米、三星:
02-26 16:02:59.382 13325-13325/iwangzhe.paizhaocaiqie I/checkSelfPermission: --------------------------------5
02-26 16:02:59.383 13325-13325/iwangzhe.paizhaocaiqie I/checkSelfPermission @@: --文件存储:   false
02-26 16:02:59.384 13325-13325/iwangzhe.paizhaocaiqie I/checkSelfPermission @@: --相机/拍照:   false
02-26 16:02:59.384 13325-13325/iwangzhe.paizhaocaiqie I/checkSelfPermission @@: --短信:   false
02-26 16:02:59.384 13325-13325/iwangzhe.paizhaocaiqie I/checkSelfPermission @@: com.android.launcher.permission.INSTALL_SHORTCUT:   false
02-26 16:02:59.385 13325-13325/iwangzhe.paizhaocaiqie I/checkSelfPermission @@: android.permission.MODIFY_AUDIO_SETTINGS:   false
02-26 16:02:59.385 13325-13325/iwangzhe.paizhaocaiqie I/checkSelfPermission: --------------------------------5

允许SD卡存储、拒绝拍照(未勾选不再提醒)后的输出结果,如下:

小米、三星:
02-26 16:07:09.383 16248-16248/iwangzhe.paizhaocaiqie I/checkSelfPermission: --------------------------------5
02-26 16:07:09.385 16248-16248/iwangzhe.paizhaocaiqie I/checkSelfPermission @@: --文件存储:   false
02-26 16:07:09.386 16248-16248/iwangzhe.paizhaocaiqie I/checkSelfPermission @@: --相机/拍照:   true
02-26 16:07:09.387 16248-16248/iwangzhe.paizhaocaiqie I/checkSelfPermission @@: --短信:   false
02-26 16:07:09.388 16248-16248/iwangzhe.paizhaocaiqie I/checkSelfPermission @@: com.android.launcher.permission.INSTALL_SHORTCUT:   false
02-26 16:07:09.390 16248-16248/iwangzhe.paizhaocaiqie I/checkSelfPermission @@: android.permission.MODIFY_AUDIO_SETTINGS:   false
02-26 16:07:09.390 16248-16248/iwangzhe.paizhaocaiqie I/checkSelfPermission: --------------------------------5
小米手机测试.png

经过反复测试:
当用户禁用(拒绝且不再提醒)、系统默认禁用的权限、用户未点过拒绝的权限、用户授权的权限均返回false;
只有用户点击拒绝后,且没有勾选不再提醒的权限返回true;

7.测试方法3:判断程序的某操作是被允许的还是拒绝的还是忽略的
AppOpsManagerCompat.noteProxyOp(activity, op, activity.getPackageName());

String[] permissions =new String[]{
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.CAMERA,
                Manifest.permission.READ_SMS,
                Manifest.permission.INSTALL_SHORTCUT,
                Manifest.permission.MODIFY_AUDIO_SETTINGS};
//测试
Log.i("checkSelfPermission", "***********************"+permissions.length);
        for (String permission : permissions) {
            String op = AppOpsManagerCompat.permissionToOp(permission);
            //判断非空
            if (TextUtils.isEmpty(op)) continue;
            int result = AppOpsManagerCompat.noteProxyOp(activity, op, activity.getPackageName());
            if (result == AppOpsManagerCompat.MODE_IGNORED) {
                Log.i("checkSelfPermission", "**" + permission + "  false");
            } else if(result == AppOpsManagerCompat.MODE_ALLOWED) {
                Log.i("checkSelfPermission", "**" + permission + "  true");
            }else if(result == AppOpsManagerCompat.MODE_DEFAULT) {
                Log.i("checkSelfPermission", "**" + permission + "  default");
            }
        }
        Log.i("checkSelfPermission", "***********************"+permissions.length);
02-26 16:18:03.016 6714-6714/iwangzhe.paizhaocaiqie I/checkSelfPermission: ***********************5
02-26 16:18:03.017 6714-6714/iwangzhe.paizhaocaiqie I/checkSelfPermission: **android.permission.READ_EXTERNAL_STORAGE  true
02-26 16:18:05.546 6714-6714/iwangzhe.paizhaocaiqie I/checkSelfPermission: **android.permission.CAMERA  false
02-26 16:18:05.546 6714-6714/iwangzhe.paizhaocaiqie I/checkSelfPermission: **android.permission.READ_SMS  false
02-26 16:18:05.547 6714-6714/iwangzhe.paizhaocaiqie I/checkSelfPermission: ***********************5

4.png

由上面两图看到,我们请求了5个权限,但是只输出的3项权限,均为危险权限,而标准权限没有任何输出,因为转op时为null了,可能是标准权限不在转换清单中,所以....。
经过测试,获得的危险权限授权状态的结果与checkSelfPermission方法返回的结果一致。

  1. 关于低版本适配
    动态权限是在6.0以上才提供的,所以我们不需要想的太复杂,只需要在23以上判断请求动态权限,23以下按照以往的方式处理即可。

设计思路:

  1. 动态权限为独立的体系,可以设计成单独的模块;
  2. 在Activity中动态权限的处理尽量简洁,应封装权限的检查与请求、权限结果回调等操作;
  3. 应考虑权限请求、处理的可扩展性
  4. 权限的检测和请求可统一封装为 “权限请求”,权限结果回调可统一封装为“权限结果处理”


    权限请求.png

    通过上图,可以看到,权限判断被封装进了权限请求中,所以我们在请求/判断权限时,统一调用接口IRequestPermissions即可;

权限结果处理.png

通过上图,可以看到,权限结果处理方案有多种,我们此处采取的是策略模式,以扩展更多的方案;
RequestPermissionsResult、RequestPermissionsResultSetApp分别表示不同的处理方案。
那么,我们再处理权限结果回调时,仅需调用接口IRequestPermissionsResult即可。


先来看下代码中是如何调用的:
第一步:实例化 “权限请求”,“权限结果处理”

IRequestPermissions requestPermissions = RequestPermissions.getInstance();//动态权限请求
IRequestPermissionsResult requestPermissionsResult = RequestPermissionsResultSetApp.getInstance();//动态权限请求结果处理

第二步:在需要的地方,请求权限

//请求权限
    private boolean requestPermissions(){
        //需要请求的权限
        String[] permissions = {Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.CAMERA};
        //开始请求权限
        return requestPermissions.requestPermissions(
                this,
                permissions,
                PermissionUtils.ResultCode1);
    }

第三步:onRequestPermissionsResult回调中,统一处理结果

//用户授权操作结果(可能授权了,也可能未授权)
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        //用户给APP授权的结果
        //判断grantResults是否已全部授权,如果是,执行相应操作,如果否,提醒开启权限
        if(requestPermissionsResult.doRequestPermissionsResult(this, permissions, grantResults)){
            //请求的权限全部授权成功,此处可以做自己想做的事了
            //输出授权结果
            Toast.makeText(MainActivity.this,"授权成功,请重新点击刚才的操作!",Toast.LENGTH_LONG).show();
        }else{
            //输出授权结果
            Toast.makeText(MainActivity.this,"请给APP授权,否则功能无法正常使用!",Toast.LENGTH_LONG).show();
        }
    }

通过这3步,即可快速实现动态权限的所有操作。
而如何检测、判断、请求的权限,如何引导用户设置权限等相关事项,开发人员不需要再关注。


具体的实现:

  1. 权限请求


    3.png

    权限请求接口

public interface IRequestPermissions {
    /**
     * 请求权限
     * @param activity 上下文
     * @param permissions 权限集合
     * @param resultCode 请求码
     * @return 如果权限已全部允许,返回true; 反之,请求权限,在
     */
    boolean requestPermissions(Activity activity, String[] permissions, int resultCode);
}

权限请求实现类

public class RequestPermissions implements IRequestPermissions {
    private static RequestPermissions requestPermissions;
    public static RequestPermissions getInstance(){
        if(requestPermissions == null){
            requestPermissions = new RequestPermissions();
        }
        return requestPermissions;
    }

    @Override
    public boolean requestPermissions(Activity activity, String[] permissions, int resultCode) {
        //判断手机版本是否23以下,如果是,不需要使用动态权限
        if(Build.VERSION.SDK_INT < 23){
            return true;
        }

        //判断并请求权限
        return requestNeedPermission(activity,permissions,resultCode);
    }

    private boolean requestAllPermission(Activity activity, String[] permissions, int resultCode){
        //判断是否已赋予了全部权限
        boolean isAllGranted = CheckPermission.checkPermissionAllGranted(activity, permissions);
        if(isAllGranted){
            return true;
        }
        ActivityCompat.requestPermissions(activity, permissions, resultCode);
        return false;
    }

    private boolean requestNeedPermission(Activity activity, String[] permissions, int resultCode){
        List<String> list = CheckPermission.checkPermissionDenied(activity, permissions);
        if(list.size() == 0){
            return true;
        }

        //请求权限
        String[] deniedPermissions = list.toArray(new String[list.size()]);
        ActivityCompat.requestPermissions(activity, deniedPermissions, resultCode);
        return false;
    }
}

权限检测类

public class CheckPermission {
    /**
     * 检查是否拥有指定的所有权限
     * @param context 上下文
     * @param permissions 权限数组
     * @return 只要有一个权限没有被授予, 则直接返回 false,否则,返回true!
     */
    public static boolean checkPermissionAllGranted(Context context, String[] permissions) {
        for (String permission : permissions) {
            if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
                return false;
            }
        }
        return true;
    }

    /**
     * 检查未允许的权限集合
     * @param context 上下文
     * @param permissions 权限集合
     * @return 未允许的权限集合
     */
    public static List<String> checkPermissionDenied(Context context, String[] permissions){
        List<String> lstPermissions = new ArrayList<>();
        for (String permission : permissions) {
            if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
                lstPermissions.add(permission);
            }
        }
        return lstPermissions;
    }
}
  1. 权限请求结果处理


    4.png

接口

public interface IRequestPermissionsResult {
    /**
     * 处理权限请求结果
     * @param activity
     * @param permissions 请求的权限数组
     * @param grantResults 权限请求结果数组
     * @return 处理权限结果如果全部通过,返回true;否则,引导用户去授权页面
     */
    boolean doRequestPermissionsResult(Activity activity, @NonNull String[] permissions, @NonNull int[] grantResults);
}

权限请求结果实现类(方案一:如果授权失败,不做任何处理)

public class RequestPermissionsResult implements IRequestPermissionsResult {
    private static RequestPermissionsResult requestPermissionsResult;
    public static RequestPermissionsResult getInstance(){
        if(requestPermissionsResult == null){
            requestPermissionsResult = new RequestPermissionsResult();
        }
        return requestPermissionsResult;
    }
    @Override
    public boolean doRequestPermissionsResult(Activity activity, @NonNull String[] permissions, @NonNull int[] grantResults) {
        boolean isAllGranted = true;
        // 判断是否所有的权限都已经授予了
        for (int grant : grantResults) {
            if (grant != PackageManager.PERMISSION_GRANTED) {
                isAllGranted = false;
                break;
            }
        }
        //已全部授权
        if (isAllGranted) {
            return true;
        }
        else {
            //什么也不做
        }
        return false;
    }
}

权限请求结果实现类(方案二:如果授权失败,引导用户进行应用授权)

public class RequestPermissionsResultSetApp implements IRequestPermissionsResult{
    private static RequestPermissionsResultSetApp requestPermissionsResult;
    public static RequestPermissionsResultSetApp getInstance(){
        if(requestPermissionsResult == null){
            requestPermissionsResult = new RequestPermissionsResultSetApp();
        }
        return requestPermissionsResult;
    }

    @Override
    public boolean doRequestPermissionsResult(Activity activity, @NonNull String[] permissions, @NonNull int[] grantResults) {
        List<String> deniedPermission = new ArrayList<>();
        for (int i=0; i<grantResults.length;i++){
            if(grantResults[i] == PackageManager.PERMISSION_DENIED){
                deniedPermission.add(permissions[i]);
            }
        }

        //已全部授权
        if (deniedPermission.size() == 0) {
            return true;
        }
        //引导用户去授权
        else {
            String name = PermissionUtils.getInstance().getPermissionNames(deniedPermission);
            SetPermissions.openAppDetails(activity,name);
        }
        return false;
    }
}

引导用户去授权

public class SetPermissions {
    /**
     * 打开APP详情页面,引导用户去设置权限
     * @param activity 页面对象
     * @param permissionNames 权限名称(如是多个,使用\n分割)
     */
    public static void openAppDetails(final Activity activity, String permissionNames) {
        StringBuilder sb = new StringBuilder();
        sb.append(PermissionUtils.PermissionTip1);
        sb.append(permissionNames);
        sb.append(PermissionUtils.PermissionTip2);
        AlertDialog.Builder builder = new AlertDialog.Builder(activity);
        builder.setMessage(sb.toString());
        builder.setPositiveButton(PermissionUtils.PermissionDialogPositiveButton, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Intent intent = new Intent();
                intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                intent.addCategory(Intent.CATEGORY_DEFAULT);
                intent.setData(Uri.parse("package:" + activity.getPackageName()));
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
                intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
                activity.startActivity(intent);
            }
        });
        builder.setNegativeButton(PermissionUtils.PermissionDialogNegativeButton, null);
        builder.show();
    }
}
  1. 公共内容提取(常量、方法等)
public class PermissionUtils {
    public static int ResultCode1 = 100;//权限请求码
    public static int ResultCode2 = 200;//权限请求码
    public static int ResultCode3 = 300;//权限请求码
    public static String PermissionTip1 = "亲爱的用户 \n\n软件部分功能需要请求您的手机权限,请允许以下权限:\n\n";//权限提醒
    public static String PermissionTip2 = "\n请到 “应用信息 -> 权限” 中授予!";//权限提醒
    public static String PermissionDialogPositiveButton = "去手动授权";
    public static String PermissionDialogNegativeButton = "取消";

    private static PermissionUtils permissionUtils;
    public static PermissionUtils getInstance(){
        if(permissionUtils == null){
            permissionUtils = new PermissionUtils();
        }
        return permissionUtils;
    }

    private HashMap<String,String> permissions;
    public HashMap<String,String> getPermissions(){
        if(permissions == null){
            permissions = new HashMap<>();
            initPermissions();
        }
        return permissions;
    }

    private void initPermissions(){
        //联系人/通讯录权限
        permissions.put("android.permission.WRITE_CONTACTS","--通讯录/联系人");
        permissions.put("android.permission.GET_ACCOUNTS","--通讯录/联系人");
        permissions.put("android.permission.READ_CONTACTS","--通讯录/联系人");
        //电话权限
        permissions.put("android.permission.READ_CALL_LOG","--电话");
        permissions.put("android.permission.READ_PHONE_STATE","--电话");
        permissions.put("android.permission.CALL_PHONE","--电话");
        permissions.put("android.permission.WRITE_CALL_LOG","--电话");
        permissions.put("android.permission.USE_SIP","--电话");
        permissions.put("android.permission.PROCESS_OUTGOING_CALLS","--电话");
        permissions.put("com.android.voicemail.permission.ADD_VOICEMAIL","--电话");
        //日历权限
        permissions.put("android.permission.READ_CALENDAR","--日历");
        permissions.put("android.permission.WRITE_CALENDAR","--日历");
        //相机拍照权限
        permissions.put("android.permission.CAMERA","--相机/拍照");
        //传感器权限
        permissions.put("android.permission.BODY_SENSORS","--传感器");
        //定位权限
        permissions.put("android.permission.ACCESS_FINE_LOCATION","--定位");
        permissions.put("android.permission.ACCESS_COARSE_LOCATION","--定位");
        //文件存取
        permissions.put("android.permission.READ_EXTERNAL_STORAGE","--文件存储");
        permissions.put("android.permission.WRITE_EXTERNAL_STORAGE","--文件存储");
        //音视频、录音权限
        permissions.put("android.permission.RECORD_AUDIO","--音视频/录音");
        //短信权限
        permissions.put("android.permission.READ_SMS","--短信");
        permissions.put("android.permission.RECEIVE_WAP_PUSH","--短信");
        permissions.put("android.permission.RECEIVE_MMS","--短信");
        permissions.put("android.permission.RECEIVE_SMS","--短信");
        permissions.put("android.permission.SEND_SMS","--短信");
        permissions.put("android.permission.READ_CELL_BROADCASTS","--短信");
    }

    /**
     * 获得权限名称集合(去重)
     * @param permission 权限数组
     * @return 权限名称
     */
    public String getPermissionNames(List<String> permission){
        if(permission==null || permission.size()==0){
            return "\n";
        }
        StringBuilder sb = new StringBuilder();
        List<String> list = new ArrayList<>();
        HashMap<String,String> permissions = getPermissions();
        for(int i=0; i<permission.size(); i++){
            String name = permissions.get(permission.get(i));
            if(name!=null && !list.contains(name)){
                list.add(name);
                sb.append(name);
                sb.append("\n");
            }
        }
        return sb.toString();
    }
}

效果图:

请求权限.jpg
请求权限--带不再提醒6.jpg
引导用户去授权--包含需要的权限名称.jpg

Demo下载地址:https://pan.baidu.com/s/1dnaugm


注意:
以上内容只是为了大家能清晰的理解动态权限的使用,Demo可以作为代码参考,但是不应拿到项目中直接使用,因为不同的项目中有不同的要求和限制。使用该Demo中的代码时,请根据自己项目的要求,进一步优化、调整Demo的代码(如什么场合使用checkSelfPermission、shouldShowRequestPermissionRationale还是noteProxyOp等)

本案例如有问题,请及时反馈给我们,感谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343