HarmonyOS 敏感权限动态申请实战:atManager 申请、状态校验与引导去设置

前言

定位、相机、麦克风、通讯录这些敏感权限,HarmonyOS 不允许应用默认拥有,必须在运行时向用户申请,且用户拒绝后不能反复弹窗骚扰。很多应用一启动就"相机闪退"或"定位拿不到",根因就是没走标准的动态权限申请流程。本文基于 ability_accessCtrl(AtManager),给出从"检查已授权 → 申请 → 处理拒绝 → 引导去系统设置"的完整闭环代码。

问题描述

典型故障:

  1. 直接 camera.getCameraManager() 不申请权限,系统抛 201 错误,应用崩溃。
  2. 用户点了"拒绝且不再提示",应用每次都弹申请框,用户恼火,且系统可能直接禁止再弹。
  3. 申请了 ohos.permission.LOCATION,但没申请 ohos.permission.APPROXIMATELY_LOCATIONPRECISE_LOCATION 子权限,定位返回空。
  4. 权限被拒后,界面还停留在"等待授权"状态,没有给用户"去设置页手动开启"的出口。

核心要求:先查后申、按权限组申请、拒绝后降级处理、提供设置页入口。

细节解析

1. 权限分类

  • system_grant(系统授权):安装即授权,无需弹窗(如 INTERNET)。
  • user_grant(用户授权):必须运行时弹窗申请(如 CAMERALOCATIONMICROPHONE)。本文聚焦这一类。
  • 部分权限有子权限:定位分 ohos.permission.APPROXIMATELY_LOCATION(模糊)与 ohos.permission.PRECISE_LOCATION(精确),申请精确必须同时声明模糊。

2. 标准三步

  1. atManager.checkAccessToken(tokenId, permission) 查当前授权状态。
  2. GRANTED 直接用;否则 atManager.requestPermissionsFromUser(context, [perms]) 弹窗申请。
  3. 解析 PermissionRequestResultauthResults[i] === 0 为授权;非 0 看 dialogShownResults 判断是否还能再弹。

3. 拒绝后的处理

  • 如果 dialogShownResults[i] === 0(弹过且被拒)→ 不要再弹,应提示用户并引导去系统设置:ability_accessCtrl.openPermissionSettings(context)(或 requestPermissionOnSetting)。
  • 如果 dialogShownResults[i] === -1(用户选了"不再提示")→ 只能去设置页。

4. module.json5 声明

申请的权限必须先在 module.json5requestPermissions 里声明,否则 requestPermissionsFromUser 直接失败。

示例代码

权限工具封装

// src/main/ets/permission/PermissionUtil.ets
import { ability_accessCtrl, Permissions, PermissionRequestResult } from '@kit.AbilityKit'
import { common } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'

export class PermissionUtil {
  private static atManager = ability_accessCtrl.createAtManager()

  // 检查单个权限是否已授权
  static async check(permission: Permissions): Promise<boolean> {
    const ctx = getContext() as common.UIAbilityContext
    const tokenId = ctx.applicationInfo.accessTokenId
    const status = await this.atManager.checkAccessToken(tokenId, permission)
    return status === ability_accessCtrl.GrantStatus.PERMISSION_GRANTED
  }

  // 申请一组权限,返回每个权限是否成功
  static async request(permissions: Permissions[]): Promise<Record<string, boolean>> {
    const ctx = getContext() as common.UIAbilityContext
    const result: Record<string, boolean> = {}
    try {
      const res: PermissionRequestResult = await this.atManager.requestPermissionsFromUser(ctx, permissions)
      permissions.forEach((p, i) => {
        result[p] = res.authResults[i] === 0
      })
      return result
    } catch (err) {
      const e = err as BusinessError
      console.error(`申请权限失败 code=${e.code} msg=${e.message}`)
      permissions.forEach(p => result[p] = false)
      return result
    }
  }

  // 是否还能再次弹窗(false 表示用户选了"不再提示",只能去设置)
  static async canAskAgain(permission: Permissions): Promise<boolean> {
    const ctx = getContext() as common.UIAbilityContext
    try {
      const res = await this.atManager.requestPermissionOnSetting(ctx, [permission])
      return res[0] === 0
    } catch {
      return false
    }
  }

  // 引导去系统设置页开启权限
  static async openSettings() {
    const ctx = getContext() as common.UIAbilityContext
    try {
      await this.atManager.openPermissionSettings(ctx)
    } catch (err) {
      const e = err as BusinessError
      console.error(`打开设置失败 code=${e.code}`)
    }
  }
}

在页面里使用(以相机为例)

// src/main/ets/pages/CameraPage.ets
import { Permissions } from '@kit.AbilityKit'
import { PermissionUtil } from '../permission/PermissionUtil'
import { promptAction } from '@kit.ArkUI'

@Entry
@Component
struct CameraPage {
  @State ready: boolean = false

  async ensureCamera(): Promise<boolean> {
    const perm: Permissions = 'ohos.permission.CAMERA'
    if (await PermissionUtil.check(perm)) {
      this.ready = true
      return true
    }
    const res = await PermissionUtil.request([perm])
    if (res[perm]) {
      this.ready = true
      return true
    }
    // 被拒:尝试引导去设置
    const canAsk = await PermissionUtil.canAskAgain(perm)
    if (canAsk) {
      // 还能弹:再次申请(实际多发生在用户点了"拒绝"但没选不再提示)
      const r2 = await PermissionUtil.request([perm])
      if (r2[perm]) { this.ready = true; return true }
    }
    // 彻底没权限:提示去设置
    AlertDialog.show({
      message: '需要相机权限才能拍照,请在设置中开启',
      primaryButton: { action: () => PermissionUtil.openSettings() },
      secondaryButton: { action: () => promptAction.showToast({ message: '已取消' }) }
    })
    return false
  }

  build() {
    Column({ space: 16 }) {
      if (this.ready) {
        Text('相机已就绪,可以拍照').fontSize(18)
      } else {
        Button('申请相机权限并拍照').onClick(() => this.ensureCamera())
      }
    }
    .width('100%').height('100%').justifyContent(FlexAlign.Center)
  }
}

module.json5 必须声明

{
  "module": {
    "requestPermissions": [
      { "name": "ohos.permission.CAMERA", "reason": "$string:camera_reason", "usedScene": { "abilities": ["EntryAbility"], "when": "always" } },
      { "name": "ohos.permission.APPROXIMATELY_LOCATION", "reason": "$string:loc_reason", "usedScene": { "abilities": ["EntryAbility"], "when": "inUse" } },
      { "name": "ohos.permission.PRECISE_LOCATION", "reason": "$string:loc_reason", "usedScene": { "abilities": ["EntryAbility"], "when": "inUse" } }
    ]
  }
}

总结

  1. 先声明后申请module.json5requestPermissions 不写,运行时申请直接报 201
  2. checkAccessTokenrequest:已授权就别弹窗,减少打扰。
  3. 子权限成对申请:精确定位要同时声明 APPROXIMATELY_LOCATION + PRECISE_LOCATION
  4. 拒绝后别硬弹:看 authResultsdialogShownResults,用户选"不再提示"就 openPermissionSettings 引导去系统设置。
  5. 给用户出口:被拒界面要提供"去设置开启"按钮,否则功能就卡死在不可用状态。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐