基于鸿蒙OS开发静脉输液智能监控系统(19)-权限管理与安全设计
基于鸿蒙OS开发静脉输液智能监控系统(19)-权限管理与安全设计
目录
- 1. HarmonyOS 权限体系
- 2. IVGuard 8 项权限详解
- 3. module.json5 权限配置详解
- 4. 运行时权限申请代码实现
- 5. 优雅降级策略
- 6. 数据安全
- 7. 最小权限原则
- 8. 用户隐私保护
- 9. 权限相关常见问题
1. HarmonyOS 权限体系
1.1 授权方式:system_grant 与 user_grant
HarmonyOS 的权限管理采用二元授权模型,所有权限按授权方式分为 system_grant(系统授权) 和 user_grant(用户授权) 两大类。理解这两类权限的本质区别,是正确配置和使用权限的前提。
1.1.1 system_grant(系统授权)
system_grant 类型的权限对应的数据或功能不会涉及用户或设备的敏感信息。应用在安装阶段,系统会自动将相应权限授予给应用,无需用户手动确认。
核心特征:
- 授权时机:应用安装时自动授予,用户无感知
- 用户交互:无弹窗,无确认对话框
- 配置要求:在
module.json5的requestPermissions中仅需声明name字段 - 不可撤销:用户无法通过系统设置手动关闭此类权限(应用卸载时自动回收)
- 典型权限:
ohos.permission.INTERNET、ohos.permission.VIBRATE、ohos.permission.GET_WIFI_INFO、ohos.permission.KEEP_BACKGROUND_RUNNING
示例:当 IVGuard 申请振动权限(VIBRATE)时,用户安装应用后系统自动授予,无需任何确认操作。这是因为振动功能属于设备基础能力,不涉及用户隐私数据。
1.1.2 user_grant(用户授权)
user_grant 类型的权限涉及用户或设备的敏感信息。应用安装后不会自动获取此类权限,必须在运行时主动向用户发起授权请求,由用户在弹窗中选择"允许"或"拒绝"。
核心特征:
- 授权时机:运行时动态申请,用户主动授权
- 用户交互:系统弹出授权对话框,用户点击"允许"或"拒绝"
- 配置要求:在
module.json5中必须完整配置name、reason、usedScene三个字段 - 可撤销:用户可随时通过"设置 > 隐私 > 权限管理"关闭已授权的权限
- 二次授权引导:用户拒绝后,再次申请时系统不再弹窗,需引导用户到系统设置页手动开启
- 典型权限:
ohos.permission.CAMERA、ohos.permission.LOCATION、ohos.permission.NFC_TAG
示例:当 IVGuard 需要使用后置摄像头监控输液瓶时,必须调用 requestPermissionsFromUser() 触发系统弹窗,用户点击"允许"后才能访问相机。如果用户拒绝,则无法使用相机功能,应用需提供降级方案。
1.1.3 两类权限对比总览
| 维度 | system_grant | user_grant |
|---|---|---|
| 授权时机 | 安装时自动授予 | 运行时弹窗请求 |
| 用户感知 | 无感知 | 明确感知(弹窗) |
| 配置字段 | 仅 name |
name + reason + usedScene |
| 可否撤销 | 不可手动撤销 | 用户可随时撤销 |
| 涉及隐私 | 不涉及敏感信息 | 涉及敏感信息 |
| 代码申请 | 无需运行时申请 | 需调用 requestPermissionsFromUser |
| 上架审核 | 配置即通过 | 需审查 reason 合理性 |
| 二次授权 | 不适用 | 拒绝后需引导至设置 |
1.2 受限权限(restricted permissions)
受限权限是 HarmonyOS 权限体系中管控最严格的一类权限。这类权限即使属于 system_grant 或 user_grant,也不能被普通应用直接申请,需要经过额外的审批流程。
受限权限的特征:
- APL 等级要求高:通常需要
system_basic或system_core级别的应用才能申请 - ACL 审批:普通
normal等级的应用若需使用受限权限,必须通过访问控制列表(ACL)申请特殊审批 - 上架审核严格:应用商店会重点审查受限权限的使用必要性和合理性
- 滥用风险高:受限权限通常涉及系统核心资源或高度敏感的用户数据
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 的工作机制:
- 声明:在应用的配置文件中声明 ACL token
- 审批:提交至 HarmonyOS 开发者平台进行审核
- 签名:审核通过后,将 ACL token 写入应用的签名证书
- 生效:系统在安装应用时验证签名中的 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 同时申请 LOCATION 和 APPROXIMATELY_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 最核心的权限之一,用于后置摄像头实时监控输液瓶液位。应用通过后置摄像头持续采集输液瓶的图像数据,利用计算机视觉算法识别液面位置,计算剩余液量和预计输完时间。
具体使用场景:
- 液位监控:用户将设备后置摄像头对准输液瓶,系统通过实时图像分析识别液面高度
- 滴速检测:通过连续帧分析检测滴斗中的液滴滴落频率,推算输液速度
- 异常识别:识别输液瓶倾斜、气泡、回血等异常状态
为什么是后置摄像头:
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 贴纸方案的设计考量:
- 快速录入:相比手动输入药品信息,NFC 一碰即读,极大提升护士工作效率
- 防错机制:NFC 数据由药房系统写入,避免人工输入导致的药物信息错误
- 可追溯性:每次 NFC 读取都有时间戳记录,支持医疗操作追溯
- 数据完整性: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.json5 的 abilities 配置中,还需要添加 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 权限用于发送输液预警通知。当监控系统检测到输液异常时,通过系统通知渠道向用户发送预警信息。
通知场景分类:
-
紧急预警(高优先级):
- 输液即将完成(剩余液量 < 10%)
- 检测到回血
- 输液管路阻塞
- 滴速异常突变
-
一般提醒(标准优先级):
- 液位到达预设阈值
- 预计输完时间提醒
- NFC 配对成功确认
-
信息通知(低优先级):
- 监控状态更新
- 护士站消息推送
通知设计原则:
- 紧急预警必须通过系统通知发送,确保用户即使不在应用内也能及时感知
- 通知内容不含具体医疗数据(如药品名称),仅提示"输液异常,请查看"
- 用户可在应用内自定义通知偏好(开关、阈值、提醒方式)
- 通知支持点击跳转到对应的监控详情页
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 正在后台进行数据传输任务"的提示信息。
后台运行约束:
- 系统会进行一致性校验,确保应用实际执行了对应类型的长时任务
- 通知栏消息被用户删除时,系统会自动停止长时任务
- 长时任务期间不可执行与声明类型不符的操作
- 需在
module.json5中同时声明backgroundModes
module.json5 配置
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
}
注意:system_grant 权限无需 reason 和 usedScene 字段,仅需声明 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 权限用于在输液异常时提供振动提醒。振动是一种无需查看屏幕即可感知的提醒方式,特别适合以下场景:
- 紧急异常提醒:检测到回血、阻塞等严重异常时,配合通知和声音进行振动提醒
- 阈值到达提醒:液位到达预设阈值时短振动提示
- NFC 配对反馈:成功读取 NFC 标签时短振动确认
- 操作确认:关键操作(如启动监控、停止监控)的触觉反馈
振动模式设计:
| 场景 | 振动模式 | 持续时间 |
|---|---|---|
| 紧急异常 | 长振动 | 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 的定位场景包括:
- 患者位置定位:确定患者所在病区、楼层和病床位置
- 护士站导航:为需要帮助的患者提供到最近护士站的路线
- 紧急求助定位:紧急情况下快速定位患者位置,通知就近护士
- 设施查找:查找最近的洗手间、电梯、药房等
定位精度需求:
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 和信号强度,可以:
- 辅助室内定位:WiFi 指纹定位是室内导航的重要辅助手段
- 确定所在楼层和区域:不同楼层的 WiFi AP 信号特征不同
- 提升定位精度:与 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 |
理由的撰写遵循以下原则:
- 以"需要"开头:直接说明功能的必要性,避免使用"想要"或"希望"等弱化语气的表达
- 具体用途说明:明确权限的具体用途而非笼统描述,例如"监控输液瓶液位变化"而非"使用相机"
- 字数控制:控制在 15 至 30 字之间,既简洁又不模糊
- 避免技术术语:面向普通用户的可理解性,不使用"NDEF"“BSSID”"AccessToken"等技术术语
- 共享理由: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 字段,不需要 reason 和 usedScene。这并非疏忽,而是由 system_grant 的授权机制决定的:
- 无需用户参与:
system_grant权限在应用安装时由系统自动授予,不触发用户授权弹窗,因此reason没有展示的场景和必要性 - 不受前后台限制:
system_grant权限的使用不受应用前后台状态的影响,因此when字段没有区分的必要 - 使用范围由系统管理:
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 类型,但我们在配置中仍然提供了 reason 和 usedScene。这是一种防御性编程策略:即便当前系统版本不强制要求,未来版本可能会加强 system_grant 权限的审核要求,提前配置可以避免后续适配工作。
3.4 reason 字符串资源引用:$string:xxx
reason 字段采用字符串资源引用而非硬编码文本,这是 HarmonyOS 的强制要求。资源引用的格式为 $string:resource_name,其中 resource_name 必须在 entry/src/main/resources/base/element/string.json 中有对应的定义。
使用资源引用的好处:
- 国际化支持:同一资源名可以对应多种语言的翻译,当用户切换系统语言时,权限理由自动切换为对应语言
- 集中管理:所有权限理由集中在一个配置文件中,便于统一审查和修改
- 编译时校验:如果引用了不存在的资源名,编译时会报错,避免运行时显示空白理由
- 格式规范:系统框架对资源引用有统一的解析和展示逻辑,确保理由在各种设备上显示一致
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" 仅在满足以下所有条件时方可选择:
- 有明确的后台运行需求,且该需求是功能核心而非可选增强
- 已论证
"inuse"无法满足功能需求,后台场景不可或缺 - 已评估对电池续航和系统资源的影响,并设计了补偿措施
- 已设计后台任务的生命周期管理机制,避免无限制的后台运行
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 的关键注意事项:
- context 必须是 UIAbilityContext:不能使用
ApplicationContext或ExtensionContext,因为权限弹窗需要与 UI 窗口关联 - 权限名称必须与声明一致:
permissions数组中的权限名称必须与module.json5中声明的一致,否则系统无法识别 - 单次不超过 3 个:建议单次请求不超过 3 个权限,避免弹窗信息过于复杂导致用户直接全部拒绝
- 已授权的权限自动跳过:如果请求的权限已经被授予,系统不会重复弹窗,而是直接在结果中返回已授权状态
- 不能在 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 | 导航页 | 患者找护士站 | 是 |
"按需申请"策略的核心优势:
- 功能关联性强:用户在需要使用功能时才看到权限请求,能将权限与功能直接关联,授权率显著高于启动时集中申请
- 避免用户反感:不会在应用启动时弹出大量授权窗口导致用户反感甚至直接卸载
- 按需授权:不会提前申请用户暂不需要的权限,减少不必要的权限持有时间
- 符合设计原则:符合 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 的设计遵循以下原则:
- 单一职责:每个方法只做一件事(检查或申请),组合方法(checkAndRequest)通过调用基础方法实现
- 容错优先:所有异常都被捕获并记录日志,不会因权限检查失败而崩溃
- 默认拒绝:当无法确定权限状态时,默认返回 false(未授权),确保功能进入安全的降级模式
- 无状态设计:工具类不维护权限状态的缓存,每次检查都实时查询系统,避免状态不同步
5. 优雅降级策略(每种权限详细分析)
权限降级是 IVGuard 容错设计的核心组成部分。在医疗场景下,应用的可用性直接关系到患者的安全与体验。因此,每一项权限的缺失都必须有对应的降级方案,确保应用在部分权限不可用时仍能提供有价值的服务。降级设计的核心原则是:功能可以受限,但不可缺失;体验可以降级,但不可中断。
5.1 无 CAMERA:手动输入液位、降级 UI 设计
降级场景分析
相机权限被拒绝是 IVGuard 最严重的降级场景,因为自动液位检测是应用的核心价值。VisionService 无法获取摄像头画面,自动检测功能完全不可用。但降级并不意味着功能完全丧失——手动液位输入模式仍然能够提供基本的监控和预警能力。
降级 UI 设计
MonitorPage 在无相机权限时的 UI 变化如下:
- 相机预览区域:替换为灰色占位区域,中央显示相机图标和"相机权限未开启"提示文字,下方显示"前往设置"按钮,点击后跳转系统应用设置页
- 手动液位输入:预览区域下方增加液位滑块控件和数字输入框,用户可以拖动滑块或直接输入数字来报告当前液位
- 液位数字显示:从自动更新变为手动更新模式,数字颜色从蓝色变为橙色以区分模式
- 流速显示:显示"手动模式下无法自动检测流速"提示,流速输入框可选填
- 模式标识:页面顶部显示"手动模式"标签,橙色背景,持续提醒用户当前非自动监控
降级 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 适配
- 隐藏 NFC 按钮:将"NFC 配对"按钮设为不可见或禁用状态,避免用户点击后失败
- 显示贴纸 ID 输入框:增加文本输入框,支持手动输入贴纸上的编号
- 显示条形码/二维码扫描按钮:ScanService 提供的扫描功能作为替代方案
- 从药物数据库搜索:DrugDatabase 提供按名称或编码搜索药物的功能
- 历史记录快速选择:显示最近使用的药物列表,用户可以快速选择而不必重新输入
降级代码逻辑:
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 适配
- 地图正常显示:楼层平面图和 POI 标记正常渲染
- 移除蓝色定位圆点:不显示用户当前位置标记
- 路径规划降级:从"显示从当前位置到目的地的路线"降级为"显示目的地位置,请自行判断方向"
- 步行时间不可用:显示"无法估算步行时间(缺少定位权限)"
- 楼层切换保留:手动选择楼层功能不受影响,用户可以通过选择楼层查看不同楼层的地图
降级后的 HospitalMapView 仍然提供以下价值:
- 查看医院各楼层的地图布局
- 了解护士站、输液室、急诊室的位置
- 手动选择楼层查看对应 POI 标记
- 了解目的地在哪个楼层哪个区域
这种降级方式保留了地图的信息展示功能,只是失去了"我在哪里"的定位能力。对于熟悉医院环境的护士而言,静态地图已经足够实用。
5.4 无 NOTIFICATION:应用内 Toast/Dialog 替代
降级场景分析
通知权限被拒绝后,NotificationService 无法通过系统通知栏发送预警,这是 IVGuard 第二严重的降级场景。在应用后台运行时,预警信息将无法通过通知栏到达用户。但应用前台运行时的替代提醒机制仍然可用。
替代方案
- Toast 提示:当应用在前台时,使用 Toast 显示预警信息,持续 5 秒
- AlertDialog 弹窗:对于高级别预警(输液完成、流速严重异常),使用 AlertDialog 确保用户注意到
- 页面内预警横幅:MonitorPage 顶部显示醒目的预警横幅,闪烁红色背景
- 振动补偿:振动权限(system_grant)不受影响,通过振动提醒用户查看应用
- 语音播报: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 秒采样中断,监控数据出现空白期。用户锁屏时也会导致同样的问题。
降级策略
- 仅前台监控:监控仅在应用前台时有效,切换应用或锁屏时暂停
- 锁屏提示:用户锁屏时弹出提示对话框"锁屏后监控将暂停,建议保持屏幕常亮或开启后台运行权限"
- 常驻通知提示:如果有通知权限,显示"IVGuard 监控中,点击返回"的通知,引导用户返回前台
- 保持屏幕常亮选项:SettingsPage 增加"监控时保持屏幕常亮"选项,防止锁屏中断监控
- 暂停恢复机制:应用重新回到前台时,自动恢复监控并补充记录暂停期间的时间段
5.6 降级 UI 的统一设计模式
IVGuard 的降级 UI 遵循统一的设计模式,确保不同降级场景下的一致用户体验:
降级提示组件
所有权限降级场景共用同一套提示组件,包含以下元素:
- 图标:使用对应功能的灰色图标表示功能不可用
- 标题:简洁描述缺失的功能,如"相机权限未开启"“定位服务不可用”
- 说明:解释降级后的功能变化和影响
- 操作按钮:提供"前往设置"(跳转系统权限设置)和"继续使用"(接受降级模式)两个按钮
- 降级标识:使用橙色标签标识当前处于降级模式
降级状态管理
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 的应用沙箱机制是数据安全的第一道防线。每个应用运行在独立的沙箱中,拥有自己的文件系统空间和进程空间。具体而言:
- 文件隔离:每个应用只能访问自己沙箱内的文件,无法直接读取其他应用的文件
- 数据库隔离:Preferences 数据库存储在应用沙箱的私有目录中,其他应用无法访问
- 内存隔离:应用进程的内存空间是独立的,其他进程无法直接读取
- 网络隔离:应用的网络请求通过系统代理,系统可以实施网络策略控制
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 采用以下脱敏规则:
- 姓名脱敏:两个字的姓名显示为"张*“,三个字及以上显示为"张*三”
- 床号保留:床号不脱敏,因为它是定位信息而非隐私信息
- 药物名称保留:药物名称不脱敏,因为其不含个人隐私
- 预警通知脱敏:通知栏消息中使用脱敏姓名,避免锁屏时暴露完整姓名
6.3 不存储的敏感信息
IVGuard 明确不存储以下类别的敏感信息:
- 身份证号码:应用功能不需要身份证号,绝不收集和存储
- 银行卡号:费用估算功能仅使用参考价格,不涉及真实支付,无需银行卡信息
- 医保卡号:保险设置仅保存报销比例等参数,不保存医保卡号
- 联系方式:不收集和存储电话号码、邮箱地址等联系方式
- 精确位置历史:不存储定位轨迹,仅在使用导航功能时临时获取位置
- 相机影像数据:不保存任何照片或视频帧,相机仅用于实时检测
这一"不存储"策略是最小数据原则的极致体现:如果数据不是功能所必需的,就根本不要收集。收集了不必要的数据不仅增加安全风险,还违反多项隐私法规。
6.4 最小数据原则的贯彻
最小数据原则要求应用只收集和存储完成功能所必需的最少数据。IVGuard 从以下方面贯彻这一原则:
- 数据字段最小化:每个数据模型只包含功能必需的字段。例如,
Medicine模型不包含患者信息,MonitorSession模型不包含药物详细属性,各模型职责清晰、数据不冗余 - 数据保留期限:监控记录保留最近 30 天,超过 30 天的记录在应用启动时自动清理
- 数据聚合存储:液位记录按会话聚合存储,不保存原始图像帧数据
- 默认值策略:新增数据时,非必要字段使用默认值而非要求用户填写
6.5 数据清除机制
IVGuard 的数据清除机制涵盖以下场景:
- 卸载自动清除:应用卸载时,沙箱内的所有数据(包括 Preferences 数据库)自动被系统清除,无需应用额外处理
- 手动清除:SettingsPage 提供"清除应用数据"选项,用户可以手动清除所有本地数据
- 过期自动清除:DataStore 在初始化时检查数据的时间戳,自动清除超过保留期限的历史记录
- 会话结束清除:监控会话结束后,临时数据(如实时帧数据)立即释放
6.6 未来加密方案:AES-256
当前版本 IVGuard 使用 HarmonyOS 沙箱隔离作为数据安全的基础保障。未来版本计划引入 AES-256 加密方案,对高敏感数据进行应用层加密存储:
- 加密范围:患者姓名、监控记录等高敏感数据在写入 Preferences 前进行 AES-256 加密
- 密钥管理:使用 HarmonyOS 的 HUKS(Universal KeyStore)服务管理加密密钥,密钥存储在安全硬件中
- 透明加密:DataStore 层面实现透明加密/解密,上层业务代码无需感知加密逻辑
- 密钥轮换:支持定期轮换加密密钥,增强长期安全性
- 迁移方案:从明文存储到加密存储的迁移,提供自动迁移工具
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 在功能停止使用后,会主动释放相关资源并停止使用权限保护的功能:
- 监控结束:调用
VisionService.stopDetection()停止相机检测,调用backgroundTaskManager.stopBackgroundRunning()释放后台任务 - 离开导航页面:停止位置更新请求,释放定位资源
- NFC 读取完成:关闭 NFC 标签会话,释放 NFC 资源
- 应用退到后台(非监控状态):不持有任何 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 建议在每个版本发布前,对权限使用情况进行审计。审计清单如下:
- 所有
module.json5中声明的权限是否都有对应的功能支撑? - 每项
user_grant权限的reason是否准确描述了当前版本的用途? usedScene.when的选择是否仍然合理?- 是否有新功能引入了新的权限需求但未在
module.json5中声明? - 是否有已废弃的功能对应的权限可以移除?
- 运行时权限申请的时机是否与功能入口对齐?
- 每项权限的降级方案是否仍然有效且经过测试?
- 权限相关的日志是否过滤了敏感信息?
8. 用户隐私保护设计
8.1 权限申请时机:功能使用前而非 App 启动
IVGuard 坚决不在应用启动时集中请求所有权限。这种做法虽然简化了开发,但严重损害了用户体验和信任。我们采用"功能驱动"的权限申请策略:
启动时不申请任何权限:用户首次打开 IVGuard 时,不会看到任何权限弹窗。应用直接展示首页,用户可以浏览基本功能和信息。
功能入口触发申请:只有当用户主动使用需要权限的功能时,才触发对应的权限申请流程。这种"按需申请"的策略使得每个权限请求都有明确的功能上下文,用户能够理解权限与功能的关联。
预引导弹窗:在系统权限弹窗之前,IVGuard 会先展示一个应用内的解释弹窗,包含以下内容:
- 为什么需要这个权限(具体的功能说明)
- 权限将如何使用(技术细节的通俗解释)
- 不授权会有什么影响(功能降级的具体描述)
- 用户可以随时在设置中关闭权限(控制权说明)
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 提供了"权限管理"区域,清晰展示每项权限的使用说明和当前状态。这一设计的目的是让用户在应用内就能了解权限的全貌,无需跳转到系统设置查看。
权限说明区域包含以下信息:
- 权限名称(使用用户可理解的中文描述,如"相机"而非"ohos.permission.CAMERA")
- 权限用途(与
module.json5中的 reason 一致) - 当前授权状态(已授权 / 未授权 / 不可用)
- 跳转系统设置的快捷按钮
8.4 可控性:用户可随时关闭任何权限
IVGuard 确保用户拥有对权限的完全控制权:
- 用户可以在系统设置中随时关闭任何权限,应用不会阻止或干扰
- 权限关闭后,应用立即切换到对应的降级模式,不会弹出强制授权的要求
- SettingsPage 中的权限说明区域提供"前往系统设置"的快捷入口
- 每次页面进入前台时,重新检查权限状态,确保降级策略与权限状态同步
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 的设计遵循以下隐私法规的核心要求:
《中华人民共和国个人信息保护法》合规
- 告知同意:权限申请前告知用途,获取用户明确同意(对应 user_grant 机制)
- 最小必要:仅收集功能必需的最少信息(对应最小权限原则)
- 安全保障:采用沙箱隔离保护存储数据(对应 HarmonyOS 安全机制)
- 删除权:用户可以随时清除数据(对应 DataStore 清除功能)
- 透明度:隐私政策公开透明(对应 SettingsPage 权限说明)
GDPR 合规考量
虽然 IVGuard 当前仅面向中国市场,但设计中已预留 GDPR 合规的扩展空间:
- 数据最小化(Data Minimization):与最小必要原则一致
- 目的限制(Purpose Limitation):权限用途与声明一致,不作超范围使用
- 存储限制(Storage Limitation):数据保留期限明确,过期自动清除
- 完整性保密性(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)时,必须采取以下防护措施:
- 参数化查询:所有数据库查询使用参数化方式,不拼接 SQL 字符串
- 输入转义:对用户输入的特殊字符进行转义处理
- 最小权限:数据库连接使用最小权限账户,禁止 DDL 操作
- ORM 封装:通过 DataStore 层封装所有数据库操作,上层代码不直接执行 SQL
9.3 敏感日志过滤
IVGuard 的日志输出遵循以下安全规则:
- 患者姓名脱敏:日志中使用"患者*"替代真实姓名
- 不记录敏感数据:身份证号、银行卡号等敏感数据不写入日志
- 使用 hilog 分级:开发阶段使用 DEBUG 级别,发布版本使用 INFO 及以上级别
- 日志域隔离: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 是纯本地应用,不涉及网络通信。未来版本接入服务端时,将采取以下安全措施:
- HTTPS 强制:所有网络请求使用 HTTPS,禁止 HTTP 明文传输
- 证书校验:启用 SSL 证书校验,防止中间人攻击
- 请求签名:API 请求使用 HMAC 签名,防止篡改
- 令牌认证:使用 OAuth 2.0 或 JWT 进行身份认证
- 数据加密:敏感数据在传输前进行端到端加密
9.5 代码混淆(Release 构建)
IVGuard 在 Release 构建时启用代码混淆,保护应用的源代码不被逆向分析:
- 名称混淆:类名、方法名、变量名替换为无意义的短名称
- 控制流混淆:打乱代码的控制流结构,增加反编译难度
- 字符串加密:对字符串常量进行加密处理
- 调试信息移除:移除所有调试日志和断言
混淆配置在 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: 第一次授权后再次弹窗是什么原因?
现象:用户已经授权某权限,但再次进入功能页面时又弹出授权窗口。
原因分析:
- 用户可能在系统设置中手动关闭了权限,导致已授权状态失效
- 系统在长时间未使用应用后自动回收了权限
- 应用版本更新后,权限的 tokenID 发生变化,旧的授权记录不再有效
解决方案:在调用 requestPermissionsFromUser() 之前,先通过 checkAccessToken() 检查权限是否已授予。如果已授予则直接使用,不再弹窗。
Q2: 拒绝后如何重新申请?
场景:用户之前拒绝了相机权限,现在想重新开启。
方案:
- 如果用户仅选择"拒绝"(未选择"不再询问"),再次调用
requestPermissionsFromUser()仍然会弹窗 - 如果用户选择了"不再询问",
requestPermissionsFromUser()不会再弹窗,需要引导用户前往系统设置手动开启 - IVGuard 在降级 UI 中提供"前往设置"按钮,点击后跳转系统应用设置页面
Q3: 权限被系统回收怎么办?
现象:应用运行过程中,某个权限突然失效。
原因:
- 系统内存压力大时,可能回收部分非关键权限以释放资源
- 系统更新后权限策略变更
- 低电量模式下系统限制部分权限
应对策略:IVGuard 在每次使用权限前都进行检查,不假设权限一直有效。如果发现权限被回收,自动切换到降级模式并提示用户。
Q4: 多设备权限同步
场景:用户在手机上授权了权限,在平板上安装同一应用后权限状态是否同步?
解答:HarmonyOS 的权限状态是设备级别的,不同设备的权限状态相互独立。用户需要在每台设备上分别授权。IVGuard 的设计已经考虑了这一点——每台设备上的权限检查和降级逻辑是独立的,不会假设其他设备的权限状态。
Q5: 权限与应用评分的关系
场景:权限数量和授权率是否影响应用在应用商店的评分?
解答:
- 应用商店的审核机制会对权限合理性进行评估,过多的权限申请可能导致审核不通过
- 用户评论中经常提及权限问题,"为什么要这么多权限"是常见的差评原因
- IVGuard 的 8 项权限都在合理范围内,且有清晰的功能对应关系
- 应用商店的推荐算法可能考虑权限与同类应用的对比,权限数显著高于同类应用可能影响曝光
Q6: 如何调试权限问题?
方法:
- 使用 hdc 命令查看权限状态:
hdc shell dumpsys permission查看所有权限的授权状态 - 查看应用特定权限:
hdc shell bm dump -n {bundleName}查看应用的权限声明 - 使用 hilog 过滤权限相关日志:
hdc hilog | grep "permission"实时监控权限操作 - 在 DevEco Studio 中使用断点调试权限申请流程
Q7: 模拟器与真机的权限行为差异
差异:
- 部分权限在模拟器上无法正常工作(如 NFC、相机),需要在真机测试
- 模拟器的权限弹窗样式可能与真机不同
- 模拟器可能自动授予某些在真机上需要用户授权的权限
- 后台运行权限的行为在模拟器和真机上可能有差异
建议:权限功能的最终验证必须在真机上进行,模拟器仅用于初步开发调试。
Q8: 如何处理权限申请异常?
场景:调用 requestPermissionsFromUser() 抛出异常。
可能原因:
- 传入的 context 不是 UIAbilityContext
- 在 onCreate 或 onWindowStageCreate 中调用(UI 未准备好)
- 权限名称与 module.json5 声明不一致
- 系统服务异常
处理方式:
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: 应用更新后权限会丢失吗?
解答:
- 一般的应用更新(覆盖安装)不会影响已授予的权限
- 如果更新中新增了权限声明,新增的
user_grant权限需要用户重新授权 - 如果更新中移除了某项权限声明,对应的授权记录会被自动清除
- 系统签名变更可能导致权限状态重置(极少见)
Q10: 如何在代码中区分"从未请求"和"拒绝后不再询问"?
方法:
HarmonyOS 目前没有提供直接区分这两种状态的 API。变通方法:
- 在 SharedPreferences 中记录权限请求的历史状态
- 如果
checkAccessToken()返回未授权,且本地记录显示曾经请求过,则可以推断用户选择了拒绝 - 如果
requestPermissionsFromUser()返回结果中authResults[i] === 2,表示用户选择了"不再询问" - 根据返回值决定是再次弹窗还是直接引导至设置页面
附录
附录 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 |
更多推荐




所有评论(0)