快崩溃了!华为应用商店已经 4 次驳回我的应用上线

一. 前言

不得不说,华为应用商店的审核还是过于严格了,最近提交的新版本应用又被拒绝了!已经提交了4个版本了,再这样下去,我就要崩溃了!

好多人已经对华为的这项审核怨言颇深了!

其实,华为审核严格是一方面,另一方面主要在于 uni-app,由于该应用是使用 uni-app 开发并打包的,所以受限于 uni-app 框架,而它给添加了太多没有用的东西,都集成在框架中,并且删除不掉。

除了华为应用商店,顺带提一下,Google Play 应用商店曾经也因为 uni-app 框架的问题遭到拒绝。

像这种问题根本无解,只能找官方解决,受限于框架,没办法,不过最终也解决了。

而像本次华为应用商店驳回审核,需要修改的地方已经给罗列的很清楚了,只需要按照他们的说明整改即可,最起码我们能通过自己修改代码就可以完成,不用找官方解决,比较简单。

所以有两个问题需要整改:

  • 隐私政策声明
  • 申请权限时需告知用户使用目的

隐私政策文件的整改很简单,以 com.bun.miitmdid 为例,需要在隐私政策中添加相关说明即可。本篇文章我们直接进行申请权限时的整改,接下来进入正文!

二. 修改权限申请逻辑

前面提到华为应用商店的审核还是过于严格了,其实相比较其他国内应用市场(小米/VIVO/OPPO)来说,区别就在于用户在申请敏感权限时,需同步告知用户申请该权限的目的。对于申请的权限,都必须有明确、合理的使用场景和功能说明,禁止诱导或误导用户授权。

如下图所示:

目前应用内申请权限时是这样的:

接下来我们要进行整改,整改完成后是这样的:

三. 权限申请

1. 使用 plus.android.requestPermissions

统一通过 plus.android.requestPermissions 向系统请求权限,如果权限属于危险权限并且用户没有授权则会弹出系统提示框由用户授权确认。

js 复制代码
plus.android.requestPermissions(permissions, successCallback, errorCallback)

2. 参数说明

  • permissions: 申请的权限列表,权限列表参考 Android 官方列表

  • successCallback: 申请权限成功回调函数,参考 AndroidSuccessCallback,返回申请权限的结果,可能被用户允许,回调函数的参数 event 包含以下属性:

    • granted - Array[String]字符串数组,已获取权限列表;
    • deniedPresent - Array[String]字符串数据,已拒绝(临时)的权限列表;
    • deniedAlways - Array[String]字符串数据,永久拒绝的权限列表。
  • errorCallback: 申请权限失败回调函数,参考 AndroidErrorCallback

    • 通常传入参数错误时触发此回调。

注意:Android 系统 6+版本(API 等级 23+),并且必须设置 targetSdkVersion>=23。

如果已经授权或被用户拒绝则返回结果。 授权结果在 successCallback 回调参数中可获取。

为了便于统一申请权限,封装为以下方法,可直接复制使用!

js 复制代码
// Android权限查询
export function requestAndroidPermission(permissionID) {
  return new Promise((resolve, reject) => {
    plus.android.requestPermissions(
      // 理论上支持多个权限同时查询,但实际上本函数封装只处理了一个权限的情况。有需要的可自行扩展封装
      [permissionID],
      function (resultObj) {
        var result = 0
        for (var i = 0; i < resultObj.granted.length; i++) {
          var grantedPermission = resultObj.granted[i]
          console.log('已获取的权限:' + grantedPermission)
          result = 1
        }
        for (var i = 0; i < resultObj.deniedPresent.length; i++) {
          var deniedPresentPermission = resultObj.deniedPresent[i]
          console.log('拒绝本次申请的权限:' + deniedPresentPermission)
          result = 0
        }
        for (var i = 0; i < resultObj.deniedAlways.length; i++) {
          var deniedAlwaysPermission = resultObj.deniedAlways[i]
          console.log('永久拒绝申请的权限:' + deniedAlwaysPermission)
          result = -1
        }
        uni.setStorageSync('permisionStatus_' + permissionID, result)

        resolve(result)
        // 若所需权限被拒绝,则打开APP设置界面,可以在APP设置界面打开相应权限
        if (result != 1) {
          // gotoAppPermissionSetting()
          uni.showModal({
            content: '权限已经被拒绝,请前往APP设置界面打开相应权限'
          })
        }
      },
      function (error) {
        console.log('申请权限错误:' + error.code + ' = ' + error.message)
        resolve({
          code: error.code,
          message: error.message
        })
      }
    )
  })
}

此文件来源于 ext.dcloud.net.cn/plugin?id=5... 部分片段

使用方式:

js 复制代码
requestAndroidPermission('android.permission.WRITE_EXTERNAL_STORAGE').then(
  result => {
    // result 表示:1已获取,0已拒绝,-1永久拒绝。可根据返回码定向处理
  }
)

四. 原生弹窗

接下来我们应该构造一个弹窗类 NativePopup 用于在应用内申请权限时弹窗告知用户,说明申请权限的使用目的。

在这里,App 端使用 plus.nativeObj.view 绘制原生内容,参考:uni-app 中使用 5+界面控件plus.nativeObj.view 规范

代码如下,可直接复制使用!

js 复制代码
export class NativePopup {
  constructor(options = {}) {
    this.sysInfo = uni.getSystemInfoSync()

    const {
      bgColor = '#fff',
      titleColor = '#000',
      contentColor = '#272727'
    } = options

    this.bgColor = bgColor
    this.titleColor = titleColor
    this.contentColor = contentColor
  }

  createPopup = () => {
    const { statusBarHeight, screenWidth } = this.sysInfo

    const popupView = new plus.nativeObj.View('popupView', {
      top: 0,
      left: 0,
      width: screenWidth,
      height: 110 + statusBarHeight + 'px'
      // backgroundColor: 'blue' // debug
    })

    popupView.addEventListener('click', this.close)

    const bgPadding = 15

    popupView.drawRect(
      {
        color: 'rgba(0, 0, 0, 0.1)',
        radius: '10px'
      },
      {
        top: statusBarHeight + 7 + 'px',
        left: bgPadding - 2 + 'px',
        width: screenWidth - bgPadding * 2 + 4 + 'px',
        height: '100px'
      }
    )

    popupView.drawRect(
      {
        color: this.bgColor,
        radius: '10px'
      },
      {
        top: statusBarHeight + 5 + 'px',
        left: bgPadding + 'px',
        width: screenWidth - bgPadding * 2 + 'px',
        height: '100px'
      }
    )

    const padding = 10

    popupView.drawText(
      this.title,
      {
        top: statusBarHeight + 10 + 'px',
        left: padding + bgPadding + 'px',
        height: '30px',
        width: screenWidth - bgPadding * 2 - padding * 2 + 'px'
      },
      {
        size: '16px',
        weight: 'bold',
        align: 'left',
        color: this.titleColor
      },
      {
        onClick: function (e) {
          console.log(e)
        }
      }
    )

    popupView.drawText(
      this.content,
      {
        top: statusBarHeight + 40 + 'px',
        height: '60px',
        left: padding + bgPadding + 'px',
        width: screenWidth - bgPadding * 2 - padding * 2 + 'px'
      },
      {
        size: '14px',
        align: 'left',
        color: this.contentColor,
        whiteSpace: 'normal'
      }
    )

    this.popupView = popupView

    return popupView
  }

  show = (options = {}) => {
    this.close()

    const { title = '权限申请说明', content = '' } = options
    this.title = title
    this.content = content

    this.createPopup()

    this.popupView.show()
  }

  close = () => {
    this.popupView && this.popupView.close()
  }
}

export const popup = new NativePopup()

使用方式如下:

js 复制代码
import { popup } from './nativePopup.js'
// 显示
popup.show({
  title: '权限申请说明',
  content: '为了xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
})
// 关闭
popup.close()

五. 监听权限申请

createRequestPermissionListener

华为应用商店审核时要求:APP在调用终端权限时,应同步告知用户申请该权限的目的,可使用 uni.createRequestPermissionListener(),在 app.vue 里全局监听。

在 Android 平台,可使用该 API 监听应用权限申请确认框的弹出和关闭。不管是哪处的业务代码在申请权限,当弹出和关闭权限申请确认框时均会触发本监听事件。

创建监听对象后,返回 RequestPermissionListener,然后调起 onConfirmonComplete

  • 当权限申请的确认框在手机端弹出时,会触发 onConfirm,回调中会以数组方式提供权限名称列表。
  • 当权限申请的确认框被用户关闭后,会触发 onComplete

所以,通过监听权限申请,在 onConfirm 回调中弹窗,可以实现不改动业务代码,全局处理权限弹窗问题!

以下代码已经声明了大部分默认权限申请说明信息,如有新增或调整,可以进行更改或传入!

js 复制代码
import { popup } from './nativePopup.js'
import permisionUtil from './permission.js'

let permissionListener = null

const prefix = 'permisionStatus_'
const { uniPlatform, platform, osAndroidAPILevel } = uni.getSystemInfoSync()

const log = (...args) => {
  console.log(...args)
}

// 默认权限申请说明信息,可以按照以下形式进行拓展
const defaultPermissionExplainMap = {
  'android.permission.BLUETOOTH_SCAN': {
    title: '蓝牙扫描权限申请说明',
    content: '应用需要扫描附近的蓝牙设备,以便进行连接或数据传输。'
  },
  'android.permission.BLUETOOTH_CONNECT': {
    title: '蓝牙连接权限申请说明',
    content: '应用需要连接蓝牙设备,以便提供音频播放或数据通信功能。'
  },
  'android.permission.READ_MEDIA_IMAGE': {
    title: '读取图片权限申请说明',
    content: '应用需要访问您的图片库,以便加载和选择照片。'
  }
}

export const createRequestPermissionListener = (permissionExplainMap = {}) => {
  if (uniPlatform != 'app' || platform != 'android') return

  if (typeof permissionExplainMap != 'object')
    throw Error('permissionExplainMap 类型错误')

  permissionListener =
    permissionListener || uni.createRequestPermissionListener()

  permissionListener.onRequest(e => {
    log('onRequest', JSON.stringify(e))
  })

  permissionListener.onConfirm(e => {
    const [permissionName] = e

    const status = uni.getStorageSync(prefix + permissionName)
    log('onConfirm permissionName', permissionName, status)
    const content =
      permissionExplainMap[permissionName] ||
      defaultPermissionExplainMap[permissionName]
    if (!status && content) popup.show(content)
  })

  permissionListener.onComplete(e => {
    const [permissionName] = e

    const status = uni.getStorageSync(prefix + permissionName)
    log('onComplete permissionName', permissionName, status)
    popup.close()
  })
}

export const stopRequestPermissionListener = () => {
  permissionListener && permissionListener.stop()
}

export { permisionUtil }

六. 用法说明

1. 引入全局监听

App.vue 的生命周期中开始监听,停止监听。

js 复制代码
import { createRequestPermissionListener } from '@/uni_modules/permission/index.js'
export default {
  onLaunch() {
    createRequestPermissionListener()
  },
  onExit() {
    stopRequestPermissionListener()
  }
}

2. 申请权限

在应用使用权限之前进行检测权限申请,例如,在进行扫码前先申请相机权限:

注意:可以不进行主动申请权限,因为在全局已经做了监听,弹窗会自动弹出!但为了特殊情况(比如在使用原生的权限申请操作,无法监听到),建议在这种情况下提前申请权限!

js 复制代码
import { requestAndroidPermission } from '@/uni_modules/permission/index.js'

async requestPermission() {
    const status = await requestAndroidPermission('android.permission.CAMERA')
    if (status != 1) {
        // 权限被拒绝
        return
    }
}

3. 渠道包

大多数情况下都是为了解决华为应用市场的审核问题,所以在这里只处理华为应用的权限弹窗即可,通过渠道包来判断。

参考:Android 平台自定义渠道包

通过 plus.runtime.channel 来判断渠道来源,只对华为应用市场的应用进行处理!

所以在开始监听权限、停止监听权限、申请权限统一加上以下代码即可。

js 复制代码
if (plus.runtime.channel === 'huawei') {
}

在进行打包时勾选一下渠道包选项:

七. 注意事项

  • 如果权限已经申请并且允许之后,onConfirm不会触发。
  • 如果同时申请多个权限时,onComplete可能会触发多次。
  • 只能监听通过 uniapp 或 plus 提供的权限申请时弹出提示,如果你使用原生的权限申请操作,无法监听到!

八. 总结

本文主要介绍了如何解决华为应用市场审核的问题,主要涉及两个方面:

  1. 隐私政策声明

    • 需要在隐私政策中明确声明应用使用的 SDK 信息
    • 包括 SDK 名称、包名、使用目的、使用的权限、涉及的个人信息等
    • 以 com.bun.miitmdid 为例,需要在隐私政策中添加相关说明
  2. 权限申请优化

    • 在申请敏感权限时,需要同步告知用户申请该权限的目的
    • 提供了完整的权限申请解决方案:
      • 封装了 Android 权限申请方法
      • 实现了原生弹窗组件用于权限说明
      • 通过全局监听权限申请,自动显示权限说明
      • 提供了常用权限的默认说明文案
    • 特别针对华为应用市场做了渠道包判断
  3. 技术实现要点

    • 使用 plus.android.requestPermissions 进行权限申请
    • 使用 plus.nativeObj.view 实现原生弹窗
    • 使用 uni.createRequestPermissionListener 监听权限申请
    • 通过 plus.runtime.channel 判断应用渠道

通过以上优化,可以有效解决华为应用市场的审核问题,提升应用的用户体验和合规性。同时,这些优化措施也可以作为其他应用市场的参考,提高应用的整体质量。

参考文档

华为应用隐私合规问题小学堂

Android 平台各功能模块隐私合规协议

Android 平台权限列表参考

Android 平台权限申请 requestPermissions

Android 平台监听权限申请

Android 平台自定义渠道包

相关推荐
wyiyiyi20 分钟前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip42 分钟前
vite和webpack打包结构控制
前端·javascript
excel1 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国1 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼1 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy1 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT2 小时前
promise & async await总结
前端
Jerry说前后端2 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化
画个太阳作晴天2 小时前
A12预装app
linux·服务器·前端
7723892 小时前
解决 Microsoft Edge 显示“由你的组织管理”问题
前端·microsoft·edge