基于鸿蒙OS开发静脉输液智能监控系统(19)-权限管理与安全设计

目录


1. HarmonyOS 权限体系

1.1 授权方式:system_grant 与 user_grant

HarmonyOS 的权限管理采用二元授权模型,所有权限按授权方式分为 system_grant(系统授权)user_grant(用户授权) 两大类。理解这两类权限的本质区别,是正确配置和使用权限的前提。

1.1.1 system_grant(系统授权)

system_grant 类型的权限对应的数据或功能不会涉及用户或设备的敏感信息。应用在安装阶段,系统会自动将相应权限授予给应用,无需用户手动确认

核心特征

  • 授权时机:应用安装时自动授予,用户无感知
  • 用户交互:无弹窗,无确认对话框
  • 配置要求:在 module.json5requestPermissions 中仅需声明 name 字段
  • 不可撤销:用户无法通过系统设置手动关闭此类权限(应用卸载时自动回收)
  • 典型权限ohos.permission.INTERNETohos.permission.VIBRATEohos.permission.GET_WIFI_INFOohos.permission.KEEP_BACKGROUND_RUNNING

示例:当 IVGuard 申请振动权限(VIBRATE)时,用户安装应用后系统自动授予,无需任何确认操作。这是因为振动功能属于设备基础能力,不涉及用户隐私数据。

1.1.2 user_grant(用户授权)

user_grant 类型的权限涉及用户或设备的敏感信息。应用安装后不会自动获取此类权限,必须在运行时主动向用户发起授权请求,由用户在弹窗中选择"允许"或"拒绝"。

核心特征

  • 授权时机:运行时动态申请,用户主动授权
  • 用户交互:系统弹出授权对话框,用户点击"允许"或"拒绝"
  • 配置要求:在 module.json5 中必须完整配置 namereasonusedScene 三个字段
  • 可撤销:用户可随时通过"设置 > 隐私 > 权限管理"关闭已授权的权限
  • 二次授权引导:用户拒绝后,再次申请时系统不再弹窗,需引导用户到系统设置页手动开启
  • 典型权限ohos.permission.CAMERAohos.permission.LOCATIONohos.permission.NFC_TAG

示例:当 IVGuard 需要使用后置摄像头监控输液瓶时,必须调用 requestPermissionsFromUser() 触发系统弹窗,用户点击"允许"后才能访问相机。如果用户拒绝,则无法使用相机功能,应用需提供降级方案。

1.1.3 两类权限对比总览
维度 system_grant user_grant
授权时机 安装时自动授予 运行时弹窗请求
用户感知 无感知 明确感知(弹窗)
配置字段 name name + reason + usedScene
可否撤销 不可手动撤销 用户可随时撤销
涉及隐私 不涉及敏感信息 涉及敏感信息
代码申请 无需运行时申请 需调用 requestPermissionsFromUser
上架审核 配置即通过 需审查 reason 合理性
二次授权 不适用 拒绝后需引导至设置

1.2 受限权限(restricted permissions)

受限权限是 HarmonyOS 权限体系中管控最严格的一类权限。这类权限即使属于 system_grantuser_grant,也不能被普通应用直接申请,需要经过额外的审批流程

受限权限的特征

  1. APL 等级要求高:通常需要 system_basicsystem_core 级别的应用才能申请
  2. ACL 审批:普通 normal 等级的应用若需使用受限权限,必须通过访问控制列表(ACL)申请特殊审批
  3. 上架审核严格:应用商店会重点审查受限权限的使用必要性和合理性
  4. 滥用风险高:受限权限通常涉及系统核心资源或高度敏感的用户数据

IVGuard 项目中不涉及受限权限,这是最小权限原则的体现。所有 8 项权限均属于 normal 级别的开放权限,无需 ACL 审批,降低了上架审核风险和用户信任门槛。

常见的受限权限示例(仅供了解,IVGuard 不使用):

  • ohos.permission.READ_CONTACTS:读取通讯录
  • ohos.permission.READ_MESSAGES:读取短信
  • ohos.permission.ANSWER_CALL:接听电话
  • ohos.permission.MANAGE_VOICEMAIL:管理语音信箱

1.3 权限级别:normal / system_basic / system_core

HarmonyOS 的每个权限都有一个 APL(Ability Privilege Level)等级标签,决定了哪些等级的应用可以申请该权限。

normal(普通级别)
  • 开放范围:所有应用均可申请
  • 风险等级:低——权限对应的系统资源开放对用户隐私和其他应用带来的风险低
  • IVGuard 关联:IVGuard 申请的全部 8 项权限均为 normal 级别

normal 级别权限列表(IVGuard 使用的部分)

权限名称 授权方式 说明
ohos.permission.CAMERA user_grant 使用相机
ohos.permission.VIBRATE system_grant 控制马达振动
ohos.permission.GET_WIFI_INFO system_grant 获取 WiFi 信息
ohos.permission.KEEP_BACKGROUND_RUNNING system_grant 后台持续运行
ohos.permission.NFC_TAG system_grant 读写 NFC 标签
ohos.permission.LOCATION user_grant 获取精准位置
ohos.permission.APPROXIMATELY_LOCATION user_grant 获取模糊位置
system_basic(基本级别)
  • 开放范围:APL 等级为 system_basic 及以上的应用
  • 风险等级:较高——涉及操作系统基础服务相关的资源
  • 典型场景:系统设置、身份认证、设备管理
  • 申请方式:需要使用 system_basic 级别的签名证书
system_core(核心级别)
  • 开放范围:仅对系统应用开放,第三方应用不可申请
  • 风险等级:最高——涉及操作系统核心底层服务
  • 典型场景:系统核心服务、底层硬件管理
  • 不可跨级申请:即使通过 ACL 也不允许 normal 等级应用获取 system_core 权限

权限级别与应用 APL 的对应关系

应用 APL 等级    →    可申请的权限级别
─────────────────────────────────────
normal           →    normal
system_basic     →    normal + system_basic
system_core      →    normal + system_basic + system_core(仅系统应用)

1.4 APL 等级与 ACL 机制

APL(Ability Privilege Level)是 HarmonyOS 为应用和权限分别定义的安全等级标签。原则上,低 APL 等级的应用默认无法申请更高等级的权限。但 ACL(Access Control List,访问控制列表)机制提供了一种特殊渠道,允许低等级应用在经过审批后访问高等级权限。

ACL 的工作机制

  1. 声明:在应用的配置文件中声明 ACL token
  2. 审批:提交至 HarmonyOS 开发者平台进行审核
  3. 签名:审核通过后,将 ACL token 写入应用的签名证书
  4. 生效:系统在安装应用时验证签名中的 ACL token,允许跨等级访问

IVGuard 的 APL 策略

IVGuard 选择不使用任何 ACL 机制,原因如下:

  • 所有功能所需的权限均为 normal 级别,无需跨等级访问
  • 避免 ACL 审批带来的上架周期延长
  • 降低用户对"特权应用"的安全顾虑
  • 符合最小权限原则和最小化攻击面原则

1.5 权限组与子权限

HarmonyOS 将相关联的权限组织为权限组(Permission Group)。当应用同时申请同一权限组内的多个子权限时,系统会合并为一个弹窗向用户展示,而不是逐个弹窗询问。

IVGuard 涉及的权限组

位置信息权限组
子权限 授权方式 精度
ohos.permission.LOCATION user_grant 精准位置(米级)
ohos.permission.APPROXIMATELY_LOCATION user_grant 模糊位置(约 5 公里)

当 IVGuard 同时申请 LOCATIONAPPROXIMATELY_LOCATION 时,系统只会弹出一个授权对话框,用户授权后两个权限同时生效。这种设计减少了弹窗次数,提升了用户体验。

注意事项

  • 精准位置(LOCATION)依赖模糊位置(APPROXIMATELY_LOCATION),必须同时申请
  • 如果用户仅授予模糊位置,IVGuard 应使用降级策略,不要求精准定位也能提供基本导航
  • 后台定位需要额外申请 ohos.permission.LOCATION_IN_BACKGROUND,IVGuard 不申请此权限

2. IVGuard 8 项权限详解

IVGuard 作为智能输液监护系统,经过严格的需求分析和权限最小化评审,最终确定申请以下 8 项权限。每一项权限都与核心功能强关联,不存在冗余权限。

2.1 CAMERA(相机权限)

基本信息
属性
权限名称 ohos.permission.CAMERA
授权方式 user_grant(用户授权)
权限级别 normal
权限组 相机
起始 API 版本 9
使用时机 inuse(仅前台使用)
用途说明

CAMERA 权限是 IVGuard 最核心的权限之一,用于后置摄像头实时监控输液瓶液位。应用通过后置摄像头持续采集输液瓶的图像数据,利用计算机视觉算法识别液面位置,计算剩余液量和预计输完时间。

具体使用场景

  1. 液位监控:用户将设备后置摄像头对准输液瓶,系统通过实时图像分析识别液面高度
  2. 滴速检测:通过连续帧分析检测滴斗中的液滴滴落频率,推算输液速度
  3. 异常识别:识别输液瓶倾斜、气泡、回血等异常状态

为什么是后置摄像头

IVGuard 场景中,用户将手机架设在输液瓶旁边,使用后置摄像头对准输液瓶进行监控。后置摄像头的优势在于:

  • 更高的分辨率和更好的成像质量
  • 支持自动对焦,适合不同距离的输液瓶
  • 与用户操作面同一侧,便于支架固定
module.json5 配置
{
  "name": "ohos.permission.CAMERA",
  "reason": "$string:camera_reason",
  "usedScene": {
    "abilities": ["EntryAbility"],
    "when": "inuse"
  }
}

配置要点

  • reason:必须引用字符串资源($string:camera_reason),不能直接写中文字符串
  • usedScene.when:设为 inuse,因为相机仅在前台监控时使用,用户离开监控页面后应释放相机资源
  • usedScene.abilities:指定 EntryAbility,限定权限使用范围
字符串资源定义

resources/base/element/string.json 中定义:

{
  "string": [
    {
      "name": "camera_reason",
      "value": "用于后置摄像头监控输液瓶液位,以便及时预警输液异常"
    }
  ]
}

reason 文案规范

  • 必须是完整短句,直白、具体、易理解
  • 避免被动语态(“被用于” → “用于”)
  • 以句号结尾
  • 明确说明"用于什么"以及"对用户的好处"
  • 该文案会显示在系统授权弹窗中,用户可见
运行时申请代码
import { abilityAccessCtrl, Permissions, PermissionRequestResult, common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

async function requestCameraPermission(context: common.UIAbilityContext): Promise<boolean> {
  const atManager = abilityAccessCtrl.createAtManager();
  const permissions: Array<Permissions> = ['ohos.permission.CAMERA'];
  try {
    const result: PermissionRequestResult =
      await atManager.requestPermissionsFromUser(context, permissions);
    if (result.authResults.length > 0 && result.authResults[0] === 0) {
      return true;
    }
    return false;
  } catch (e) {
    const err = e as BusinessError;
    console.error(`CAMERA permission request failed: code=${err.code}, message=${err.message}`);
    return false;
  }
}

2.2 NFC_TAG(NFC 标签权限)

基本信息
属性
权限名称 ohos.permission.NFC_TAG
授权方式 user_grant(用户授权)
权限级别 normal
起始 API 版本 7
使用时机 inuse(仅前台使用)
用途说明

NFC_TAG 权限用于读取 NFC 贴纸中配对的药物信息。IVGuard 在输液瓶上贴附 NFC 标签,护士或患者用手机触碰标签即可快速录入药物信息,包括:

  • 药品名称与编码
  • 剂量与浓度
  • 配药时间
  • 患者关联信息

NFC 贴纸方案的设计考量

  1. 快速录入:相比手动输入药品信息,NFC 一碰即读,极大提升护士工作效率
  2. 防错机制:NFC 数据由药房系统写入,避免人工输入导致的药物信息错误
  3. 可追溯性:每次 NFC 读取都有时间戳记录,支持医疗操作追溯
  4. 数据完整性:NFC 标签存储结构化数据,保证信息格式统一

NFC 技术支持

IVGuard 支持以下 NFC 标签技术类型:

  • NfcA(ISO 14443-3A):最常见,兼容性最广
  • NfcB(ISO 14443-3B):部分医疗设备标签使用
  • NDEF:标准 NFC 数据交换格式,推荐用于药品信息存储
  • MifareUltralight:低成本标签,适合大规模部署
module.json5 配置
{
  "name": "ohos.permission.NFC_TAG",
  "reason": "$string:nfc_tag_reason",
  "usedScene": {
    "abilities": ["EntryAbility"],
    "when": "inuse"
  }
}
字符串资源定义
{
  "name": "nfc_tag_reason",
  "value": "用于读取输液瓶NFC贴纸中的药物信息,快速准确地录入配药数据"
}
NFC 前台读取关键代码
import { tag } from '@kit.ConnectivityKit';
import { BusinessError } from '@kit.BasicServicesKit';

async function readNfcTag(tagInfo: tag.TagInfo): Promise<string> {
  if (tagInfo === null || tagInfo === undefined) {
    return '';
  }
  if (tagInfo.technology === null || tagInfo.technology === undefined
      || tagInfo.technology.length === 0) {
    return '';
  }
  let ndefTag: tag.NdefTag | null = null;
  for (let i = 0; i < tagInfo.technology.length; i++) {
    if (tagInfo.technology[i] === tag.NDEF) {
      try {
        ndefTag = tag.getNdef(tagInfo);
      } catch (error) {
        const err = error as BusinessError;
        console.error(`getNdef failed: code=${err.code}, message=${err.message}`);
        return '';
      }
      break;
    }
  }
  if (ndefTag === null) {
    return '';
  }
  try {
    ndefTag.connect();
    const ndefMessage: tag.NdefMessage | null = ndefTag.getNdefMessage();
    if (ndefMessage !== null) {
      const records: tag.NdefRecord[] = ndefMessage.getNdefRecords();
      if (records.length > 0) {
        const payload: string = records[0].payloadToString();
        ndefTag.close();
        return payload;
      }
    }
    ndefTag.close();
  } catch (error) {
    const err = error as BusinessError;
    console.error(`NFC read failed: code=${err.code}, message=${err.message}`);
  }
  return '';
}

NFC skills 配置

module.json5abilities 配置中,还需要添加 NFC 标签发现的 action:

{
  "abilities": [
    {
      "name": "EntryAbility",
      "skills": [
        {
          "entities": ["entity.system.home"],
          "actions": [
            "action.system.home",
            "ohos.nfc.tag.action.TAG_FOUND"
          ]
        }
      ]
    }
  ]
}

2.3 NOTIFICATION_CONTROLLER(通知权限)

基本信息
属性
权限名称 ohos.permission.NOTIFICATION_CONTROLLER
授权方式 user_grant(用户授权)
权限级别 normal
起始 API 版本 9
使用时机 inuse(仅前台使用)
用途说明

NOTIFICATION_CONTROLLER 权限用于发送输液预警通知。当监控系统检测到输液异常时,通过系统通知渠道向用户发送预警信息。

通知场景分类

  1. 紧急预警(高优先级):

    • 输液即将完成(剩余液量 < 10%)
    • 检测到回血
    • 输液管路阻塞
    • 滴速异常突变
  2. 一般提醒(标准优先级):

    • 液位到达预设阈值
    • 预计输完时间提醒
    • NFC 配对成功确认
  3. 信息通知(低优先级):

    • 监控状态更新
    • 护士站消息推送

通知设计原则

  • 紧急预警必须通过系统通知发送,确保用户即使不在应用内也能及时感知
  • 通知内容不含具体医疗数据(如药品名称),仅提示"输液异常,请查看"
  • 用户可在应用内自定义通知偏好(开关、阈值、提醒方式)
  • 通知支持点击跳转到对应的监控详情页
module.json5 配置
{
  "name": "ohos.permission.NOTIFICATION_CONTROLLER",
  "reason": "$string:notification_reason",
  "usedScene": {
    "abilities": ["EntryAbility"],
    "when": "inuse"
  }
}
字符串资源定义
{
  "name": "notification_reason",
  "value": "用于发送输液异常预警通知,确保您及时了解输液状态变化"
}
通知发送代码示例
import { notificationManager } from '@kit.NotificationKit';
import { BusinessError } from '@kit.BasicServicesKit';

async function sendInfusionAlert(title: string, text: string): Promise<void> {
  const request: notificationManager.NotificationRequest = {
    id: 1,
    content: {
      notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
      normal: {
        title: title,
        text: text
      }
    }
  };
  try {
    await notificationManager.publish(request);
  } catch (e) {
    const err = e as BusinessError;
    console.error(`Notification publish failed: code=${err.code}, message=${err.message}`);
  }
}

2.4 KEEP_BACKGROUND_RUNNING(后台运行权限)

基本信息
属性
权限名称 ohos.permission.KEEP_BACKGROUND_RUNNING
授权方式 system_grant(系统授权)
权限级别 normal
起始 API 版本 8
使用时机 always(始终运行)
用途说明

KEEP_BACKGROUND_RUNNING 是 IVGuard 8 项权限中唯一使用 always 时机的权限,也是唯一一个 system_grant 类型但需要配合长时任务 API 使用的权限。它允许应用在后台持续运行,确保输液监控不会因为应用退至后台而中断。

为什么必须后台运行

输液监控是一个持续过程,典型的输液周期为 30 分钟至数小时不等。在此期间:

  • 患者可能查看其他应用(如微信、浏览器)
  • 患者可能锁屏等待
  • 护士可能切换到其他工作应用

如果应用退至后台被系统挂起,监控中断将导致无法及时发现输液异常,可能造成严重医疗安全事故。

长时任务类型选择

IVGuard 使用的长时任务类型为 BackgroundMode.DATA_TRANSFER,因为摄像头持续采集和图像分析属于持续数据传输和处理场景。在后台运行时,系统通知栏会显示"IVGuard 正在后台进行数据传输任务"的提示信息。

后台运行约束

  1. 系统会进行一致性校验,确保应用实际执行了对应类型的长时任务
  2. 通知栏消息被用户删除时,系统会自动停止长时任务
  3. 长时任务期间不可执行与声明类型不符的操作
  4. 需在 module.json5 中同时声明 backgroundModes
module.json5 配置
{
  "name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
}

注意:system_grant 权限无需 reasonusedScene 字段,仅需声明 name

长时任务代码实现
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { wantAgent, common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

async function startBackgroundMonitoring(
  context: common.UIAbilityContext
): Promise<void> {
  const wantAgentInfo: wantAgent.WantAgentInfo = {
    wants: [
      {
        bundleName: context.abilityInfo.bundleName,
        abilityName: context.abilityInfo.name
      }
    ],
    actionType: wantAgent.OperationType.START_ABILITY,
    requestCode: 0,
    wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
  };
  try {
    const wantAgentObj: wantAgent.WantAgent =
      await wantAgent.getWantAgent(wantAgentInfo);
    await backgroundTaskManager.startBackgroundRunning(
      context,
      backgroundTaskManager.BackgroundMode.DATA_TRANSFER,
      wantAgentObj
    );
    console.info('Background monitoring task started successfully');
  } catch (e) {
    const err = e as BusinessError;
    console.error(`startBackgroundRunning failed: code=${err.code}`);
  }
}

async function stopBackgroundMonitoring(
  context: common.UIAbilityContext
): Promise<void> {
  try {
    await backgroundTaskManager.stopBackgroundRunning(context);
    console.info('Background monitoring task stopped successfully');
  } catch (e) {
    const err = e as BusinessError;
    console.error(`stopBackgroundRunning failed: code=${err.code}`);
  }
}
backgroundModes 声明

module.json5 的 abilities 配置中,还需声明后台模式:

{
  "abilities": [
    {
      "name": "EntryAbility",
      "backgroundModes": ["dataTransfer"]
    }
  ]
}

2.5 VIBRATE(振动权限)

基本信息
属性
权限名称 ohos.permission.VIBRATE
授权方式 system_grant(系统授权)
权限级别 normal
起始 API 版本 7
用途说明

VIBRATE 权限用于在输液异常时提供振动提醒。振动是一种无需查看屏幕即可感知的提醒方式,特别适合以下场景:

  1. 紧急异常提醒:检测到回血、阻塞等严重异常时,配合通知和声音进行振动提醒
  2. 阈值到达提醒:液位到达预设阈值时短振动提示
  3. NFC 配对反馈:成功读取 NFC 标签时短振动确认
  4. 操作确认:关键操作(如启动监控、停止监控)的触觉反馈

振动模式设计

场景 振动模式 持续时间
紧急异常 长振动 500ms
一般提醒 短振动 100ms
NFC 配对成功 双短振 50ms × 2
操作确认 微振 15ms
module.json5 配置
{
  "name": "ohos.permission.VIBRATE"
}

注意:system_grant 权限无需 reason 字段。VIBRATE 作为设备基础能力权限,安装时自动授予。

2.6 LOCATION(精确定位权限)

基本信息
属性
权限名称 ohos.permission.LOCATION
授权方式 user_grant(用户授权)
权限级别 normal
权限组 位置信息
起始 API 版本 9
使用时机 inuse(仅前台使用)
用途说明

LOCATION 权限用于医院室内导航功能。IVGuard 的定位场景包括:

  1. 患者位置定位:确定患者所在病区、楼层和病床位置
  2. 护士站导航:为需要帮助的患者提供到最近护士站的路线
  3. 紧急求助定位:紧急情况下快速定位患者位置,通知就近护士
  4. 设施查找:查找最近的洗手间、电梯、药房等

定位精度需求

IVGuard 在医院场景下需要米级精度定位,原因如下:

  • 医院病区走廊布局复杂,模糊定位(5 公里精度)完全无法区分病区
  • 紧急求助场景要求精确定位到具体病床
  • 室内导航需要精确到 2-3 米范围才有实用价值

与 APPROXIMATELY_LOCATION 的关系

LOCATION(精准位置)和 APPROXIMATELY_LOCATION(模糊位置)属于同一权限组,必须同时申请。精准位置依赖模糊位置,不能单独申请 LOCATION。当用户授予位置权限时,两个权限同时生效。

module.json5 配置
{
  "name": "ohos.permission.LOCATION",
  "reason": "$string:location_reason",
  "usedScene": {
    "abilities": ["EntryAbility"],
    "when": "inuse"
  }
}
字符串资源定义
{
  "name": "location_reason",
  "value": "用于医院室内导航,帮助您快速找到护士站和医疗设施"
}
定位获取代码示例
import { geoLocationManager } from '@kit.LocationKit';
import { BusinessError } from '@kit.BasicServicesKit';

async function getCurrentPosition(): Promise<geoLocationManager.Location | null> {
  try {
    const location: geoLocationManager.Location =
      await geoLocationManager.getCurrentLocation();
    return location;
  } catch (e) {
    const err = e as BusinessError;
    console.error(`getCurrentLocation failed: code=${err.code}, message=${err.message}`);
    return null;
  }
}

2.7 APPROXIMATELY_LOCATION(模糊定位权限)

基本信息
属性
权限名称 ohos.permission.APPROXIMATELY_LOCATION
授权方式 user_grant(用户授权)
权限级别 normal
权限组 位置信息
起始 API 版本 9
使用时机 inuse(仅前台使用)
用途说明

APPROXIMATELY_LOCATION 是 LOCATION 的前置依赖权限。在 HarmonyOS 权限体系中,精准位置权限必须与模糊位置权限同时申请。

虽然 IVGuard 主要使用精准定位,但在降级场景中,模糊定位也可以提供有价值的服务:

  • 确定用户所在的大致区域(如医院某栋楼)
  • 在用户拒绝精准定位但允许模糊定位时,提供简化版导航
module.json5 配置
{
  "name": "ohos.permission.APPROXIMATELY_LOCATION",
  "reason": "$string:location_reason",
  "usedScene": {
    "abilities": ["EntryAbility"],
    "when": "inuse"
  }
}

注意reason 可与 LOCATION 共用同一个字符串资源 $string:location_reason

2.8 GET_WIFI_INFO(WiFi 信息权限)

基本信息
属性
权限名称 ohos.permission.GET_WIFI_INFO
授权方式 system_grant(系统授权)
权限级别 normal
起始 API 版本 8
用途说明

GET_WIFI_INFO 权限用于获取当前连接的 WiFi 信息,辅助室内定位。在大型医院中,WiFi 接入点(AP)的分布密度很高,每个 AP 的位置相对固定。通过获取当前连接的 WiFi BSSID 和信号强度,可以:

  1. 辅助室内定位:WiFi 指纹定位是室内导航的重要辅助手段
  2. 确定所在楼层和区域:不同楼层的 WiFi AP 信号特征不同
  3. 提升定位精度:与 GPS/北斗定位融合,弥补室内卫星信号弱的不足
重要说明:SCAN_WIFI → GET_WIFI_INFO

在早期开发中,IVGuard 曾使用 ohos.permission.SCAN_WIFI(扫描 WiFi 权限),但该权限属于 system_basic 级别,普通应用无法申请。经分析后改为 GET_WIFI_INFO,原因如下:

维度 SCAN_WIFI GET_WIFI_INFO
授权方式 system_grant system_grant
权限级别 system_basic normal
功能 主动扫描周边 WiFi 获取当前连接的 WiFi 信息
普通应用可用
IVGuard 需求 超出实际需要 满足需求

GET_WIFI_INFO 仅获取当前已连接 WiFi 的信息,不涉及主动扫描,权限级别为 normal,完全满足 IVGuard 的辅助定位需求,同时避免了跨等级权限申请的问题。

module.json5 配置
{
  "name": "ohos.permission.GET_WIFI_INFO"
}

历史教训:在 IVGuard 早期开发阶段,我们曾误用 ohos.permission.SCAN_WIFI 作为 WiFi 权限声明。这是一个不存在的权限名称,编译器不会报错,但运行时权限检查始终返回"未授权"。经过查阅 HarmonyOS 官方文档确认正确名称为 ohos.permission.GET_WIFI_INFO。这一教训深刻说明:HarmonyOS 的权限名称必须严格匹配官方文档定义,不能凭经验从 Android 体系类比推测;system_grant 类型权限安装时自动授予,若运行时检查始终返回未授权,首先应确认权限名称拼写是否正确;权限功能应在开发早期进行真机验证,而非仅在模拟器上测试。


3. module.json5 权限配置完整解析

3.1 requestPermissions 数组结构

module.json5 中的 requestPermissions 是模块级权限声明的核心配置,它是一个数组,每个元素代表一项权限的声明。IVGuard 的完整权限声明如下:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:camera_reason",
        "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
      },
      {
        "name": "ohos.permission.NFC_TAG",
        "reason": "$string:nfc_reason",
        "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
      },
      {
        "name": "ohos.permission.NOTIFICATION_CONTROLLER",
        "reason": "$string:notify_reason",
        "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
      },
      {
        "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
        "reason": "$string:bg_reason",
        "usedScene": { "abilities": ["EntryAbility"], "when": "always" }
      },
      {
        "name": "ohos.permission.VIBRATE"
      },
      {
        "name": "ohos.permission.LOCATION",
        "reason": "$string:location_reason",
        "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
      },
      {
        "name": "ohos.permission.APPROXIMATELY_LOCATION",
        "reason": "$string:location_reason",
        "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
      },
      {
        "name": "ohos.permission.GET_WIFI_INFO"
      }
    ]
  }
}

数组中权限声明的顺序遵循以下原则:核心功能权限(CAMERA)排在最前,辅助功能权限排在后面;user_grant 权限排在前面,system_grant 权限排在后面;属于同一功能模块的权限紧邻排列(LOCATION 与 APPROXIMATELY_LOCATION)。这种排序方式便于代码审查时快速理解权限的优先级和归属关系。

3.2 user_grant 必填字段详解

对于 user_grant 类型的权限,以下字段是强制必填的,缺失任何一项都将导致编译错误或运行时异常:

name 字段

name 是权限的完整标识符,必须与 HarmonyOS 预定义的权限名称完全一致。权限名称采用反向域名格式:ohos.permission.XXX。如果名称拼写错误或使用了不存在的权限名,编译器在 module.json5 解析阶段不会报错(因为配置文件是声明式的),但运行时调用 checkAccessToken()requestPermissionsFromUser() 时,系统将无法识别该权限,导致权限检查始终返回未授权状态。

IVGuard 中的 5 项 user_grant 权限名称:

  • ohos.permission.CAMERA —— 相机访问权限
  • ohos.permission.NFC_TAG —— NFC 标签读写权限
  • ohos.permission.NOTIFICATION_CONTROLLER —— 通知发布权限
  • ohos.permission.LOCATION —— 精确定位权限
  • ohos.permission.APPROXIMATELY_LOCATION —— 粗略定位权限

reason 字段

reason 是授权理由字段,必须引用字符串资源而非硬编码文本。格式为 $string:resource_name,其中 resource_name 对应 entry/src/main/resources/base/element/string.json 中定义的字符串资源。IVGuard 中定义的权限理由字符串资源如下:

资源名 中文值 对应权限
camera_reason 需要使用相机监控输液瓶液位变化 CAMERA
nfc_reason 需要读取NFC贴纸中的药物信息 NFC_TAG
notify_reason 需要在输液快结束时发送提醒通知 NOTIFICATION_CONTROLLER
bg_reason 需要在后台持续监控输液进度 KEEP_BACKGROUND_RUNNING
location_reason 需要获取位置信息用于医院室内导航 LOCATION / APPROXIMATELY_LOCATION

理由的撰写遵循以下原则:

  1. 以"需要"开头:直接说明功能的必要性,避免使用"想要"或"希望"等弱化语气的表达
  2. 具体用途说明:明确权限的具体用途而非笼统描述,例如"监控输液瓶液位变化"而非"使用相机"
  3. 字数控制:控制在 15 至 30 字之间,既简洁又不模糊
  4. 避免技术术语:面向普通用户的可理解性,不使用"NDEF"“BSSID”"AccessToken"等技术术语
  5. 共享理由:LOCATION 和 APPROXIMATELY_LOCATION 共用同一个 reason 字符串,因为它们服务于同一功能,分别说明可能造成用户困惑

usedScene 字段

usedScene 声明权限的使用场景,包含两个子字段:

  • abilities:使用该权限的 Ability 列表。IVGuard 只有 EntryAbility 一个 Ability,因此所有权限的 abilities 值均为 ["EntryAbility"]。如果应用有多个 Ability,应按实际使用情况分别列出。
  • when:权限的使用时机,取值为 "inuse""always""inuse" 表示权限仅在应用前台使用期间有效,"always" 表示前后台均可使用。

usedScene 的声明与运行时权限检查直接关联。如果声明了 when: "inuse",当应用处于后台时,系统可能自动回收该权限的保护效果,导致受保护的 API 调用失败。因此,when 的选择必须与功能的实际使用场景严格匹配。

3.3 system_grant 只需 name 的原因

system_grant 类型的权限在配置中只需要 name 字段,不需要 reasonusedScene。这并非疏忽,而是由 system_grant 的授权机制决定的:

  1. 无需用户参与system_grant 权限在应用安装时由系统自动授予,不触发用户授权弹窗,因此 reason 没有展示的场景和必要性
  2. 不受前后台限制system_grant 权限的使用不受应用前后台状态的影响,因此 when 字段没有区分的必要
  3. 使用范围由系统管理system_grant 权限保护的是系统级资源,其使用范围由系统框架自身管理,不需要应用额外声明使用场景

IVGuard 中的 3 项 system_grant 权限配置:

// KEEP_BACKGROUND_RUNNING —— 虽然是 system_grant,但我们额外配置了 reason 和 usedScene
// 原因:1) 便于代码文档化;2) 部分系统版本可能利用这些信息;3) 保持配置一致性
{
  "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
  "reason": "$string:bg_reason",
  "usedScene": { "abilities": ["EntryAbility"], "when": "always" }
}

// VIBRATE —— 纯粹的 system_grant,无需额外字段
{
  "name": "ohos.permission.VIBRATE"
}

// GET_WIFI_INFO —— 纯粹的 system_grant,无需额外字段
{
  "name": "ohos.permission.GET_WIFI_INFO"
}

值得注意的是,KEEP_BACKGROUND_RUNNING 虽然是 system_grant 类型,但我们在配置中仍然提供了 reasonusedScene。这是一种防御性编程策略:即便当前系统版本不强制要求,未来版本可能会加强 system_grant 权限的审核要求,提前配置可以避免后续适配工作。

3.4 reason 字符串资源引用:$string:xxx

reason 字段采用字符串资源引用而非硬编码文本,这是 HarmonyOS 的强制要求。资源引用的格式为 $string:resource_name,其中 resource_name 必须在 entry/src/main/resources/base/element/string.json 中有对应的定义。

使用资源引用的好处:

  1. 国际化支持:同一资源名可以对应多种语言的翻译,当用户切换系统语言时,权限理由自动切换为对应语言
  2. 集中管理:所有权限理由集中在一个配置文件中,便于统一审查和修改
  3. 编译时校验:如果引用了不存在的资源名,编译时会报错,避免运行时显示空白理由
  4. 格式规范:系统框架对资源引用有统一的解析和展示逻辑,确保理由在各种设备上显示一致

IVGuard 的字符串资源文件 string.json 中,权限相关的资源定义占 5 项,每项都经过精心措辞,确保用户能够理解权限的必要性。

3.5 usedScene.when: “inuse” vs “always” 的选择原则

when 字段是权限配置中最需要审慎决策的字段。它直接决定了权限在前台和后台的有效性范围,影响用户隐私保护的强度和应用功能的可靠性。

选择 “inuse” 的情况(默认选择)

"inuse" 应当作为默认选择,适用于绝大多数权限。选择条件:

  • 权限仅在应用前台使用时需要
  • 功能不涉及后台运行
  • 隐私敏感度较高,用户期望其使用范围受限于前台

IVGuard 中 7 项权限使用 when: "inuse":CAMERA、NFC_TAG、NOTIFICATION_CONTROLLER、VIBRATE(实际无 when 字段,等效 inuse)、LOCATION、APPROXIMATELY_LOCATION、GET_WIFI_INFO(实际无 when 字段,等效 inuse)。

选择 “always” 的情况(需要额外论证)

"always" 仅在满足以下所有条件时方可选择:

  1. 有明确的后台运行需求,且该需求是功能核心而非可选增强
  2. 已论证 "inuse" 无法满足功能需求,后台场景不可或缺
  3. 已评估对电池续航和系统资源的影响,并设计了补偿措施
  4. 已设计后台任务的生命周期管理机制,避免无限制的后台运行

IVGuard 中仅 1 项权限使用 when: "always"KEEP_BACKGROUND_RUNNING。论证如下:输液监控功能的核心是每 3 秒一次的液位采样,监控过程持续 30 分钟到数小时。在此期间,用户不可避免地会切换应用(查看微信、接听电话等),如果使用 "inuse",应用进入后台后系统会暂停或终止进程,导致监控中断。输液监控的中断可能导致预警遗漏,直接影响患者安全。因此,"always" 是此权限唯一合理的选择。


4. 运行时权限申请完整实现

4.1 abilityAccessCtrl.createAtManager()

abilityAccessCtrl 模块是 HarmonyOS 权限管理的核心模块,createAtManager() 是获取权限管理器实例的入口方法。AtManager 实例提供了权限检查、权限申请、权限标志查询等核心能力。

import { abilityAccessCtrl } from '@kit.AbilityKit'

const atManager = abilityAccessCtrl.createAtManager()

AtManager 实例提供的核心方法如下:

方法 用途 参数 返回值
checkAccessToken(tokenID, permission) 检查指定 token 是否拥有某权限 tokenID: number, permission: string Promise<GrantStatus>
requestPermissionsFromUser(context, permissions) 向用户请求权限授权 context: UIAbilityContext, permissions: string[] Promise<PermissionRequestResult>
getPermissionFlags(tokenID, permission) 获取权限标志位 tokenID: number, permission: string Promise<number>

createAtManager() 可以在应用的任何位置调用,不需要特定上下文。但 requestPermissionsFromUser() 需要一个有效的 UIAbilityContext,这意味着权限申请必须在 Ability 的生命周期内进行。

4.2 requestPermissionsFromUser 调用

requestPermissionsFromUser 是运行时权限申请的核心方法。它触发系统授权弹窗,让用户决定是否授予权限。

import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'

async function requestPermissions(
  context: common.UIAbilityContext,
  permissionList: Permissions[]
): Promise<abilityAccessCtrl.PermissionRequestResult> {
  const atManager = abilityAccessCtrl.createAtManager()
  try {
    const result = await atManager.requestPermissionsFromUser(context, permissionList)
    return result
  } catch (e) {
    hilog.error(0x0000, 'IVGuard', `Permission request error: ${(e as Error).message}`)
    throw e
  }
}

调用 requestPermissionsFromUser 的关键注意事项:

  1. context 必须是 UIAbilityContext:不能使用 ApplicationContextExtensionContext,因为权限弹窗需要与 UI 窗口关联
  2. 权限名称必须与声明一致permissions 数组中的权限名称必须与 module.json5 中声明的一致,否则系统无法识别
  3. 单次不超过 3 个:建议单次请求不超过 3 个权限,避免弹窗信息过于复杂导致用户直接全部拒绝
  4. 已授权的权限自动跳过:如果请求的权限已经被授予,系统不会重复弹窗,而是直接在结果中返回已授权状态
  5. 不能在 onCreate 或 onWindowStageCreate 中调用:此时 UI 尚未准备好,调用会抛出异常

4.3 PermissionRequestResult 解析

requestPermissionsFromUser 的返回值是 PermissionRequestResult 对象,包含以下字段:

interface PermissionRequestResult {
  authResults: number[]   // 每个权限的授权结果码
  permissions: string[]   // 请求的权限列表(与传入参数对应)
}

authResults 数组含义

authResults 数组的每个元素对应 permissions 数组中同一位置的权限的授权结果,取值含义如下:

常量名 含义 处理策略
0 PERMISSION_GRANTED 用户同意授权 正常使用功能
-1 PERMISSION_DENIED 用户拒绝授权 进入降级模式
2 PERMISSION_DENIED_DO_NOT_ASK_AGAIN 用户拒绝且选择不再询问 引导用户前往系统设置手动开启

典型解析代码:

function parsePermissionResult(
  result: abilityAccessCtrl.PermissionRequestResult
): Record<string, number> {
  const statusMap: Record<string, number> = {}
  for (let i = 0; i < result.permissions.length; i++) {
    const perm = result.permissions[i]
    const status = result.authResults[i]
    statusMap[perm] = status
  }
  return statusMap
}

在实际应用中,我们需要根据不同的授权结果采取不同的策略。对于 PERMISSION_GRANTED,正常启动功能;对于 PERMISSION_DENIED,进入降级模式并提供替代方案;对于 PERMISSION_DENIED_DO_NOT_ASK_AGAIN,需要引导用户前往系统设置页面手动开启权限,因为再次调用 requestPermissionsFromUser 不会弹窗。

4.4 申请时机的最佳实践

IVGuard 采用"按需申请"策略,权限申请时机与功能入口严格对齐。下表展示了每项 user_grant 权限的申请时机:

权限 申请时机 触发页面 用户场景 申请前是否预引导
CAMERA 用户首次进入 MonitorPage 监控页 护士/患者开始监控输液
NFC_TAG 用户点击"NFC配对"按钮 药物详情页 护士配对药物信息 否(按钮已暗示意图)
NOTIFICATION_CONTROLLER 用户首次启动监控 监控页 监控启动前确保预警通道
LOCATION + APPROXIMATELY_LOCATION 用户首次进入 HospitalNavPage 导航页 患者找护士站

"按需申请"策略的核心优势:

  1. 功能关联性强:用户在需要使用功能时才看到权限请求,能将权限与功能直接关联,授权率显著高于启动时集中申请
  2. 避免用户反感:不会在应用启动时弹出大量授权窗口导致用户反感甚至直接卸载
  3. 按需授权:不会提前申请用户暂不需要的权限,减少不必要的权限持有时间
  4. 符合设计原则:符合 HarmonyOS 权限设计的"最小知情"原则,用户只在使用功能时才被告知权限需求

4.5 封装权限检查工具方法

为了在应用各处方便地检查和申请权限,IVGuard 封装了统一的权限工具类 PermissionUtil,提供权限检查、权限申请、检查并申请等便捷方法:

import { abilityAccessCtrl, bundleManager, common, Permissions } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'

export class PermissionUtil {
  private static atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager()

  static async checkPermission(
    context: common.UIAbilityContext,
    permission: Permissions
  ): Promise<boolean> {
    try {
      const bundleInfo = await bundleManager.getBundleInfoForSelf(
        bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
      )
      const tokenID = bundleInfo.appInfo.accessTokenId
      const status = await PermissionUtil.atManager.checkAccessToken(tokenID, permission)
      return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
    } catch (e) {
      hilog.error(0x0000, 'IVGuard', `Check permission failed: ${(e as Error).message}`)
      return false
    }
  }

  static async requestPermission(
    context: common.UIAbilityContext,
    permission: Permissions
  ): Promise<boolean> {
    try {
      const result = await PermissionUtil.atManager.requestPermissionsFromUser(
        context, [permission]
      )
      if (result.authResults.length > 0) {
        return result.authResults[0] === 0
      }
      return false
    } catch (e) {
      hilog.error(0x0000, 'IVGuard', `Request permission failed: ${(e as Error).message}`)
      return false
    }
  }

  static async checkAndRequest(
    context: common.UIAbilityContext,
    permission: Permissions
  ): Promise<boolean> {
    const alreadyGranted = await PermissionUtil.checkPermission(context, permission)
    if (alreadyGranted) {
      return true
    }
    return await PermissionUtil.requestPermission(context, permission)
  }

  static async checkMultiple(
    context: common.UIAbilityContext,
    permissions: Permissions[]
  ): Promise<Record<string, boolean>> {
    const results: Record<string, boolean> = {}
    for (let i = 0; i < permissions.length; i++) {
      results[permissions[i]] = await PermissionUtil.checkPermission(context, permissions[i])
    }
    return results
  }
}

使用示例——在 MonitorPage 中检查并申请相机权限:

import { PermissionUtil } from '../utils/PermissionUtil'

aboutToAppear(): void {
  const context = getContext(this) as common.UIAbilityContext
  PermissionUtil.checkAndRequest(context, 'ohos.permission.CAMERA').then((granted) => {
    if (!granted) {
      this.showManualInput = true
      hilog.warn(0x0000, 'IVGuard', 'Camera permission denied, using manual mode')
    }
    this.startMonitoring()
  })
}

PermissionUtil 的设计遵循以下原则:

  1. 单一职责:每个方法只做一件事(检查或申请),组合方法(checkAndRequest)通过调用基础方法实现
  2. 容错优先:所有异常都被捕获并记录日志,不会因权限检查失败而崩溃
  3. 默认拒绝:当无法确定权限状态时,默认返回 false(未授权),确保功能进入安全的降级模式
  4. 无状态设计:工具类不维护权限状态的缓存,每次检查都实时查询系统,避免状态不同步

5. 优雅降级策略(每种权限详细分析)

权限降级是 IVGuard 容错设计的核心组成部分。在医疗场景下,应用的可用性直接关系到患者的安全与体验。因此,每一项权限的缺失都必须有对应的降级方案,确保应用在部分权限不可用时仍能提供有价值的服务。降级设计的核心原则是:功能可以受限,但不可缺失;体验可以降级,但不可中断。

5.1 无 CAMERA:手动输入液位、降级 UI 设计

降级场景分析

相机权限被拒绝是 IVGuard 最严重的降级场景,因为自动液位检测是应用的核心价值。VisionService 无法获取摄像头画面,自动检测功能完全不可用。但降级并不意味着功能完全丧失——手动液位输入模式仍然能够提供基本的监控和预警能力。

降级 UI 设计

MonitorPage 在无相机权限时的 UI 变化如下:

  1. 相机预览区域:替换为灰色占位区域,中央显示相机图标和"相机权限未开启"提示文字,下方显示"前往设置"按钮,点击后跳转系统应用设置页
  2. 手动液位输入:预览区域下方增加液位滑块控件和数字输入框,用户可以拖动滑块或直接输入数字来报告当前液位
  3. 液位数字显示:从自动更新变为手动更新模式,数字颜色从蓝色变为橙色以区分模式
  4. 流速显示:显示"手动模式下无法自动检测流速"提示,流速输入框可选填
  5. 模式标识:页面顶部显示"手动模式"标签,橙色背景,持续提醒用户当前非自动监控

降级 UI 的核心设计原则:

  • 视觉区分:自动模式和手动模式使用不同的颜色标识(蓝色 vs 橙色),用户一眼可辨
  • 交互简化:手动输入的交互必须简单直观,滑块 + 数字输入双模式满足不同用户习惯
  • 快捷入口:提供跳转系统设置的快捷按钮,方便用户随时开启相机权限
  • 流程不阻塞:手动模式下的所有功能流程与自动模式一致,不因权限缺失而阻塞任何操作

降级代码逻辑:

private updateMonitoringMode(): void {
  const hasCamera = this.checkCameraPermission()
  if (hasCamera) {
    this.monitoringMode = 'auto'
    this.showManualInput = false
    VisionService.startDetection()
  } else {
    this.monitoringMode = 'manual'
    this.showManualInput = true
    VisionService.stopDetection()
  }
}

private onManualLevelInput(level: number): void {
  this.currentLevel = level
  this.checkAlert()
}

5.2 无 NFC:手动输入贴纸 ID、MedicineDetailPage 适配

降级场景分析

NFC 权限被拒绝或设备不支持 NFC 时,NfcService 无法读取 NFC 贴纸中的药物配对信息。这对药物配对功能有影响,但不影响药物监控的核心流程——药物信息可以通过其他方式输入。

MedicineDetailPage 适配

  1. 隐藏 NFC 按钮:将"NFC 配对"按钮设为不可见或禁用状态,避免用户点击后失败
  2. 显示贴纸 ID 输入框:增加文本输入框,支持手动输入贴纸上的编号
  3. 显示条形码/二维码扫描按钮:ScanService 提供的扫描功能作为替代方案
  4. 从药物数据库搜索:DrugDatabase 提供按名称或编码搜索药物的功能
  5. 历史记录快速选择:显示最近使用的药物列表,用户可以快速选择而不必重新输入

降级代码逻辑:

private handleNfcUnavailable(): void {
  this.showManualStickerInput = true
  this.nfcButtonVisible = false
  this.showBarcodeScanButton = true
  hilog.info(0x0000, 'IVGuard', 'NFC unavailable, showing manual input and barcode scan')
}

5.3 无 LOCATION:静态平面图无蓝点、HospitalMapView 适配

降级场景分析

定位权限被拒绝后,NavService 无法获取用户位置,HospitalMapView 无法显示"您在这里"的蓝色定位圆点。路径规划和步行时间估算功能也受影响。但地图的信息展示功能仍然可用。

HospitalMapView 适配

  1. 地图正常显示:楼层平面图和 POI 标记正常渲染
  2. 移除蓝色定位圆点:不显示用户当前位置标记
  3. 路径规划降级:从"显示从当前位置到目的地的路线"降级为"显示目的地位置,请自行判断方向"
  4. 步行时间不可用:显示"无法估算步行时间(缺少定位权限)"
  5. 楼层切换保留:手动选择楼层功能不受影响,用户可以通过选择楼层查看不同楼层的地图

降级后的 HospitalMapView 仍然提供以下价值:

  • 查看医院各楼层的地图布局
  • 了解护士站、输液室、急诊室的位置
  • 手动选择楼层查看对应 POI 标记
  • 了解目的地在哪个楼层哪个区域

这种降级方式保留了地图的信息展示功能,只是失去了"我在哪里"的定位能力。对于熟悉医院环境的护士而言,静态地图已经足够实用。

5.4 无 NOTIFICATION:应用内 Toast/Dialog 替代

降级场景分析

通知权限被拒绝后,NotificationService 无法通过系统通知栏发送预警,这是 IVGuard 第二严重的降级场景。在应用后台运行时,预警信息将无法通过通知栏到达用户。但应用前台运行时的替代提醒机制仍然可用。

替代方案

  1. Toast 提示:当应用在前台时,使用 Toast 显示预警信息,持续 5 秒
  2. AlertDialog 弹窗:对于高级别预警(输液完成、流速严重异常),使用 AlertDialog 确保用户注意到
  3. 页面内预警横幅:MonitorPage 顶部显示醒目的预警横幅,闪烁红色背景
  4. 振动补偿:振动权限(system_grant)不受影响,通过振动提醒用户查看应用
  5. 语音播报:SpeechService 的语音提醒功能不受通知权限影响

应用内 Toast 替代通知的实现:

private showAlertFallback(title: string, message: string): void {
  this.getUIContext().getPromptAction().showToast({
    message: `${title}: ${message}`,
    duration: 5000
  })
}

重要提醒:Toast 和 Dialog 仅在应用前台时有效。如果应用在后台,这些提醒无法到达用户。因此,当通知权限被拒绝时,IVGuard 会在监控启动时额外提醒用户"建议保持应用在前台运行,或开启通知权限以确保预警及时送达"。

5.5 无 KEEP_BACKGROUND_RUNNING:仅前台监控、锁屏暂停提示

降级场景分析

后台运行权限不可用时(极端情况下系统可能回收此权限),应用切换到后台后系统会在数分钟内暂停应用进程,3 秒采样中断,监控数据出现空白期。用户锁屏时也会导致同样的问题。

降级策略

  1. 仅前台监控:监控仅在应用前台时有效,切换应用或锁屏时暂停
  2. 锁屏提示:用户锁屏时弹出提示对话框"锁屏后监控将暂停,建议保持屏幕常亮或开启后台运行权限"
  3. 常驻通知提示:如果有通知权限,显示"IVGuard 监控中,点击返回"的通知,引导用户返回前台
  4. 保持屏幕常亮选项:SettingsPage 增加"监控时保持屏幕常亮"选项,防止锁屏中断监控
  5. 暂停恢复机制:应用重新回到前台时,自动恢复监控并补充记录暂停期间的时间段

5.6 降级 UI 的统一设计模式

IVGuard 的降级 UI 遵循统一的设计模式,确保不同降级场景下的一致用户体验:

降级提示组件

所有权限降级场景共用同一套提示组件,包含以下元素:

  1. 图标:使用对应功能的灰色图标表示功能不可用
  2. 标题:简洁描述缺失的功能,如"相机权限未开启"“定位服务不可用”
  3. 说明:解释降级后的功能变化和影响
  4. 操作按钮:提供"前往设置"(跳转系统权限设置)和"继续使用"(接受降级模式)两个按钮
  5. 降级标识:使用橙色标签标识当前处于降级模式

降级状态管理

IVGuard 使用统一的状态管理机制追踪各权限的授权状态:

export class PermissionState {
  cameraGranted: boolean = false
  nfcGranted: boolean = false
  notificationGranted: boolean = false
  locationGranted: boolean = false
  approxLocationGranted: boolean = false
  backgroundRunningGranted: boolean = true  // system_grant 默认授予
  vibrateGranted: boolean = true             // system_grant 默认授予
  wifiInfoGranted: boolean = true            // system_grant 默认授予

  static async checkAll(context: common.UIAbilityContext): Promise<PermissionState> {
    const state = new PermissionState()
    state.cameraGranted = await PermissionUtil.checkPermission(context, 'ohos.permission.CAMERA')
    state.nfcGranted = await PermissionUtil.checkPermission(context, 'ohos.permission.NFC_TAG')
    state.notificationGranted = await NotificationService.isNotificationEnabled()
    state.locationGranted = await PermissionUtil.checkPermission(context, 'ohos.permission.LOCATION')
    state.approxLocationGranted = await PermissionUtil.checkPermission(
      context, 'ohos.permission.APPROXIMATELY_LOCATION'
    )
    return state
  }
}

每次页面进入前台时,都会重新检查权限状态,确保降级策略与实际权限状态同步。这种"不信任缓存"的设计避免了权限状态不同步导致的 UI 错误。


6. 数据安全深度分析

6.1 Preferences 本地存储安全机制:应用沙箱隔离

IVGuard 当前使用 HarmonyOS 的 Preferences API 作为本地存储方案。Preferences 是一种轻量级的键值对存储,适合存储应用的配置信息和少量业务数据。IVGuard 的 DataStore 类封装了所有 Preferences 操作。

应用沙箱隔离

HarmonyOS 的应用沙箱机制是数据安全的第一道防线。每个应用运行在独立的沙箱中,拥有自己的文件系统空间和进程空间。具体而言:

  1. 文件隔离:每个应用只能访问自己沙箱内的文件,无法直接读取其他应用的文件
  2. 数据库隔离:Preferences 数据库存储在应用沙箱的私有目录中,其他应用无法访问
  3. 内存隔离:应用进程的内存空间是独立的,其他进程无法直接读取
  4. 网络隔离:应用的网络请求通过系统代理,系统可以实施网络策略控制

Preferences 文件的实际存储路径类似于 /data/app/el2/100/base/{bundleName}/preferences/{name}.xml,其中 el2 表示加密存储等级(Encryption Level 2),设备锁屏后数据会被加密,解锁后才能访问。

IVGuard 的 DataStore 初始化代码:

import { preferences } from '@kit.ArkData'
import { common } from '@kit.AbilityKit'

export class DataStore {
  private static store: preferences.Preferences | undefined = undefined

  static async init(context: common.UIAbilityContext): Promise<void> {
    if (DataStore.store !== undefined) {
      return
    }
    DataStore.store = preferences.getPreferencesSync(context, { name: 'ivguard_data' })
  }
}

6.2 医疗数据分类与脱敏策略

IVGuard 处理的数据涉及医疗场景,需要进行严格的数据分类和脱敏处理。

数据分类

IVGuard 中的数据按敏感程度分为三个等级:

等级 数据类型 示例 存储策略 展示策略
高敏感 患者身份信息 患者姓名、床号 本地加密存储 部分脱敏显示
中敏感 医疗过程数据 液位记录、流速数据 本地存储 完整显示
低敏感 应用配置数据 设置项、阈值配置 本地存储 完整显示

脱敏策略

对于患者姓名等高敏感数据,IVGuard 采用以下脱敏规则:

  1. 姓名脱敏:两个字的姓名显示为"张*“,三个字及以上显示为"张*三”
  2. 床号保留:床号不脱敏,因为它是定位信息而非隐私信息
  3. 药物名称保留:药物名称不脱敏,因为其不含个人隐私
  4. 预警通知脱敏:通知栏消息中使用脱敏姓名,避免锁屏时暴露完整姓名

6.3 不存储的敏感信息

IVGuard 明确不存储以下类别的敏感信息:

  1. 身份证号码:应用功能不需要身份证号,绝不收集和存储
  2. 银行卡号:费用估算功能仅使用参考价格,不涉及真实支付,无需银行卡信息
  3. 医保卡号:保险设置仅保存报销比例等参数,不保存医保卡号
  4. 联系方式:不收集和存储电话号码、邮箱地址等联系方式
  5. 精确位置历史:不存储定位轨迹,仅在使用导航功能时临时获取位置
  6. 相机影像数据:不保存任何照片或视频帧,相机仅用于实时检测

这一"不存储"策略是最小数据原则的极致体现:如果数据不是功能所必需的,就根本不要收集。收集了不必要的数据不仅增加安全风险,还违反多项隐私法规。

6.4 最小数据原则的贯彻

最小数据原则要求应用只收集和存储完成功能所必需的最少数据。IVGuard 从以下方面贯彻这一原则:

  1. 数据字段最小化:每个数据模型只包含功能必需的字段。例如,Medicine 模型不包含患者信息,MonitorSession 模型不包含药物详细属性,各模型职责清晰、数据不冗余
  2. 数据保留期限:监控记录保留最近 30 天,超过 30 天的记录在应用启动时自动清理
  3. 数据聚合存储:液位记录按会话聚合存储,不保存原始图像帧数据
  4. 默认值策略:新增数据时,非必要字段使用默认值而非要求用户填写

6.5 数据清除机制

IVGuard 的数据清除机制涵盖以下场景:

  1. 卸载自动清除:应用卸载时,沙箱内的所有数据(包括 Preferences 数据库)自动被系统清除,无需应用额外处理
  2. 手动清除:SettingsPage 提供"清除应用数据"选项,用户可以手动清除所有本地数据
  3. 过期自动清除:DataStore 在初始化时检查数据的时间戳,自动清除超过保留期限的历史记录
  4. 会话结束清除:监控会话结束后,临时数据(如实时帧数据)立即释放

6.6 未来加密方案:AES-256

当前版本 IVGuard 使用 HarmonyOS 沙箱隔离作为数据安全的基础保障。未来版本计划引入 AES-256 加密方案,对高敏感数据进行应用层加密存储:

  1. 加密范围:患者姓名、监控记录等高敏感数据在写入 Preferences 前进行 AES-256 加密
  2. 密钥管理:使用 HarmonyOS 的 HUKS(Universal KeyStore)服务管理加密密钥,密钥存储在安全硬件中
  3. 透明加密:DataStore 层面实现透明加密/解密,上层业务代码无需感知加密逻辑
  4. 密钥轮换:支持定期轮换加密密钥,增强长期安全性
  5. 迁移方案:从明文存储到加密存储的迁移,提供自动迁移工具

7. 最小权限原则实践

7.1 8 项权限每一项的必要性论证

最小权限原则不仅是一种设计理念,更需要可追溯的论证记录。以下对 IVGuard 的 8 项权限逐一论证其必要性:

ohos.permission.CAMERA(必要性等级:极高)

相机权限是自动液位检测的基础。没有相机权限,VisionService 无法获取摄像头画面,无法识别贴纸标记,无法推算液位。这是 IVGuard 与传统输液监控方式的根本区别——正是相机权限使得"智能"监控成为可能。替代方案(手动输入)虽然可用,但依赖人工操作,存在延迟和遗漏风险,无法达到智能监控的实时性和可靠性。

ohos.permission.NFC_TAG(必要性等级:高)

NFC 权限用于快速、准确地完成药物配对。在医院环境中,NFC 贴纸的非接触式读取特性显著提高了配药效率,减少了人工输入的错误率。替代方案(手动输入、条码扫描)虽然可行,但操作步骤更多、出错概率更高。NFC 权限的存在使得药物配对流程从"输入-确认"简化为"靠近-读取",提升了护士的工作效率。

ohos.permission.NOTIFICATION_CONTROLLER(必要性等级:极高)

通知权限是预警信息送达的关键通道。输液预警的核心价值在于"及时提醒"——如果预警信息无法及时到达护士或患者,预警就失去了意义。替代方案(Toast、Dialog)仅在应用前台有效,而输液监控期间用户很可能切换到其他应用。没有通知权限,预警的实时性和可靠性将大打折扣。

ohos.permission.KEEP_BACKGROUND_RUNNING(必要性等级:极高)

后台运行权限确保监控过程不因应用切换到后台而中断。输液监控需要持续 30 分钟到数小时,在此期间用户必然需要使用手机的其他功能。没有后台运行权限,应用进入后台数分钟后就会被系统暂停,监控中断,预警遗漏。对于医疗监控应用而言,监控中断是不可接受的风险。

ohos.permission.VIBRATE(必要性等级:中)

振动权限提供了病房环境下最合适的预警方式。在安静的病房中,铃声提醒会打扰其他患者,而振动提醒仅持有人可感知,既达到了提醒效果又不影响他人。振动权限作为 system_grant 类型,不会增加用户的授权负担。替代方案(仅依赖通知)在手机静音时可能被忽略。

ohos.permission.LOCATION(必要性等级:中)

精确定位权限用于医院室内导航的精确路线规划。对于不熟悉医院环境的患者和家属,精确导航可以帮助他们快速找到护士站、输液室等关键位置。替代方案(粗略定位 + 静态地图)虽然可以提供基本的方向指引,但精度不足,可能导致导航路线不准确。

ohos.permission.APPROXIMATELY_LOCATION(必要性等级:中)

粗略定位权限作为精确定位的降级方案,在用户拒绝精确定位时提供基本的位置服务。粗略定位足够判断用户所在的楼层和大致区域,配合 WiFi 指纹匹配可以实现可用的楼层定位。与 LOCATION 权限同属一个权限组,同时声明可以提供更灵活的授权选择。

ohos.permission.GET_WIFI_INFO(必要性等级:低-中)

WiFi 信息权限用于辅助室内定位的楼层判断。WiFi BSSID 与楼层映射表的匹配可以快速确定用户所在楼层,是定位系统的补充手段。替代方案(手动选择楼层)虽然可行,但增加了用户操作步骤。WiFi 权限作为 system_grant 类型,不增加用户授权负担,且信息敏感度低。

7.2 inuse vs always:只有 KEEP_BACKGROUND_RUNNING 用 always

IVGuard 的 8 项权限中,7 项使用 when: "inuse",仅 KEEP_BACKGROUND_RUNNING 使用 when: "always"。这一决策的深层考量如下:

when: "inuse" 意味着权限仅在应用前台运行时有效。对于相机、NFC、定位等敏感权限,用户期望它们只在自己主动使用应用时才被调用。“inuse” 保护了用户的知情权——当应用不在前台时,这些权限自动失效,无法被(恶意)利用。

KEEP_BACKGROUND_RUNNING 是唯一的例外,因为它的核心功能就是在应用不在前台时保持运行。使用 “inuse” 会使此权限失去意义——如果后台运行权限仅在应用前台时有效,那就等同于没有后台运行权限。

这种"7:1"的比例(7 项 inuse vs 1 项 always)充分体现了 IVGuard 对最小权限原则的尊重:仅在有不可替代的功能需求时才使用更宽泛的权限范围。

7.3 权限回收:功能关闭后不再使用

IVGuard 在功能停止使用后,会主动释放相关资源并停止使用权限保护的功能:

  1. 监控结束:调用 VisionService.stopDetection() 停止相机检测,调用 backgroundTaskManager.stopBackgroundRunning() 释放后台任务
  2. 离开导航页面:停止位置更新请求,释放定位资源
  3. NFC 读取完成:关闭 NFC 标签会话,释放 NFC 资源
  4. 应用退到后台(非监控状态):不持有任何 active 的权限使用

权限的"回收"不是指撤销已授予的权限(这需要用户手动操作),而是指应用不再调用受该权限保护的 API。这种主动释放的行为减少了权限的"暴露时间",降低了潜在的安全风险。

7.4 未申请的权限分析

IVGuard 没有申请以下常见权限,每项都有明确的理由:

未申请权限 常见用途 不申请理由
ohos.permission.MICROPHONE 语音输入 IVGuard 不需要语音输入功能,语音播报通过媒体播放实现
ohos.permission.READ_CONTACTS 读取通讯录 不需要读取用户通讯录,联系人信息手动输入
ohos.permission.CALL_PHONE 拨打电话 使用导航到护士站替代拨打电话,避免受限权限
ohos.permission.SEND_MESSAGES 发送短信 通知权限已满足预警需求,短信属于受限权限
ohos.permission.READ_CALENDAR 读取日历 不需要与日历集成
ohos.permission.WRITE_CALENDAR 写入日历 同上
ohos.permission.ANSWER_CALL 接听电话 不涉及电话功能
ohos.permission.NOTIFICATION_AGENT_CONTROLLER 代理通知 不需要代理其他应用的通知

7.5 权限审计清单

IVGuard 建议在每个版本发布前,对权限使用情况进行审计。审计清单如下:

  1. 所有 module.json5 中声明的权限是否都有对应的功能支撑?
  2. 每项 user_grant 权限的 reason 是否准确描述了当前版本的用途?
  3. usedScene.when 的选择是否仍然合理?
  4. 是否有新功能引入了新的权限需求但未在 module.json5 中声明?
  5. 是否有已废弃的功能对应的权限可以移除?
  6. 运行时权限申请的时机是否与功能入口对齐?
  7. 每项权限的降级方案是否仍然有效且经过测试?
  8. 权限相关的日志是否过滤了敏感信息?

8. 用户隐私保护设计

8.1 权限申请时机:功能使用前而非 App 启动

IVGuard 坚决不在应用启动时集中请求所有权限。这种做法虽然简化了开发,但严重损害了用户体验和信任。我们采用"功能驱动"的权限申请策略:

启动时不申请任何权限:用户首次打开 IVGuard 时,不会看到任何权限弹窗。应用直接展示首页,用户可以浏览基本功能和信息。

功能入口触发申请:只有当用户主动使用需要权限的功能时,才触发对应的权限申请流程。这种"按需申请"的策略使得每个权限请求都有明确的功能上下文,用户能够理解权限与功能的关联。

预引导弹窗:在系统权限弹窗之前,IVGuard 会先展示一个应用内的解释弹窗,包含以下内容:

  1. 为什么需要这个权限(具体的功能说明)
  2. 权限将如何使用(技术细节的通俗解释)
  3. 不授权会有什么影响(功能降级的具体描述)
  4. 用户可以随时在设置中关闭权限(控制权说明)

8.2 拒绝后的引导:跳转系统设置

当用户拒绝 user_grant 权限后,IVGuard 不会反复弹窗骚扰,而是提供一次性的引导,帮助用户前往系统设置手动开启权限:

private guideToSettings(): void {
  const context = getContext(this) as common.UIAbilityContext
  const want: Want = {
    action: 'action.settings.application.info',
    parameters: {
      settingsParamBundleName: context.abilityInfo.bundleName
    }
  }
  context.startAbility(want)
}

引导话术的设计:不使用命令式语气(如"必须开启"),而使用建议式语气(如"建议开启以获得最佳体验")。同时明确告知用户即使不开启权限,应用仍然可以正常使用(降级模式),不会因权限缺失而无法使用。

8.3 透明度:SettingsPage 展示权限使用说明

IVGuard 的 SettingsPage 提供了"权限管理"区域,清晰展示每项权限的使用说明和当前状态。这一设计的目的是让用户在应用内就能了解权限的全貌,无需跳转到系统设置查看。

权限说明区域包含以下信息:

  1. 权限名称(使用用户可理解的中文描述,如"相机"而非"ohos.permission.CAMERA")
  2. 权限用途(与 module.json5 中的 reason 一致)
  3. 当前授权状态(已授权 / 未授权 / 不可用)
  4. 跳转系统设置的快捷按钮

8.4 可控性:用户可随时关闭任何权限

IVGuard 确保用户拥有对权限的完全控制权:

  1. 用户可以在系统设置中随时关闭任何权限,应用不会阻止或干扰
  2. 权限关闭后,应用立即切换到对应的降级模式,不会弹出强制授权的要求
  3. SettingsPage 中的权限说明区域提供"前往系统设置"的快捷入口
  4. 每次页面进入前台时,重新检查权限状态,确保降级策略与权限状态同步

8.5 隐私政策草稿

以下是 IVGuard 隐私政策的框架草稿:

IVGuard 隐私政策

生效日期:2026年8月3日

一、信息收集
本应用收集以下信息:
1. 您输入的患者姓名和床号(用于监控标识)
2. 药物名称和配药信息(用于配伍检查)
3. 输液监控数据(液位、流速等)
4. 应用设置偏好

本应用不收集以下信息:
1. 身份证号码
2. 银行卡号
3. 医保卡号
4. 联系方式
5. 精确位置历史轨迹

二、信息存储
所有数据存储在您的设备本地,采用 HarmonyOS 应用沙箱隔离保护。
本应用不上传任何数据到远程服务器。

三、信息使用
收集的信息仅用于:
1. 输液监控和预警
2. 药物配伍检查
3. 费用估算
4. 室内导航

四、信息共享
本应用不与任何第三方共享您的数据。

五、信息删除
1. 您可以在应用设置中手动清除所有数据
2. 卸载应用后,所有数据将被自动清除

六、权限使用
本应用申请的权限及其用途如下:
[列表与 module.json5 中声明的权限一致]

七、儿童隐私
本应用不面向 14 岁以下儿童使用。

8.6 GDPR / 个人信息保护法合规

IVGuard 的设计遵循以下隐私法规的核心要求:

《中华人民共和国个人信息保护法》合规

  1. 告知同意:权限申请前告知用途,获取用户明确同意(对应 user_grant 机制)
  2. 最小必要:仅收集功能必需的最少信息(对应最小权限原则)
  3. 安全保障:采用沙箱隔离保护存储数据(对应 HarmonyOS 安全机制)
  4. 删除权:用户可以随时清除数据(对应 DataStore 清除功能)
  5. 透明度:隐私政策公开透明(对应 SettingsPage 权限说明)

GDPR 合规考量

虽然 IVGuard 当前仅面向中国市场,但设计中已预留 GDPR 合规的扩展空间:

  1. 数据最小化(Data Minimization):与最小必要原则一致
  2. 目的限制(Purpose Limitation):权限用途与声明一致,不作超范围使用
  3. 存储限制(Storage Limitation):数据保留期限明确,过期自动清除
  4. 完整性保密性(Integrity and Confidentiality):沙箱隔离 + 未来 AES-256 加密

9. 安全编码实践

9.1 输入验证

IVGuard 对所有用户输入进行严格的验证,防止注入攻击和数据污染:

药物名称验证

static validateMedicineName(name: string): boolean {
  if (name.length === 0 || name.length > 50) {
    return false
  }
  const pattern = new RegExp('^[\\u4e00-\\u9fa5a-zA-Z0-9\\-\\(\\)\\s]+$')
  return pattern.test(name)
}

验证规则:长度 1-50 字符,只允许中文、英文、数字、连字符、括号和空格。

金额验证

static validateAmount(amount: string): boolean {
  const num = parseFloat(amount)
  if (isNaN(num) || num < 0 || num > 999999) {
    return false
  }
  return true
}

验证规则:必须为有效数字,范围 0-999999,防止负数和溢出。

患者姓名验证

static validatePatientName(name: string): boolean {
  if (name.length === 0 || name.length > 20) {
    return false
  }
  const pattern = new RegExp('^[\\u4e00-\\u9fa5a-zA-Z\\s·]+$')
  return pattern.test(name)
}

验证规则:长度 1-20 字符,只允许中文、英文、空格和间隔号(·)。

9.2 SQL 注入防护(未来 RDB)

当前 IVGuard 使用 Preferences 存储,不存在 SQL 注入风险。但未来迁移到关系型数据库(RDB)时,必须采取以下防护措施:

  1. 参数化查询:所有数据库查询使用参数化方式,不拼接 SQL 字符串
  2. 输入转义:对用户输入的特殊字符进行转义处理
  3. 最小权限:数据库连接使用最小权限账户,禁止 DDL 操作
  4. ORM 封装:通过 DataStore 层封装所有数据库操作,上层代码不直接执行 SQL

9.3 敏感日志过滤

IVGuard 的日志输出遵循以下安全规则:

  1. 患者姓名脱敏:日志中使用"患者*"替代真实姓名
  2. 不记录敏感数据:身份证号、银行卡号等敏感数据不写入日志
  3. 使用 hilog 分级:开发阶段使用 DEBUG 级别,发布版本使用 INFO 及以上级别
  4. 日志域隔离:IVGuard 使用独立的日志域(0x0000),便于过滤和审计
hilog.info(0x0000, 'IVGuard', `Level alert triggered for patient ${this.maskName(patientName)}`)

private maskName(name: string): string {
  if (name.length <= 2) {
    return name.charAt(0) + '*'
  }
  return name.charAt(0) + '*' + name.charAt(name.length - 1)
}

9.4 网络安全(未来服务端)

当前 IVGuard 是纯本地应用,不涉及网络通信。未来版本接入服务端时,将采取以下安全措施:

  1. HTTPS 强制:所有网络请求使用 HTTPS,禁止 HTTP 明文传输
  2. 证书校验:启用 SSL 证书校验,防止中间人攻击
  3. 请求签名:API 请求使用 HMAC 签名,防止篡改
  4. 令牌认证:使用 OAuth 2.0 或 JWT 进行身份认证
  5. 数据加密:敏感数据在传输前进行端到端加密

9.5 代码混淆(Release 构建)

IVGuard 在 Release 构建时启用代码混淆,保护应用的源代码不被逆向分析:

  1. 名称混淆:类名、方法名、变量名替换为无意义的短名称
  2. 控制流混淆:打乱代码的控制流结构,增加反编译难度
  3. 字符串加密:对字符串常量进行加密处理
  4. 调试信息移除:移除所有调试日志和断言

混淆配置在 build-profile.json5 中设置:

{
  "app": {
    "products": [
      {
        "name": "release",
        "signingConfig": "release",
        "compatibleSdkVersion": "5.0.0(12)",
        "runtimeOS": "HarmonyOS",
        "obfuscation": {
          "enable": true,
          "rules": [
            "./obfuscation-rules.txt"
          ]
        }
      }
    ]
  }
}

10. 权限相关常见问题 FAQ

Q1: 第一次授权后再次弹窗是什么原因?

现象:用户已经授权某权限,但再次进入功能页面时又弹出授权窗口。

原因分析

  1. 用户可能在系统设置中手动关闭了权限,导致已授权状态失效
  2. 系统在长时间未使用应用后自动回收了权限
  3. 应用版本更新后,权限的 tokenID 发生变化,旧的授权记录不再有效

解决方案:在调用 requestPermissionsFromUser() 之前,先通过 checkAccessToken() 检查权限是否已授予。如果已授予则直接使用,不再弹窗。

Q2: 拒绝后如何重新申请?

场景:用户之前拒绝了相机权限,现在想重新开启。

方案

  1. 如果用户仅选择"拒绝"(未选择"不再询问"),再次调用 requestPermissionsFromUser() 仍然会弹窗
  2. 如果用户选择了"不再询问",requestPermissionsFromUser() 不会再弹窗,需要引导用户前往系统设置手动开启
  3. IVGuard 在降级 UI 中提供"前往设置"按钮,点击后跳转系统应用设置页面

Q3: 权限被系统回收怎么办?

现象:应用运行过程中,某个权限突然失效。

原因

  1. 系统内存压力大时,可能回收部分非关键权限以释放资源
  2. 系统更新后权限策略变更
  3. 低电量模式下系统限制部分权限

应对策略:IVGuard 在每次使用权限前都进行检查,不假设权限一直有效。如果发现权限被回收,自动切换到降级模式并提示用户。

Q4: 多设备权限同步

场景:用户在手机上授权了权限,在平板上安装同一应用后权限状态是否同步?

解答:HarmonyOS 的权限状态是设备级别的,不同设备的权限状态相互独立。用户需要在每台设备上分别授权。IVGuard 的设计已经考虑了这一点——每台设备上的权限检查和降级逻辑是独立的,不会假设其他设备的权限状态。

Q5: 权限与应用评分的关系

场景:权限数量和授权率是否影响应用在应用商店的评分?

解答

  1. 应用商店的审核机制会对权限合理性进行评估,过多的权限申请可能导致审核不通过
  2. 用户评论中经常提及权限问题,"为什么要这么多权限"是常见的差评原因
  3. IVGuard 的 8 项权限都在合理范围内,且有清晰的功能对应关系
  4. 应用商店的推荐算法可能考虑权限与同类应用的对比,权限数显著高于同类应用可能影响曝光

Q6: 如何调试权限问题?

方法

  1. 使用 hdc 命令查看权限状态:hdc shell dumpsys permission 查看所有权限的授权状态
  2. 查看应用特定权限:hdc shell bm dump -n {bundleName} 查看应用的权限声明
  3. 使用 hilog 过滤权限相关日志:hdc hilog | grep "permission" 实时监控权限操作
  4. 在 DevEco Studio 中使用断点调试权限申请流程

Q7: 模拟器与真机的权限行为差异

差异

  1. 部分权限在模拟器上无法正常工作(如 NFC、相机),需要在真机测试
  2. 模拟器的权限弹窗样式可能与真机不同
  3. 模拟器可能自动授予某些在真机上需要用户授权的权限
  4. 后台运行权限的行为在模拟器和真机上可能有差异

建议:权限功能的最终验证必须在真机上进行,模拟器仅用于初步开发调试。

Q8: 如何处理权限申请异常?

场景:调用 requestPermissionsFromUser() 抛出异常。

可能原因

  1. 传入的 context 不是 UIAbilityContext
  2. 在 onCreate 或 onWindowStageCreate 中调用(UI 未准备好)
  3. 权限名称与 module.json5 声明不一致
  4. 系统服务异常

处理方式

try {
  const result = await atManager.requestPermissionsFromUser(context, permissions)
  // 处理结果
} catch (e) {
  const err = e as BusinessError
  hilog.error(0x0000, 'IVGuard', `Permission request failed: ${err.code} - ${err.message}`)
  // 进入降级模式
  this.enterDegradedMode()
}

Q9: 应用更新后权限会丢失吗?

解答

  1. 一般的应用更新(覆盖安装)不会影响已授予的权限
  2. 如果更新中新增了权限声明,新增的 user_grant 权限需要用户重新授权
  3. 如果更新中移除了某项权限声明,对应的授权记录会被自动清除
  4. 系统签名变更可能导致权限状态重置(极少见)

Q10: 如何在代码中区分"从未请求"和"拒绝后不再询问"?

方法
HarmonyOS 目前没有提供直接区分这两种状态的 API。变通方法:

  1. 在 SharedPreferences 中记录权限请求的历史状态
  2. 如果 checkAccessToken() 返回未授权,且本地记录显示曾经请求过,则可以推断用户选择了拒绝
  3. 如果 requestPermissionsFromUser() 返回结果中 authResults[i] === 2,表示用户选择了"不再询问"
  4. 根据返回值决定是再次弹窗还是直接引导至设置页面

附录

附录 A:权限速查表

# 权限名称 授权类型 when 核心功能 降级方案
1 CAMERA user_grant inuse 液位自动检测 手动输入液位
2 NFC_TAG user_grant inuse 药物NFC配对 手动输入/扫码
3 NOTIFICATION_CONTROLLER user_grant inuse 预警通知推送 Toast/Dialog替代
4 KEEP_BACKGROUND_RUNNING system_grant always 后台持续监控 仅前台监控
5 VIBRATE system_grant - 振动预警提醒 无需降级
6 LOCATION user_grant inuse 精确室内导航 粗略定位/静态地图
7 APPROXIMATELY_LOCATION user_grant inuse 粗略楼层定位 静态地图无蓝点
8 GET_WIFI_INFO system_grant - WiFi辅助定位 手动选择楼层

附录 B:权限申请代码模板

import { abilityAccessCtrl, bundleManager, common, Permissions } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'

export class PermissionHelper {
  private static manager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager()

  static async check(context: common.UIAbilityContext, perm: Permissions): Promise<boolean> {
    try {
      const info = await bundleManager.getBundleInfoForSelf(
        bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
      )
      const tokenID = info.appInfo.accessTokenId
      const status = await PermissionHelper.manager.checkAccessToken(tokenID, perm)
      return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
    } catch (e) {
      hilog.error(0x0000, 'IVGuard', `Permission check failed: ${(e as Error).message}`)
      return false
    }
  }

  static async request(context: common.UIAbilityContext, perm: Permissions): Promise<boolean> {
    try {
      const result = await PermissionHelper.manager.requestPermissionsFromUser(context, [perm])
      return result.authResults.length > 0 && result.authResults[0] === 0
    } catch (e) {
      hilog.error(0x0000, 'IVGuard', `Permission request failed: ${(e as Error).message}`)
      return false
    }
  }

  static async ensure(context: common.UIAbilityContext, perm: Permissions): Promise<boolean> {
    const granted = await PermissionHelper.check(context, perm)
    if (granted) {
      return true
    }
    return await PermissionHelper.request(context, perm)
  }
}

附录 C:相关文档索引

文档 描述
HarmonyOS 权限开发指南 https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/permission-develop-V5
module.json5 配置说明 https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/module-configuration-file-V5
backgroundTaskManager API https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/backgroundtaskmanager-V5
notificationManager API https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-notificationmanager-V5
IVGuard 架构设计文档 01-architecture-overview.md
IVGuard 数据模型文档 04-data-models.md
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐