解决 uniapp 开发中的相机相册权限申请同步告知目的问题(兼容 Android 13)| 华为应用商店上架解决方案
本文将分享如何在 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
- 被永久拒绝的权限列表(字符串数组)。
官方使用示例:
// 申请定位权限
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){ //权限被临时拒绝
// 弹出提示框解释为何需要定位权限,可再次调用plus.android.requestPermissions申请权限
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));
});
}
官方示例中已经提到,如果权限被永久拒绝,需要提示用户手动打开系统设置进行授权。以下是实现代码:
// 引导用户授权
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
组件来实现持久性弹窗。 该组件可以手动打开或关闭,并且可以自定义样式。
何时打开何时关闭?
如果检查权限时发现权限未被授予,则在申请权限之前打开弹窗;在用户操作后触发成功回调函数时,关闭弹窗。
<uni-popup ref="cameraPopup" type="top">
<view class="popup-content">
<view class="title">相机权限说明</view>
<view class="content">你的目的</view>
</view>
</uni-popup>
// 权限说明弹窗
.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
参考上面给出的代码。
<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):
<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
弹窗同步告知目的,申请权限前显示弹窗,用户操作后关闭。
参考
- 查询权限 API checkPermission 介绍
- 请求权限 API requestPermissions 介绍
- 系统版本属性 plus.os.version 介绍
首发地址:https://blog.xchive.top/2025/solve-the-problem-of-synchronized-notification-purpose-of-camera-album-permission-request-in-uniapp-development.html