上线前做隐私交互检查,一个容易被跳过的情况是:用户已经在设置中撤回同意,回到主页面却仍看见“同步成功”的提示;或者退出应用再打开后,页面沿用了内存里的旧布尔值,网络入口重新变成可点。界面文案看起来正常,不代表请求真的被挡在门外。这里不讨论隐私政策的法律结论,也不模拟应用市场的审核结果,而是把“用户选择已写入”“下一次启动恢复正确”“后续请求确实被门禁拦住”当成可观察的三个技术阶段。

演示工程叫 ConsentLedger,测试编号 RVC-308,示例政策修订标识 POL-2026-10。界面最终状态约定为 REVOKED、网络入口 BLOCKED、离线帮助 AVAILABLE。政策修订标识纯属应用业务字段,不是鸿蒙系统公布的版本号;状态截图同样只是方案预览,并非真实审核截图。

一、一个按钮的颜色不足以表明请求不会发出

业务上至少有两种不同的“关闭”。一种是关闭向用户展示的联网按钮,另一种是底层任务执行前读取最新授权状态并拒绝动作。前者解决误触,后者才是工程门禁。用户在设置页撤回后,后台残留的定时操作、恢复回调或上一次页面留下的异步任务,都可能绕过按钮本身。因此不能只把 @State isAgreed 设置成 false,就宣布所有服务已停止。

选择存储也不宜随手扩展成一个庞大的审计数据库。用户首选项 Preferences 适合存小型键值设置,支持读取、写入和持久化。演示只保存两个值:choice 和 policyRev。choice 只有 GRANTED、REVOKED 两种明确状态。未知、异常和版本不匹配一律按“没有当前有效同意”处理;这种默认拒绝策略更容易排查,而不是把读不到数据当作曾经同意。

首次启动若存储记录不存在,页面应先显示“未开放在线功能”,让用户查看文档、理解数据用途。离线帮助不依赖远程请求,应一直保留入口。演示中的业务政策版本若发生变化,不应简单沿用旧修订的同意值;是否必须重新征求同意,需由产品法务、业务设计和适用法规判断,此处只是选择一种保守的工程策略。

二、把写入与持久化失败当成两件事

先做最小的状态存储服务。使用 @kit.ArkData 中的 preferences,通过 getPreferencesSync 获取当前应用的Preferences实例,并使用 putSync、flushSync 完成提交。这里的“成功”专指本地轻量状态写入及冲刷,没有代表任何远端服务也已更新。

这段代码解决的是重启后恢复与撤回落盘。read() 还检查修订标识;就算磁盘里存在 GRANTED,也不会在版本不匹配时悄悄放行。

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

const CURRENT_REV = 'POL-2026-10';

class ConsentStore {
  constructor(private ctx: common.UIAbilityContext) {}

  read(): string {
    const db = preferences.getPreferencesSync(this.ctx, { name: 'consent-ledger' });
    const rev = db.getSync('policyRev', '') as string;
    const choice = db.getSync('choice', 'REVOKED') as string;
    if (rev !== CURRENT_REV) return 'REVOKED';
    return choice === 'GRANTED' ? 'GRANTED' : 'REVOKED';
  }

  write(choice: string): void {
    if (choice !== 'GRANTED' && choice !== 'REVOKED') throw new Error('bad choice');
    const db = preferences.getPreferencesSync(this.ctx, { name: 'consent-ledger' });
    db.putSync('policyRev', CURRENT_REV);
    db.putSync('choice', choice);
    db.flushSync();
  }
}

先写版本再写选择,再执行 flushSync,是为了把失败边界显式留给调用方。Preferences适合小量设置,不适合记录无限增长的授权事件流水,更不能替代专门的审计或证据保全系统。这里没有实现跨设备同步;不同设备是否共用选择取决于产品账户体系,不应擅自认为本机Preferences能够自动分布式同步。

写入失败时,调用方应把UI状态恢复为安全值 REVOKED,提示“保存失败,联网功能仍关闭”,并保留重试操作。相反,按钮点击后立刻渲染“已同意”,却不等待存储成功,会制造短暂的不一致窗口。无论选择写入成功与否,请求网关都需要自己读取可信的当前状态;不能拿一次页面刷新充当全部业务模块的授权来源。

三、联网动作统一经过一把可撤销的闸门

下一步给调用远端服务的业务函数加一道门。为了聚焦隐私决策,网络本身采用调用者注入的异步动作 work,不伪装成某个未核实的专用“隐私拦截SDK”。对于已经派发的请求,客户端最多尽力取消,不能承诺把先前上传的数据从服务端收回;本文处理的是后续请求的发起限制、迟到回包的应用层展示隔离。

以下示例中的 generation 是业务世代号。撤回时立即递增,之前开始的异步动作即使返回,也不能覆盖当前的UI状态。run() 在执行前后二次检查;调用方负责在真实网络层接入超时、取消与服务端数据删除机制。

class ConsentGate {
  private generation: number = 0;
  private locallyRevoked: boolean = false;
  private store: ConsentStore;
  constructor(store: ConsentStore) { this.store = store; }

  revoke(): void {
    this.locallyRevoked = true;
    this.generation += 1;
    this.store.write('REVOKED');
  }

  grant(): void {
    this.store.write('GRANTED'); // 落盘成功后,才重新开放本实例
    this.locallyRevoked = false;
    this.generation += 1;
  }

  async run(work: () => Promise<void>): Promise<string> {
    if (this.locallyRevoked || this.store.read() !== 'GRANTED') return 'BLOCKED';
    const ticket = this.generation;
    try {
      await work();
    } catch (_error) {
      return 'REQUEST_FAILED';
    }
    if (this.locallyRevoked || ticket !== this.generation ||
        this.store.read() !== 'GRANTED') {
      return 'LATE_RESULT_IGNORED';
    }
    return 'APPLIED';
  }
}

此处 BLOCKED 表示根本没有调用 work;LATE_RESULT_IGNORED 表示请求可能已经发生,但结果不得再写入当前页面。两者不应该混为一谈。generation 仅覆盖当前 ConsentGate 对象的生命周期,应用重启后还需要从持久化记录恢复初始状态;多窗口、多Ability或进程间执行请求时,必须把同一业务授权判断扩展到实际执行点,而不能靠单个页面实例自说自话。

另一个重要细节在撤回失败场景。上面的演示先把 locallyRevoked 设为真,再做持久化;所以即使 store.write 抛错,本进程后续请求仍会被拒绝。页面要提示保存失败并提供重试,而且重启后的安全默认也必须由存储恢复逻辑保证。已有远端请求是否已取消,不能由这段本地状态控制推导出来。若要对外声明“撤回生效”,还需要结合服务端协议的实际行为验证。

四、让页面展示解释业务状态,而不是遮住错误

ConsentLedger 的页面保留“查看隐私说明”“撤回同意”“离线帮助”三个明确动作。演示默认使用已撤回态,这意味着底部联网入口不可用,同时离线说明仍然能访问。下面的组件把 @State 作为页面可见状态,但不把它当成权威持久化来源。aboutToAppear() 只负责从持久层读取;撤回动作完成后重新同步一次显示。

@Entry
@Component
struct ConsentPage {
  @State choice: string = 'REVOKED';
  @State gateState: string = 'BLOCKED';
  private store?: ConsentStore;

  aboutToAppear(): void {
    const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
    this.store = new ConsentStore(context);
    this.choice = this.store.read();
    this.gateState = this.choice === 'GRANTED' ? 'AVAILABLE' : 'BLOCKED';
  }

  build() {
    Column({ space: 16 }) {
      Text('隐私与联网管理').fontSize(24)
      Text('RVC-308 · POL-2026-10')
      Text(`当前选择:${this.choice}`)
      Text(`联网入口:${this.gateState}`)
      Text('离线帮助:AVAILABLE')
      Button('撤回同意').onClick(() => {
        try {
          this.store?.write('REVOKED');
        } finally {
          this.choice = 'REVOKED';
          this.gateState = 'BLOCKED';
        }
      })
      Button('查看离线帮助')
    }.padding(20).width('100%')
  }
}

在生产工程里,需要再补上失败提示与用户主动再次同意的UI确认链路。这里将按钮点击后的状态设置为拒绝,是为了安全默认;finally 并不代表持久化一定成功。示例日志约定为 RVC-308 choice=REVOKED、RVC-308 gate=BLOCKED、RVC-308 offline=AVAILABLE。实际事件埋点应避免记录用户的敏感原始信息,仅记录必要的流程状态;日志和截图都不应该被当作法律意义上的用户同意凭证。

五、在提交发布前怎样回归这个门禁

建议先从空偏好文件进入应用,验证联网入口初始关闭;再模拟用户主动同意,确认持久化成功才开放;随后撤回并立刻尝试执行联网操作,run() 应在调用外部动作之前返回 BLOCKED。接着退出应用、重新进入,确认读取结果仍为 REVOKED。最后在请求尚未返回时撤回,检查迟到结果不会修改已经显示的状态;若请求本身已经到达服务端,则如实把这一事实记录到工程验证项中,而不是写成“完全阻断”。

本轮画面的最终示例固定为 RVC-308、POL-2026-10、REVOKED、BLOCKED、AVAILABLE。UI里不出现任何虚构的市场审核通过章,也不写精确的网络耗时数字。一个可能的异常分支是 flushSync 失败:页面应保持 BLOCKED 并提示重试。另一个分支是偏好文件损坏或政策修订已变:读取函数回退到拒绝状态,并提供用户重做选择的显式路径。这些路径都应经过真实设备与目标SDK回归,不能靠合成示意图证明。

需要强调:产品应依据实际数据处理活动提供相应的告知与选择,这里只讨论本地偏好状态与应用请求门禁的一致性,并不替代隐私合规审查。应用市场规则、SDK采集行为、权限弹框触发时机,都必须以待发布版本的真实依赖、实际政策和官方审核要求为准。

六、最终看的是状态闭环,不是一张静态页面

开发阶段可以用四个问题做自检:用户拒绝时会不会自动发起网络任务,撤回后存储是否可靠更新,已发出的迟到结果会不会污染页面,再次启动会不会把旧选择恢复成新同意。只有四个问题都能回答,BLOCKED 才不是一个好看的标签。系统层权限授予与本应用业务同意是不同概念,前者存在并不自动意味着后者有效;反过来,用户点击“同意”也不会自动授予系统权限。

对许多应用来说,最稳妥的方案不是加入更多花哨的弹窗,而是让业务入口始终依赖同一个可追踪的状态源。Preferences负责小范围持久化,门禁负责请求是否可发出,ArkUI负责如实展示,三者职责保持清晰,问题发生时才能定位到底是落盘、恢复还是执行路径漏掉了检查。

官方资料:

  • 华为《使用Preferences实现数据持久化》:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V14/data-persistence-by-preferences-V14
  • 华为《应用数据存储与管理》相关参考:https://developer.huawei.com/consumer/cn/doc/
  • 华为《网络SDK数据合规指导》:https://developer.huawei.com/consumer/es/doc/system-Guides/network-sdk-data-compliance-0000001050318444
Logo

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

更多推荐