一、把“同意过”与“现在能启动”分开

准备上架的应用,经常会有这样一个看似很小的状态:用户已经阅读隐私说明,点过同意,于是本地留下一条布尔值。以后启动页面,只要读到 true 就打开相关功能。页面上看不出什么问题,工程上却藏着两个时序:本地记录是否已经真正落盘,以及冷启动时业务能力是否早于读取结果执行。尤其当用户主动撤回同意后,这两个时序会变得非常敏感。

本篇不讨论如何在审核后台填写材料,也不把示例说成已经通过审核。这里搭建一个名为 ConsentGate 的小型页面,专门把“授权状态”与“功能启动许可”隔离。演示任务标识为 CG-312,隐私说明版本为 v3。我们选择的镜头是撤回之后:页面显示 已撤回,本地持久化反馈 FLUSHED,敏感能力门禁为 BLOCKED。图中时间 09:44:12、日志与状态都是预设演示,不是设备性能统计。

曾经同意不等于现在同意。即使上一次运行保存的是 GRANTED,也要确认策略版本仍有效、读取已经结束,并且没有新的撤回动作。最容易出错的是在页面创建时先初始化三方能力、稍后才异步读取 Preferences;在这种实现里,UI 表示拒绝也可能无法阻止早已开始的工作。我的取舍是先锁门,再读取,最后根据结果决定能否放行。

二、先把状态建成可解释的门禁

ConsentGate 的状态不是一个简单的 boolean。页面需要描述读取中、未知、同意、撤回和写入失败,而真正的业务门禁只有明确同意且策略版本匹配时才能打开。UNKNOWN 不是默认同意,SAVE_FAILED 也不能解释成撤回成功。两者都应该让敏感能力维持阻断,等待用户或应用采取明确操作。

这部分先解决冷启动读取的顺序问题。使用华为 ArkData 的 preferences.getPreferences 获取实例,再通过 get 读取两项轻量状态;读取的异常和未知版本统一返回关闭。这里的 readGate 是项目自己封装的方法,不是系统 API。

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

export class ConsentRepository {
  constructor(private context: common.UIAbilityContext) {}

  async readGate(policyVersion: string): Promise<boolean> {
    try {
      const store = await preferences.getPreferences(
        this.context, { name: 'consent_gate' });
      const decision = String(await store.get('decision', 'UNKNOWN'));
      const version = String(await store.get('version', ''));
      return decision === 'GRANTED' && version === policyVersion;
    } catch (error) {
      console.error('[ConsentGate] read failed');
      return false;
    }
  }
}

getPreferences 与 get 都允许异步调用,这里的判断必须等待 Promise 结束。只在页面上先写 sdkGate='BLOCKED' 并不够:真正负责启动敏感功能的业务层也必须接收同一个放行结果。否则 UI 与后台服务很容易出现两份相互矛盾的状态。当前样例没有接入任何实际 SDK,所谓门禁仅表示 应用侧调用开关。

策略版本用 v3 是为了演示同意语义与说明版本绑定,而不是暗示华为规定了必须采用这种字段。后续改版时,开发团队需要自行判断什么变化必须要求用户重新阅读;如果政策文本已变化,但本地仍只有旧版本,同样按未取得有效同意处理。

三、撤回动作要先堵住入口,再说明是否落盘

撤回最危险的不是按钮没响应,而是按钮立即变灰、页面显示成功,可磁盘上仍然是旧值。Preferences 的 put 修改的是缓存数据,持久化还需要按存储模式使用 flush。官方文档同时提醒,首选项并非多进程安全存储。因此我们把写入顺序和错误反馈当成一个完整动作,而不是让 UI 自行推测成功。

下面的代码解决撤回后的调用放行问题:一进入撤回流程先把运行时门禁设为 BLOCKED,然后写 REVOKED、版本 v3,等待 flush 结束后才显示 FLUSHED。成功日志按演示顺序使用 CG-312;失败时只显示 SAVE_FAILED,不输出“撤回已持久化”的承诺。

async revokeConsent(): Promise<void> {
  this.sdkGate = 'BLOCKED';
  this.consentStatus = 'REVOKED';
  this.storageState = 'WRITING';
  console.info('[ConsentGate] CG-312 action=REVOKE');
  try {
    const store = await preferences.getPreferences(
      this.context, { name: 'consent_gate' });
    await store.put('decision', 'REVOKED');
    await store.put('version', 'v3');
    await store.flush();
    this.storageState = 'FLUSHED';
    console.info('[ConsentGate] CG-312 storage=FLUSHED');
    console.info('[ConsentGate] CG-312 sdkGate=BLOCKED');
  } catch (error) {
    this.storageState = 'SAVE_FAILED';
    console.error('[ConsentGate] CG-312 storage=SAVE_FAILED');
  }
}

代码在当前页面会阻断后续敏感操作,但不能承诺磁盘故障情况下撤回意图一定跨进程保留。两次 put 加一次 flush 也并不是数据库事务;如果应用在写入中途被终止,重新启动时仍可能读到旧记录。正式项目可考虑把版本与决定编码为一个完整记录,记录必要的同意证据版本,并为写失败设计补偿和再确认机制。不能因为界面状态好看,就省略异常路径。

FLUSHED 仅表示本地存储操作已完成,不意味着云端相关数据已经删除,也不代表已经取消操作系统级权限。权限授权、第三方 SDK 数据删除及账户注销是各自独立的链路,需要分别验证和解释。这里不借用一个页面按钮去冒充全部撤回义务。

四、冷启动不要和页面生命周期抢速度

页面 aboutToAppear 往往适合发起本地状态读取,却不适合在没有任何判定结果时抢先启动依赖同意的工作。实践中我更在意一个朴素原则:没有拿到明确放行令牌之前,敏感功能入口一律不可调用。这样即便加载 UI 很快、读取偶尔很慢,也只会多出现短暂等待,不会产生不可解释的提前访问。

下面的简化页面状态把 UI 与后台门禁接起来,关键是先设置默认 BLOCKED,再等待 readGate('v3')。当页面已经离开时,不把异步返回重新写回旧组件。这个 pageAlive 是业务保护标识,而不是 HarmonyOS 自动提供的生命周期锁。

@State consentStatus: string = 'UNKNOWN';
@State sdkGate: string = 'BLOCKED';
@State storageState: string = 'WAITING';
private pageAlive: boolean = false;

aboutToAppear(): void {
  this.pageAlive = true;
  this.sdkGate = 'BLOCKED';
  this.loadConsent();
}

private async loadConsent(): Promise<void> {
  const allowed = await this.repo.readGate('v3');
  if (!this.pageAlive) return;
  this.consentStatus = allowed ? 'GRANTED' : 'UNKNOWN';
  this.sdkGate = allowed ? 'ALLOWED' : 'BLOCKED';
}

aboutToDisappear(): void {
  this.pageAlive = false;
}

这里的 this.repo 代表页面初始化时注入的 ConsentRepository,而不是系统对象。若工程采用单实例应用级服务,光有页面布尔值仍无法阻止服务自己提前启动;应将 readGate 的结果封装为应用级准入控制,并让所有敏感启动分支调用它。页面离开并不代表需要销毁 Preferences 文件,也不应该直接删除用户历史选择,避免下一次启动出现无谓的重复弹窗。

五、用失败矩阵而不是一张成功图验收

示例画面以撤回完成为终点:CG-312 / v3 / REVOKED / FLUSHED / BLOCKED。HiLog 的示意顺序是 09:44:10 consent=YES version=v3,09:44:12 action=REVOKE,接着 storage=FLUSHED 与 sdkGate=BLOCKED。这些是为了让正文、示例代码和界面拥有同一份状态词典,不是运行产生的真实记录。手机图则把同一结果翻译为“已撤回”“敏感能力已暂停”和“重新查看隐私说明”。

真正验收时,至少要人工覆盖四组情况:初装无记录应保持关闭;持有旧版本同意应进入重新确认;主动撤回应立即阻断后续调用并等待持久化结果;模拟 get、put、flush 失败时应显示明确错误。还要在应用重启、页面快速进出、后台恢复和重复点击撤回的情况下复核,不能只观察屏幕上有没有红色提示。

对于审计证据,建议把“用户动作”“应用侧门禁状态”“本地持久化反馈”和“实际敏感能力调用情况”分开记录。日志里只留下动作与状态代码,不记录用户隐私说明全文、设备标识或原始敏感信息。真正对外发布时,仍要检查应用隐私政策、所用 SDK 的数据处理声明与相关业务授权链路;这份示例不替代法律或应用市场的审核规则。

六、工程结论:撤回成功是持续约束,不是一个提示词

这个小页面刻意没有接入真实第三方 SDK,也没有声称能够自动阻断所有系统权限。它提供的是可移植的应用侧约束:默认拒绝、明确放行、撤回先锁门、落盘完成再反馈、失败状态可辨认。最值得坚持的一点,是把“UI 看起来同意了”和“服务允许执行”拆成两个独立判断,而不是到处散落一个 isAgreed 变量。

本例已完成的只是代码级方案说明与演示画面的结构核对,没有进行 DevEco 构建、设备冷启动、存储断电测试或上架审核。要进入实际发布,需用真实工程检查依赖初始化位置、多进程边界、系统权限与 SDK 停止能力,尤其要对写入失败后的跨启动恢复建立可以复现的测试。只有这些事实可验证,页面上那句“已撤回”才真正有工程意义。

参考资料:华为 HarmonyOS《@ohos.data.preferences(用户首选项)》API 参考(2026-09-09);华为《SDK 合规使用指南》(2026-03-24)。以上资料用于确认首选项接口与合规原则,不代表本 Demo 已通过审核。

Logo

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

更多推荐