终于等到你--权限工具类

作者: Blankj | 来源:发表于2018-01-10 11:19 被阅读2362次

    Foreword

    之前总是有小伙伴问 AndroidUtilCode 中有没有权限工具类,但都被我怼回去了,让先用着其他第三方的,不过,到了如今的 1.11.0 版本的 AndroidUtilCode,这个一直拖欠着的权限工具类总算要问世了,以后小伙伴们如果用 AndroidUtilCode 需要动态授权的话,就不用再多依赖一个第三方库了,下面来介绍下其功能。

    Functions

    • 兼容安卓各版本,包括 Android 8.0
    • 支持任意地方申请权限,不仅限于 Activity 和 Fragment 等
    • 支持多权限同时申请
    • 采用链式调用,一句话解决权限申请

    Achieve

    首先来介绍其实现方式,关于运行时权限的介绍可以在官网查看 -> 传送门。关于危险权限列表,我封装危险权限常量类 PermissionConstants.java,代码如下所示:

    import android.Manifest;
    import android.Manifest.permission;
    import android.annotation.SuppressLint;
    import android.support.annotation.StringDef;
    
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    
    
    /**
     * <pre>
     *     author: Blankj
     *     blog  : http://blankj.com
     *     time  : 2017/12/29
     *     desc  : 权限相关常量
     * </pre>
     */
    @SuppressLint("InlinedApi")
    public final class PermissionConstants {
    
        public static final String CALENDAR   = Manifest.permission_group.CALENDAR;
        public static final String CAMERA     = Manifest.permission_group.CAMERA;
        public static final String CONTACTS   = Manifest.permission_group.CONTACTS;
        public static final String LOCATION   = Manifest.permission_group.LOCATION;
        public static final String MICROPHONE = Manifest.permission_group.MICROPHONE;
        public static final String PHONE      = Manifest.permission_group.PHONE;
        public static final String SENSORS    = Manifest.permission_group.SENSORS;
        public static final String SMS        = Manifest.permission_group.SMS;
        public static final String STORAGE    = Manifest.permission_group.STORAGE;
    
        private static final String[] GROUP_CALENDAR   = {
                permission.READ_CALENDAR, permission.WRITE_CALENDAR
        };
        private static final String[] GROUP_CAMERA     = {
                permission.CAMERA
        };
        private static final String[] GROUP_CONTACTS   = {
                permission.READ_CONTACTS, permission.WRITE_CONTACTS, permission.GET_ACCOUNTS
        };
        private static final String[] GROUP_LOCATION   = {
                permission.ACCESS_FINE_LOCATION, permission.ACCESS_COARSE_LOCATION
        };
        private static final String[] GROUP_MICROPHONE = {
                permission.RECORD_AUDIO
        };
        private static final String[] GROUP_PHONE      = {
                permission.READ_PHONE_STATE, permission.READ_PHONE_NUMBERS, permission.CALL_PHONE,
                permission.ANSWER_PHONE_CALLS, permission.READ_CALL_LOG, permission.WRITE_CALL_LOG,
                permission.ADD_VOICEMAIL, permission.USE_SIP, permission.PROCESS_OUTGOING_CALLS
        };
        private static final String[] GROUP_SENSORS    = {
                permission.BODY_SENSORS
        };
        private static final String[] GROUP_SMS        = {
                permission.SEND_SMS, permission.RECEIVE_SMS, permission.READ_SMS,
                permission.RECEIVE_WAP_PUSH, permission.RECEIVE_MMS,
        };
        private static final String[] GROUP_STORAGE    = {
                permission.READ_EXTERNAL_STORAGE, permission.WRITE_EXTERNAL_STORAGE
        };
    
        @StringDef({CALENDAR, CAMERA, CONTACTS, LOCATION, MICROPHONE, PHONE, SENSORS, SMS, STORAGE,})
        @Retention(RetentionPolicy.SOURCE)
        public @interface Permission {
        }
    
        public static String[] getPermissions(@Permission final String permission) {
            switch (permission) {
                case CALENDAR:
                    return GROUP_CALENDAR;
                case CAMERA:
                    return GROUP_CAMERA;
                case CONTACTS:
                    return GROUP_CONTACTS;
                case LOCATION:
                    return GROUP_LOCATION;
                case MICROPHONE:
                    return GROUP_MICROPHONE;
                case PHONE:
                    return GROUP_PHONE;
                case SENSORS:
                    return GROUP_SENSORS;
                case SMS:
                    return GROUP_SMS;
                case STORAGE:
                    return GROUP_STORAGE;
            }
            return new String[]{permission};
        }
    }
    

    为了适配 Android 8.0,我在申请权限的时候,会把清单文件中使用到的同组的权限都一次性申请完毕,相关代码如下所示:

    private static final List<String> PERMISSIONS = getPermissions();
    
    /**
     * 获取应用权限
     *
     * @return 清单文件中的权限列表
     */
    public static List<String> getPermissions() {
        return getPermissions(Utils.getApp().getPackageName());
    }
    
    /**
     * 获取应用权限
     *
     * @param packageName 包名
     * @return 清单文件中的权限列表
     */
    public static List<String> getPermissions(final String packageName) {
        PackageManager pm = Utils.getApp().getPackageManager();
        try {
            return Arrays.asList(
                    pm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS)
                            .requestedPermissions
            );
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
            return Collections.emptyList();
        }
    }
    
    /**
     * 设置请求权限
     *
     * @param permissions 要请求的权限
     * @return {@link PermissionUtils}
     */
    public static PermissionUtils permission(@Permission final String... permissions) {
        return new PermissionUtils(permissions);
    }
    
    private PermissionUtils(final String... permissions) {
        mPermissions = new LinkedHashSet<>();
        for (String permission : permissions) {
            for (String aPermission : PermissionConstants.getPermissions(permission)) {
                if (PERMISSIONS.contains(aPermission)) {
                    mPermissions.add(aPermission);
                }
            }
        }
        sInstance = this;
    }
    

    为了支持任意地方都可以申请权限,我在 PermissionUtils.java 中封装了 PermissionActivity,源码如下所示:

    @RequiresApi(api = Build.VERSION_CODES.M)
    public static class PermissionActivity extends Activity {
        public static void start(final Context context) {
            Intent starter = new Intent(context, PermissionActivity.class);
            starter.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(starter);
        }
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            if (sInstance.mThemeCallback != null) {
                sInstance.mThemeCallback.onActivityCreate(this);
            } else {
                Window window = getWindow();
                window.setBackgroundDrawable(null);
                int option = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
                window.getDecorView().setSystemUiVisibility(option);
                window.setStatusBarColor(Color.TRANSPARENT);
            }
            super.onCreate(savedInstanceState);
            if (sInstance.rationale(this)) {
                finish();
                return;
            }
            if (sInstance.mPermissionsRequest != null) {
                int size = sInstance.mPermissionsRequest.size();
                requestPermissions(sInstance.mPermissionsRequest.toArray(new String[size]), 1);
            }
        }
        @Override
        public void onRequestPermissionsResult(int requestCode,
                                               @NonNull String[] permissions,
                                               @NonNull int[] grantResults) {
            sInstance.onRequestPermissionsResult(this);
            finish();
        }
    }
    

    这样我们便可以自己全权处理权限请求,但启动的这个 PermissionActivity 的主题并不一定符合小伙伴们应用的 Activity 相关主题,所以我留了个设置主题的回调接口,比如可以把这个 Activity 设置为全屏等操作,这样便可无感知地启动一个 Activity,相关主题属性如下:

    <style name="ActivityTranslucent">
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:colorBackgroundCacheHint">@null</item>
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:activityOpenEnterAnimation">@null</item>
        <item name="android:activityOpenExitAnimation">@null</item>
        <item name="android:activityCloseEnterAnimation">@null</item>
        <item name="android:activityCloseExitAnimation">@null</item>
        <item name="android:statusBarColor">@android:color/transparent</item>
    </style>
    

    这个应该能适配很多应用了。

    当然,如果有设置 rationale 的话,也就是设置拒绝权限后再次请求的回调接口,此时便会走 sInstance.rationale(this),具体代码如下所示:

    @RequiresApi(api = Build.VERSION_CODES.M)
    private boolean rationale(final Activity activity) {
        boolean isRationale = false;
        if (mOnRationaleListener != null) {
            for (String permission : mPermissionsRequest) {
                if (activity.shouldShowRequestPermissionRationale(permission)) {
                    getPermissionsStatus(activity);
                    mOnRationaleListener.rationale(new ShouldRequest() {
                        @Override
                        public void again(boolean again) {
                            if (again) {
                                startPermissionActivity();
                            } else {
                                requestCallback();
                            }
                        }
                    });
                    isRationale = true;
                    break;
                }
            }
            mOnRationaleListener = null;
        }
        return isRationale;
    }
    

    逻辑就是如果 rationale 回调接口 执行了 shouldRequest.again(true);,那么就会继续申请下去,反之则不再申请,多用在弹出一个提示对话框来让用户选择是否继续请求权限。

    最终就是发起请求和接受请求,并把最终状态保存到 mPermissionsGrantedmPermissionsDeniedmPermissionsDeniedForever 中,最终回调 callback 的接口,相关代码如下所示:

    private void getPermissionsStatus(final Activity activity) {
        for (String permission : mPermissionsRequest) {
            if (isGranted(permission)) {
                mPermissionsGranted.add(permission);
            } else {
                mPermissionsDenied.add(permission);
                if (!activity.shouldShowRequestPermissionRationale(permission)) {
                    mPermissionsDeniedForever.add(permission);
                }
            }
        }
    }
    
    private void requestCallback() {
        if (mSimpleCallback != null) {
            if (mPermissionsRequest.size() == 0
                    || mPermissions.size() == mPermissionsGranted.size()) {
                mSimpleCallback.onGranted();
            } else {
                if (!mPermissionsDenied.isEmpty()) {
                    mSimpleCallback.onDenied();
                }
            }
            mSimpleCallback = null;
        }
        if (mFullCallback != null) {
            if (mPermissionsRequest.size() == 0
                    || mPermissions.size() == mPermissionsGranted.size()) {
                mFullCallback.onGranted(mPermissionsGranted);
            } else {
                if (!mPermissionsDenied.isEmpty()) {
                    mFullCallback.onDenied(mPermissionsDeniedForever, mPermissionsDenied);
                }
            }
            mFullCallback = null;
        }
        mOnRationaleListener = null;
        mThemeCallback = null;
    }
    
    private void onRequestPermissionsResult(final Activity activity) {
        getPermissionsStatus(activity);
        requestCallback();
    }
    

    Use

    说了那么多,总算到使用了,其实使用起来非常方便,一句话即可,比如我们要申请 android.permission.READ_CALENDAR 权限,那么我们可以去 PermissionConstants.java 中找到其所属组,也就是 CALENDAR,而应用是全屏类型的应用,那么我们可以像下面这样发起请求。

    PermissionUtils.permission(PermissionConstants.CALENDAR)
            .rationale(new PermissionUtils.OnRationaleListener() {
                @Override
                public void rationale(final ShouldRequest shouldRequest) {
                    PermissionHelper.showRationaleDialog(shouldRequest);
                }
            })
            .callback(new PermissionUtils.FullCallback() {
                @Override
                public void onGranted(List<String> permissionsGranted) {
                    updateAboutPermission();
                }
                @Override
                public void onDenied(List<String> permissionsDeniedForever,
                                     List<String> permissionsDenied) {
                    if (!permissionsDeniedForever.isEmpty()) {
                        PermissionHelper.showOpenAppSettingDialog();
                    }
                    LogUtils.d(permissionsDeniedForever, permissionsDenied);
                }
            })
            .theme(new PermissionUtils.ThemeCallback() {
                @Override
                public void onActivityCreate(Activity activity) {
                    ScreenUtils.setFullScreen(activity);// 设置全屏
                }
            })
            .request();
    

    如果还有不会的可以参考 AndroidUtilCode 中的 demo -> PermissionActivity.java

    Tips:推荐小伙伴们最好把权限请求相关的操作都放在一个 helper 类中,就像我 AndroidUtilCode 中 demo 的做法,创建一个 PermissionHelper.java,毕竟有很多权限请求都是重复。

    Conclusion

    好了,本次的权限工具类介绍就到此结束了,在这么简洁的工具类背后都是本柯基辛勤付出的汗水,疯狂地 debug,疯狂地测试来消除内存泄漏的问题,虽然路途很艰辛,但最终还是成功地完成了该工具类,终于等到你。

    相关文章

      网友评论

      • Android轮子哥:写得很不错,推荐一个单独的框架,也是一句话:https://www.jianshu.com/p/c69ff8a445ed
      • 96ee612940ce:APPutils静默安装那个有的设备不行(我用tcl电视 和智能魔镜试的),我用了另一个(网上有人专门写的静默安装的工具类)PackageUtils 就可以静默安装,所以我建议你能不能也集成一下。
        96ee612940ce:算了,我还是发你qq把。
        96ee612940ce:@Blankj public static int installSilent(Context context, String filePath, String pmParams) {
        if (filePath == null || filePath.length() == 0) {
        return INSTALL_FAILED_INVALID_URI;
        }

        File file = new File(filePath);
        if (file == null || file.length() <= 0 || !file.exists() || !file.isFile()) {
        return INSTALL_FAILED_INVALID_URI;
        }

        /**
        * if context is system app, don't need root permission, but should add <uses-permission
        * android:name="android.permission.INSTALL_PACKAGES" /> in mainfest
        **/
        StringBuilder command = new StringBuilder().append("LD_LIBRARY_PATH=/vendor/lib*:/system/lib* pm install ")
        .append(pmParams == null ? "" : pmParams).append(" ").append(filePath.replace(" ", "\\ "));
        PackShellUtils.CommandResult commandResult = PackShellUtils.execCommand(command.toString(), !isSystemApplication(context), true);
        Blankj:@耿直boy_273b 代码发出来我看下
      • 吴国友:ClipboardUtils不在了呢
        Blankj:@吴国友 去github看
        吴国友:@Blankj 那个库里面好像没有subutils
        Blankj:@吴国友 subutils里
      • 大象屁股:如果 能加上注释,那就更好了
      • buhanzhe:666 之前用的就是第三方的 待会试试
      • e156012376cf:已关注 萌新向大佬学习
      • 我是无穷:oppo,vivo,小米怎么处理的?
        Blankj:@我是无穷 那就说明有权限了哈
        我是无穷:@Blankj 特殊手机返回都是成功
        Blankj:@我是无穷 有权限自然会走onGranted 没权限会走 onDenied
      • Stay_Li:👍👍
      • 码农朱同学:有些手机比较奇葩,没允许权限照样能进
        Blankj:@etrnel 但手机又不是只有 vivo,所以还是需要的哈
        etrnel:比如vivo的。会自己提示权限,都不用动态申请:joy:
        Blankj:@zhuxh 说明rom被修改过
      • xiaobailong24:赞👍!
      • 我才不是死宅:牛逼,先顶一下

      本文标题:终于等到你--权限工具类

      本文链接:https://www.haomeiwen.com/subject/grtonxtx.html