近段时间浏览新闻经常会看到工信部通报某某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() {@Overridepublic void onGranted() {UIToast.showShort("onGranted");}@Overridepublic void onDenied() {UIToast.showShort("onDenied");}}).request();

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

PermissionUtils.permission(PermissionConstants.STORAGE).callback(new PermissionUtils.SingleCallback() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic void onGranted() {UIToast.showShort("onGranted");}@Overridepublic 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() {@Overridepublic void onGranted(@NonNull List<String> permissionsGranted) {callback.onGranted();}@Overridepublic 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() {@Overridepublic void onGranted(@NonNull List<String> permissionsGranted) {callback.onGranted();}@Overridepublic 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防劫持方案

Android 申请权限前简单封装弹框阐述申请理由工具类,应付app合规检查相关推荐

  1. Android移动开发-Android开发日历时常用的农历和公历换算代码工具类

    下面是与Android开发日历时常用的有关农历计算.公历计算.二十四气节相关的代码工具类的代码. Constant.java逻辑代码如下: package com.fukaimei.calendar. ...

  2. uniapp封装弹框和一键复制

    封装文件utils/common // 不带图标的轻提示 export function showTip(title, duration = 1500) {uni.showToast({title,i ...

  3. android魅族权限弹窗,魅族高管:Flyme 9将成为用户对抗APP强制获取权限的“最强后盾”...

    新酷产品第一时间免费试玩,还有众多优质达人分享独到生活经验,快来新浪众测,体验各领域最前沿.最有趣.最好玩的产品吧~!下载客户端还能获得专享福利哦! 新品拿到手软,好物天天推荐,新浪众测等你加入.在这 ...

  4. Android 常用的地球经纬度转换公里(km)计算工具类

    地球赤道上环绕地球一周走一圈共40075.04公里,而@一圈分成360°,而每1°(度)有60,每一度一秒在赤道上的长度计算如下: 40075.04km/360°=111.31955km 111.31 ...

  5. 简单rides和Memory切换缓存 Rides工具类主要方法

    内容简介,父类以及子类结构,重点在于通过@Service实现类,判断类型来new出不同的缓存实现类 首先要写一个缓存接口CacheService,来统一规定缓存的必要操作. public interf ...

  6. 【Android 应用开发】Google 官方 EasyPermissions 权限申请库 ( 最简单用法 | 一行代码搞定权限申请 | 推荐用法 )

    文章目录 一.添加依赖 二.在 AndroidManifest.xml 中配置权限 三.权限申请最简单用法 四.推荐使用的用法 五.GitHub 地址 上一篇博客 [Android 应用开发]Goog ...

  7. 一个Android动态权限的流式权限管理库EasyPermission,帮你申请动态权限

    转载请注明出处https://blog.csdn.net/yeluofengchui/article/details/91126117 已经出2.0版本了,使用更方便,详情见EasyPermissio ...

  8. Android PermissionUtils:运行时权限工具类及申请权限的正确姿势

    Android PermissionUtils:运行时权限工具类及申请权限的正确姿势 ifadai 关注 2017.06.16 16:22* 字数 318 阅读 3637评论 1喜欢 6 Permis ...

  9. Android动态权限详解

    1 什么是动态权限 去年底,上级主管部门为加强国内Android应用隐私管理,出台了一系列规定,我们的App也做了相应的修改.主要一条修改为,隐私提示与权限获取顺序.修改测试过程中,发觉部分同学对An ...

最新文章

  1. python能写软件吗-用什么软件写python
  2. 洛谷——P2256 一中校运会之百米跑
  3. 若依微服务版怎样新增业务子模块并使用代码生成实现对表的增删改查
  4. 数百个 HTML5 例子学习 HT 图形组件 – 拓扑图篇
  5. kubernetes英语怎么读_陷阱英语单词怎么读?
  6. ASP调用web services
  7. 如何在Debian上安装配置ownCloud
  8. jq之$(“ul li:first“)
  9. 论述计算机与外设的访问控制方法,试论述计算机与外设访问控制的方法有多少种各有什么优缺点...
  10. 36岁青椒的“我”想对26岁读博的“你”说些话
  11. SharePoint读取和设置列表栏的内容
  12. 财务数字转换--大小写转换
  13. 红芯宣布完成 2.5 亿 C 轮融资,却被网友发现其浏览器安装包解压出 Chrome?
  14. UniApp:Vue特性篇:vue2.0的广播与接收(待详细了解)
  15. word2vec初步使用
  16. 开发常用的Git命令
  17. Chrome 浏览器 原生工具进行网页长截图
  18. 在项目中CR是什么意思?
  19. 【1072】鸡尾酒疗法
  20. C语言实现二分法查找某个数字(超详细)

热门文章

  1. 计算机英语新词的认知语义阐释论文,计算机英文专业论文题目 计算机英文论文题目怎样定...
  2. XML文件的数据抽取
  3. 解决keil软件*** Error: Project ‘first‘ requires ‘C51‘ Toolchain which is not installed.
  4. python高斯求和1-500
  5. Promise.all 方法详解
  6. PM、PMP、PMO分别都是什么 以及三者的关系
  7. 1963年以来世界最伟大的一百首流行歌曲
  8. 苏菲的世界-part2
  9. USB 设备热插拔的检测
  10. 制造业ai帮助制造企业实现人工智能转型