解析鸿蒙 AntiPeepProtectionPlugin:订阅状态、弹设置和蒙层处理

适合谁看

  • 想读懂防窥插件内部结构的人

  • 想写事件型、状态型鸿蒙原生插件的人

  • 想看一个比命令型插件更复杂的例子的人

问题背景

防窥保护的复杂度不在 API 数量,而在状态变化:

  • 系统开关可能没开

  • 状态可能随时从可见变隐藏

  • 页面离开后还要取消订阅

  • 蒙层不能重复设置

这类能力如果套用"调用一次拿结果"的思路,很容易写坏。

项目中的真实场景

相关实现主要在:

  • app/ohos/entry/src/main/ets/plugins/AntiPeepProtectionPlugin.ets --- 鸿蒙原生插件

  • app/lib/core/platform/anti_peep_protection_channel.dart --- Flutter 侧通道

Flutter 侧的使用场景:

复制代码
// app.dart → _ScaffoldWithNavBarState

// 进入收藏页时激活
if (shouldProtect) {
  AntiPeepProtectionChannel.activateCollectionProtection();
} else {
  AntiPeepProtectionChannel.deactivateCollectionProtection();
}

核心实现

一、4 个防重复状态变量------插件稳定的关键

插件定义了 4 个布尔状态变量:

复制代码
private isProtectionActive: boolean = false;           // 防窥是否激活
private isSubscribed: boolean = false;                 // 是否已订阅系统事件
private hasShownMask: boolean = false;                 // 是否已显示蒙层
private hasRequestedOptionsForActivation: boolean = false; // 是否已请求过开启设置
变量 作用 防止什么问题
isProtectionActive 标记当前是否在防窥保护中 页面未激活时不处理事件
isSubscribed 标记是否已订阅系统事件 避免重复订阅导致多次回调
hasShownMask 标记是否已显示蒙层 避免重复设置蒙层
hasRequestedOptionsForActivation 标记是否已请求过开启设置 避免重复弹设置对话框

这 4 个变量是整个插件稳定运行的核心。它们确保了:

  • 每个操作只执行一次

  • 不会因为重复调用导致状态混乱

  • 系统条件未满足时有明确的降级路径

二、激活流程------handleActivate()

复制代码
private async handleActivate(result: MethodResult): Promise<void> {
  this.isProtectionActive = true;      // 标记激活
  this.hasShownMask = false;           // 重置蒙层标记
  this.emitEvent('ACTIVATE');          // 通知 Flutter

  try {
    await this.ensureSubscription();   // 核心:确保订阅
    result.success(null);
  } catch (err) {
    // 即使出错也返回 success,不影响 Flutter 页面
    result.success(null);
  }
}

关键设计:

  1. 先标记状态,再做异步操作 --- isProtectionActive = trueawait 之前设置

  2. 出错也返回 success --- 防窥是增强功能,出错不应该阻塞 Flutter 页面

  3. emitEvent('ACTIVATE') --- 通知 Flutter 侧防窥已激活

三、ensureSubscription()------完整订阅流程

这是整个插件最复杂的方法:

复制代码
private async ensureSubscription(): Promise<void> {
  // 1. 已订阅则直接返回
  if (this.isSubscribed) {
    return;
  }

  // 2. 检查系统开关是否已开
  const isOpen = await dlpAntiPeep.isDlpAntiPeepSwitchOn();
  if (!isOpen) {
    // 3. 开关未开,检查是否已请求过
    if (this.hasRequestedOptionsForActivation) {
      console.info(TAG, 'Anti-peep option dialog already requested');
      this.emitEvent('REQUEST_SKIPPED', 'dialog_already_requested');
      return;
    }

    // 4. 首次请求开启设置
    this.hasRequestedOptionsForActivation = true;
    this.emitEvent('REQUEST_OPTIONS');
    const requestResult = await dlpAntiPeep.requestAntiPeepOptions(
      this.getAbilityContext()
    );

    // 5. 检查请求结果
    if (
      requestResult !== dlpAntiPeep.AntiPeepOptionsResult.SUCCESS &&
      requestResult !== dlpAntiPeep.AntiPeepOptionsResult.ALREADY_ON
    ) {
      console.info(TAG, `Anti-peep not enabled: ${requestResult}`);
      this.emitEvent('REQUEST_RESULT', `${requestResult}`);
      return;  // 用户拒绝开启,不再重试
    }
    this.emitEvent('REQUEST_RESULT', `${requestResult}`);
  }

  // 6. 订阅系统状态事件
  dlpAntiPeep.on('dlpAntiPeep', this.onStatusChange);
  this.isSubscribed = true;
  this.emitEvent('SUBSCRIBED');

  // 7. 获取当前状态并处理
  try {
    await this.onStatusChange(dlpAntiPeep.getDlpAntiPeepInfo());
  } catch (err) {
    console.error(TAG, `Failed to fetch initial status: ${err.message}`);
  }
}

流程图:

复制代码
ensureSubscription()
  │
  ├─ isSubscribed == true → 直接返回
  │
  ├─ isDlpAntiPeepSwitchOn()
  │   │
  │   ├─ 已开 → 跳到步骤 6
  │   │
  │   └─ 未开
  │       │
  │       ├─ hasRequestedOptionsForActivation == true
  │       │   → REQUEST_SKIPPED → 返回
  │       │
  │       └─ requestAntiPeepOptions()
  │           │
  │           ├─ SUCCESS / ALREADY_ON → 继续
  │           └─ 其他 → REQUEST_RESULT → 返回
  │
  ├─ dlpAntiPeep.on('dlpAntiPeep', onStatusChange)  ← 订阅
  │
  └─ getDlpAntiPeepInfo() → onStatusChange()        ← 获取初始状态

四、状态回调------onStatusChange()

复制代码
private readonly onStatusChange = async (
  status: dlpAntiPeep.DlpAntiPeepStatus
): Promise<void> => {
  // 未激活时不处理
  if (!this.isProtectionActive) {
    return;
  }

  // PASS:恢复正常可见
  if (status === dlpAntiPeep.DlpAntiPeepStatus.PASS) {
    this.hasShownMask = false;       // 重置蒙层标记
    this.emitEvent('PASS');
    return;
  }

  // HIDE:需要隐藏内容
  if (status === dlpAntiPeep.DlpAntiPeepStatus.HIDE) {
    this.emitEvent('HIDE');
    if (!this.hasShownMask) {        // 防止重复设置蒙层
      await this.setMaskLayer();
    }
  }
};

两个状态的处理:

状态 处理 关键逻辑
PASS 恢复可见 重置 hasShownMask,允许下次再设蒙层
HIDE 隐藏内容 发事件 + 设置蒙层(如果还没设过)

hasShownMask 的作用:如果蒙层已经设过了,就不重复设。避免多次调用 setAntiPeepMaskLayer 导致系统异常。

五、蒙层设置------setMaskLayer()

复制代码
private async setMaskLayer(): Promise<void> {
  try {
    const currentWindow = await window.getLastWindow(this.getAbilityContext());
    const windowId = currentWindow.getWindowProperties().id;
    await dlpAntiPeep.setAntiPeepMaskLayer(windowId);
    this.hasShownMask = true;     // 标记已设蒙层
    this.emitEvent('MASK_SHOWN');
  } catch (err) {
    console.error(TAG, `Failed to set mask layer: ${err.message}`);
  }
}

蒙层设置需要两步:

  1. 获取当前窗口的 windowId

  2. 调用 dlpAntiPeep.setAntiPeepMaskLayer(windowId) 设置蒙层

设置成功后标记 hasShownMask = true,防止重复设置。

六、取消订阅------cleanup()

复制代码
private cleanup(): void {
  this.isProtectionActive = false;
  this.hasRequestedOptionsForActivation = false;
  this.hasShownMask = false;

  if (!this.isSubscribed) {
    return;  // 没订阅过就不需要取消
  }

  try {
    dlpAntiPeep.off('dlpAntiPeep', this.onStatusChange);
    console.info(TAG, 'Anti-peep status subscription removed');
  } catch (err) {
    console.error(TAG, `Failed to remove subscription: ${err.message}`);
  } finally {
    this.isSubscribed = false;
  }
}

cleanup 做的事:

  1. 重置所有状态变量

  2. 如果已订阅,取消订阅 dlpAntiPeep.off()

  3. 标记 isSubscribed = false

在两个时机调用:

  • handleDeactivate() --- Flutter 主动取消防窥时

  • onDetachedFromEngine() --- Flutter 引擎销毁时

七、事件回传------emitEvent()

复制代码
private emitEvent(event: string, detail?: string): void {
  if (!this.channel) {
    return;
  }

  const args = new Map<string, Object>();
  args.set('event', event);
  if (detail) {
    args.set('detail', detail);
  }
  this.channel.invokeMethod('onAntiPeepEvent', args);
}

通过 MethodChannel 向 Flutter 回传事件。Flutter 侧接收:

复制代码
// anti_peep_protection_channel.dart

_channel.setMethodCallHandler((call) async {
  if (call.method == 'onAntiPeepEvent') {
    final event = arguments['event'] as String?;
    switch (event) {
      case 'HIDE':
        visibilityState.value = AntiPeepVisibilityState.hidden;
        break;
      case 'PASS':
      case 'DEACTIVATE':
        visibilityState.value = AntiPeepVisibilityState.visible;
        break;
    }
  }
});

Flutter 侧通过 ValueNotifier<AntiPeepVisibilityState> 暴露状态,页面可以 ValueListenableBuilder 监听。

八、和命令型插件的对比

维度 命令型插件(TTS/ASR) 状态型插件(防窥)
调用方式 一次调用,一次结果 持续订阅,状态变化
状态管理 pendingResult 追踪一次命令 4 个布尔变量追踪持续状态
回调模型 onComplete / onStop onStatusChange(PASS/HIDE)
生命周期 speak → 完成 → 结束 activate → 订阅 → 事件流 → deactivate
资源清理 shutdownEngine off() 取消订阅 + cleanup()

防窥插件的复杂度明显高于 TTS 插件,因为它需要处理"状态持续变化"的场景。

关键代码位置

文件 作用
app/ohos/entry/src/main/ets/plugins/AntiPeepProtectionPlugin.ets 鸿蒙防窥插件(本文核心)
app/lib/core/platform/anti_peep_protection_channel.dart Flutter 防窥通道
app/lib/app.dart 页面激活/取消防窥的调用点

代码结构全景图

复制代码
AntiPeepProtectionPlugin
│
├─ 状态变量
│   ├─ isProtectionActive          ← 当前是否激活
│   ├─ isSubscribed                ← 是否已订阅系统事件
│   ├─ hasShownMask                ← 是否已显示蒙层
│   └─ hasRequestedOptionsForActivation ← 是否已请求过开启设置
│
├─ 生命周期
│   ├─ onAttachedToEngine()        ← 创建 channel
│   └─ onDetachedFromEngine()      ← 清空 channel + cleanup()
│
├─ 方法分发
│   └─ onMethodCall()
│       ├─ 'activateCollectionProtection' → handleActivate()
│       └─ 'deactivateCollectionProtection' → handleDeactivate()
│
├─ handleActivate()
│   ├─ 标记 isProtectionActive = true
│   └─ ensureSubscription()
│       ├─ 已订阅 → 返回
│       ├─ isDlpAntiPeepSwitchOn()
│       │   ├─ 未开 → requestAntiPeepOptions()
│       │   └─ 已开 → 继续
│       ├─ dlpAntiPeep.on('dlpAntiPeep', onStatusChange)  ← 订阅
│       └─ getDlpAntiPeepInfo() → onStatusChange()        ← 初始状态
│
├─ handleDeactivate()
│   ├─ emitEvent('DEACTIVATE')
│   └─ cleanup()
│
├─ onStatusChange(status)
│   ├─ PASS → 重置 hasShownMask + emitEvent('PASS')
│   └─ HIDE → emitEvent('HIDE') + setMaskLayer()
│
├─ setMaskLayer()
│   └─ window.getLastWindow() → dlpAntiPeep.setAntiPeepMaskLayer()
│
├─ cleanup()
│   ├─ 重置所有状态变量
│   └─ dlpAntiPeep.off('dlpAntiPeep', onStatusChange)
│
└─ emitEvent(event, detail?)
    └─ channel.invokeMethod('onAntiPeepEvent', args)

常见坑

  • 没检查系统开关就直接订阅 --- 系统开关未开时订阅会失败

  • 页面离开后不取消订阅 --- 事件继续触发,状态混乱

  • 已经显示过蒙层还重复设置 --- 系统可能异常,用 hasShownMask 防重复

  • 只发日志不把状态回推给 Flutter --- Flutter 侧无法感知状态变化

  • cleanup 时不重置状态变量 --- 下次激活时状态残留

  • handleActivate 出错时抛异常 --- Flutter 页面会卡死,应该 catch 后返回 success

  • requestAntiPeepOptions 失败后还重复弹 --- 用 hasRequestedOptionsForActivation 防重复

可复用模板

状态型插件模板

复制代码
export class StatePlugin implements FlutterPlugin, MethodCallHandler {
  private channel: MethodChannel | null = null;
  private isActive: boolean = false;
  private isSubscribed: boolean = false;

  onMethodCall(call: MethodCall, result: MethodResult): void {
    switch (call.method) {
      case 'activate': this.handleActivate(result); break;
      case 'deactivate': this.handleDeactivate(result); break;
    }
  }

  private async handleActivate(result: MethodResult): Promise<void> {
    this.isActive = true;
    try {
      await this.ensureSubscription();
      result.success(null);
    } catch (err) {
      result.success(null);  // 出错也返回 success
    }
  }

  private handleDeactivate(result: MethodResult): void {
    this.cleanup();
    result.success(null);
  }

  private async ensureSubscription(): Promise<void> {
    if (this.isSubscribed) return;
    // 检查系统条件 → 订阅事件 → 获取初始状态
    systemApi.on('event', this.onStatusChange);
    this.isSubscribed = true;
  }

  private onStatusChange(status: Status): void {
    if (!this.isActive) return;
    this.emitEvent(status);
  }

  private cleanup(): void {
    this.isActive = false;
    if (!this.isSubscribed) return;
    systemApi.off('event', this.onStatusChange);
    this.isSubscribed = false;
  }

  private emitEvent(event: string): void {
    this.channel?.invokeMethod('onEvent', { event });
  }
}

防重复检查清单

复制代码
每个状态型操作必须检查:
  □ 是否已激活(isProtectionActive)?
  □ 是否已订阅(isSubscribed)?
  □ 是否已请求过设置(hasRequestedOptionsForActivation)?
  □ 是否已显示过蒙层(hasShownMask)?
  □ cleanup 时是否重置了所有状态变量?
  □ 页面离开时是否调用了 cleanup?

本篇总结

AntiPeepProtectionPlugin 的难点在于状态持续变化,不在于单次 API 调用。核心设计是:

  1. 4 个防重复状态变量 --- 确保每个操作只执行一次

  2. 完整订阅流程 --- 检查开关 → 请求选项 → 订阅事件 → 获取初始状态

  3. 状态回调区分 PASS/HIDE --- PASS 重置蒙层标记,HIDE 设置蒙层

  4. cleanup 重置所有状态 --- 取消订阅 + 清空标记

  5. 出错不阻塞 Flutter --- handleActivate 出错也返回 success

这也是为什么它必须独立设计,而不是塞进命令型通道里。命令型插件(TTS/ASR)追踪的是一次命令的生命周期;状态型插件(防窥)追踪的是持续状态的变化。两者的复杂度模型完全不同。