本文将分享如何在 uniapp 开发中申请相机和相册权限时,解决应用商店要求同步告知权限申请目的的问题,并兼容 Android 13。
背景
在某一次版本迭代中,上架华为应用商店审核不通过,原因是申请相机、相册权限时「未同步告知权限申请的使用目的」。
审核方给出的修改建议如下:
修改建议:APP在申请敏感权限时,应同步说明权限申请的使用目的,包括但不限于申请权限的名称、服务的具体功能、用途;告知方式不限于弹窗、蒙层、浮窗、或者自定义操作系统权限弹框等。请排查应用内所有权限申请行为,确保均符合要求。
特别提示:需在申请权限的同时/过程中同步告知目的。
在申请权限的同时给用户同步告知目的,建议申请目的与“权限弹窗”需同屏展示。
分析
简而言之,申请权限之前,需要同步显示“提示”,告知用户权限的使用目的,不能提前告知,也不能在申请权限之后再告知。
- 因为不能每次都显示申请目的,如何判断当前是否已经拥有权限?
- 在用户未对权限弹窗进行操作时,如何确保告知目的不会消失,并且与权限弹窗同屏显示?
难点:
- 判断权限是否已授权
- 权限申请流程
- 实现持久性弹窗
解决
由于主要针对安卓应用商店,这里只考虑安卓的情况,iOS部分较为简单。
回到难点上,
如何判断当前有无权限
在 uniapp 中可以使用 plus.navigator.checkPermission(permission)
API 检查运行环境的权限,入参 permission
是权限的名称。其中,相机、相册的权限名称分别为:
android.permission.CAMERA
android.permission.READ_EXTERNAL_STORAGE
该 API 返回一个字符串,表示权限的授权状态。authorized
代表已被用户授权使用此权限。其他返回值请查阅 此处。
因此,我们只需要判断返回的权限状态是否为 authorized
,如果是,则表示已有权限,无需继续申请;否则,需要申请对应权限。
申请权限
使用 plus.android.requestPermissions(Array[String] permissions, AndroidSuccessCallback successCb, AndroidErrorCallback errorCB)
API 可以向系统请求权限。其中,permissions
是申请的权限列表,successCb
是申请权限成功后的回调函数,errorCB
是失败回调函数(通常用于处理参数错误的情况)。
- 成功回调函数
successCb
包含三个参数:
granted
- 已获取的权限列表(字符串数组);
deniedPresent
- 被临时拒绝的权限列表(字符串数组);
deniedAlways
- 被永久拒绝的权限列表(字符串数组)。
官方使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function requestLocation(){ plus.android.requestPermissions(['android.permission.ACCESS_FINE_LOCATION'], function(e){ if(e.deniedAlways.length>0){ console.log('Always Denied!!! '+e.deniedAlways.toString()); } if(e.deniedPresent.length>0){ console.log('Present Denied!!! '+e.deniedPresent.toString()); } if(e.granted.length>0){ console.log('Granted!!! '+e.granted.toString()); } }, function(e){ console.log('Request Permissions error:'+JSON.stringify(e)); }); }
|
官方示例中已经提到,如果权限被永久拒绝,需要提示用户手动打开系统设置进行授权。以下是实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| export function guideUserToAuthorize(modelContent = '请打开对应权限(点击确定后在权限中授权对应权限)') { uni.showModal({ title: '提示', content: modelContent, success: (res) => { if (res.confirm) { openAppDetailedSettingsPage(); } } }); }
export function openAppDetailedSettingsPage() { var Intent = plus.android.importClass("android.content.Intent"); var Settings = plus.android.importClass("android.provider.Settings"); var Uri = plus.android.importClass("android.net.Uri"); var mainActivity = plus.android.runtimeMainActivity(); var intent = new Intent(); intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); var uri = Uri.fromParts("package", mainActivity.getPackageName(), null); intent.setData(uri); mainActivity.startActivity(intent); }
|
对于相机和相册权限的申请,无论是永久拒绝还是临时拒绝,都需要引导用户打开设置页面进行授权。只有在权限被允许时,才能继续执行相关的业务操作。
持久性弹窗
在 uniapp 中,可以使用 uni-popup
组件来实现持久性弹窗。 该组件可以手动打开或关闭,并且可以自定义样式。
何时打开何时关闭?
如果检查权限时发现权限未被授予,则在申请权限之前打开弹窗;在用户操作后触发成功回调函数时,关闭弹窗。
1 2 3 4 5 6
| <uni-popup ref="cameraPopup" type="top"> <view class="popup-content"> <view class="title">相机权限说明</view> <view class="content">你的目的</view> </view> </uni-popup>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| .popup-content { position: relative; z-index: 2000; background-color: #eee; width: 90%; margin: 0 auto; margin-top: 10vh; padding: 32rpx; border-radius: 32rpx; color: #000; box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.3); .title { font-size: 32rpx; font-weight: bold; } .content { margin-top: 16rpx; line-height: 1.5; } }
|
涉及到业务代码,这里只给出部分关键代码。
参考代码1(未兼容Android13):
由于相机、相册一般是一起使用,这份代码同时申请了相机、相册的权限。
函数 guideUserToAuthorize
参考上面给出的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| <template> <view> <uni-popup ref="cameraPopup" type="top"> <view class="popup-content"> <view class="title">相机权限说明</view> <view class="content">相机目的(根据实际业务替换)</view> </view> </uni-popup> <uni-popup ref="galleryPopup" type="top"> <view class="popup-content"> <view class="title">相册权限说明</view> <view class="content">相册目的(实际业务替换)</view> </view> </uni-popup> </view> </template>
<script> import { guideUserToAuthorize } from '@/utils'; export default { components: {}, props: { }, data() { return { }; }, methods: { $_chooseImage() { let platform = uni.getSystemInfoSync().platform; //首先判断app是安卓还是ios if (platform == "ios") { //这里是ios的方法 // 执行业务代码 } else if (platform == "android") { if (plus.navigator.checkPermission('android.permission.CAMERA') === 'authorized' && plus.navigator.checkPermission('android.permission.READ_EXTERNAL_STORAGE') === 'authorized') { // 已经获得相机和相册权限 // 执行业务代码 } else { // 其中一个权限未取得或被拒绝 this.getAndroidCameraPermission() } } }, // 获得安卓相机权限 getAndroidCameraPermission() { this.$refs.cameraPopup.open(); plus.android.requestPermissions(['android.permission.CAMERA'], (e) => { this.$refs.cameraPopup.close(); if (e.deniedAlways.length > 0) { //权限被永久拒绝 // 弹出提示框解释为何需要权限,引导用户打开设置页面开启 guideUserToAuthorize('请打开手机相机功能(点击确定后在权限中授权相机功能)') } else if (e.deniedPresent.length > 0) { //权限被临时拒绝 // 弹出提示框解释为何需要权限,可再次调用plus.android.requestPermissions申请权限 guideUserToAuthorize('请打开手机相机功能(点击确定后在权限中授权相机功能)') } else { this.$refs.galleryPopup.open(); plus.android.requestPermissions(['android.permission.READ_EXTERNAL_STORAGE'], (e) => { this.$refs.galleryPopup.close(); if (e.deniedAlways.length > 0) { //权限被永久拒绝 // 弹出提示框解释为何需要权限,引导用户打开设置页面开启 guideUserToAuthorize('请打开相册存储功能(点击确定后在权限中授权相册存储功能)') } else if (e.deniedPresent.length > 0) { //权限被临时拒绝 // 弹出提示框解释为何需要权限,可再次调用plus.android.requestPermissions申请权限 guideUserToAuthorize('请打开相册存储功能(点击确定后在权限中授权相册存储功能)') } else { // 执行业务代码 } }) } }) } } }; </script>
<style lang="scss"> // 权限说明弹窗 .popup-content { position: relative; z-index: 2000; background-color: #eee; width: 90%; margin: 0 auto; margin-top: 10vh; padding: 32rpx; border-radius: 32rpx; color: #000; // 阴影 box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.3); .title { font-size: 32rpx; font-weight: bold; } .content { margin-top: 16rpx; line-height: 1.5; } } </style>
|
Android 13
问题来了,Android 13 的相册权限变更为 READ_MEDIA_IMAGES
,如果继续使用之前的代码,将无法正确判断相册权限。因此,在判断和申请相册权限时,还需要判断当前系统版本。
plus.os.version
可以获取系统版本信息。
参考代码2(兼容Android13):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
| <template> <view> <uni-popup ref="cameraPopup" type="top"> <view class="popup-content"> <view class="title">相机权限说明</view> <view class="content">相机目的(根据实际业务替换)</view> </view> </uni-popup> <uni-popup ref="galleryPopup" type="top"> <view class="popup-content"> <view class="title">相册权限说明</view> <view class="content">相册目的(实际业务替换)</view> </view> </uni-popup> </view> </template>
<script> import { guideUserToAuthorize } from '@/utils';
export default { data() { return { }; }, methods: { $_chooseImage() { let platform = uni.getSystemInfoSync().platform; //首先判断app是安卓还是ios if (platform == "ios") { //这里是ios的方法 // 执行业务代码 } else if (platform == "android") { const sdkVersion = parseInt(plus.os.version.split('.')[0], 10); const cameraPermissionState = plus.navigator.checkPermission('android.permission.CAMERA') const readExternalStoragePermissionState = plus.navigator.checkPermission('android.permission.READ_EXTERNAL_STORAGE') const readMediaImagesPermissionState = plus.navigator.checkPermission('android.permission.READ_MEDIA_IMAGES') // Android 13 的照片权限是 READ_MEDIA_IMAGES if ((sdkVersion < 13 && cameraPermissionState === 'authorized' && readExternalStoragePermissionState === 'authorized') || (sdkVersion >= 13 && cameraPermissionState === 'authorized' && readMediaImagesPermissionState === 'authorized') ) { // 已经获得相机和相册权限 // 执行业务代码 } else { // 其中一个权限未取得或被拒绝 this.getAndroidCameraPermission() } } }, // 获得安卓相机权限 getAndroidCameraPermission() { this.$refs.cameraPopup.open(); plus.android.requestPermissions(['android.permission.CAMERA'], (e) => { this.$refs.cameraPopup.close(); if (e.deniedAlways.length > 0) { //权限被永久拒绝 // 弹出提示框解释为何需要权限,引导用户打开设置页面开启 // this.guideUserToAuthorize('请打开手机相机功能(点击确定后在权限中授权相机功能)') guideUserToAuthorize('请打开手机相机功能(点击确定后在权限中授权相机功能)') } else if (e.deniedPresent.length > 0) { //权限被临时拒绝 // 弹出提示框解释为何需要权限,可再次调用plus.android.requestPermissions申请权限 // this.guideUserToAuthorize('请打开手机相机功能(点击确定后在权限中授权相机功能)') guideUserToAuthorize('请打开手机相机功能(点击确定后在权限中授权相机功能)') } else { this.$refs.galleryPopup.open(); const sdkVersion = parseInt(plus.os.version.split('.')[0], 10); let permissionName = 'READ_EXTERNAL_STORAGE' // Android 13 的照片权限是 READ_MEDIA_IMAGES if (sdkVersion >= 13) { permissionName = 'READ_MEDIA_IMAGES' } plus.android.requestPermissions([`android.permission.${permissionName}`], (e) => { this.$refs.galleryPopup.close(); if (e.deniedAlways.length > 0) { //权限被永久拒绝 // 弹出提示框解释为何需要权限,引导用户打开设置页面开启 guideUserToAuthorize('请打开相册存储功能(点击确定后在权限中授权相册存储功能)') } else if (e.deniedPresent.length > 0) { //权限被临时拒绝 // 弹出提示框解释为何需要权限,可再次调用plus.android.requestPermissions申请权限 guideUserToAuthorize('请打开相册存储功能(点击确定后在权限中授权相册存储功能)') } else { // 执行业务代码 } }) } }) }, } }; </script>
<style lang="scss"> // 权限说明弹窗 .popup-content { position: relative; z-index: 2000; background-color: #eee; width: 90%; margin: 0 auto; margin-top: 10vh; padding: 32rpx; border-radius: 32rpx; color: #000; // 阴影 box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.3); .title { font-size: 32rpx; font-weight: bold; } .content { margin-top: 16rpx; line-height: 1.5; } } </style>
|
总结
通过使用 checkPermission
API 判断权限状态后,再决定是否调用 requestPermissions
API 申请权限。同时,使用uni-popup
弹窗同步告知目的,申请权限前显示弹窗,用户操作后关闭。
参考