第77篇 | HarmonyOS DLP Anti Peep:防窥状态监听与遮罩效果

第 77 篇讲 DLP Anti Peep。相册和保险箱属于高隐私内容,除了进入前认证,还要考虑用户正在浏览照片时是否被旁人窥屏。HarmonyOS 的 DLP Anti Peep 能力可以监听系统防窥状态,并在检测到风险时设置遮罩层。

双镜记忆相机把防窥能力放在相册详情页里:进入详情时启动防窥监听,页面隐藏时停止;检测到 HIDE 状态时更新遮罩状态,并调用 setAntiPeepMaskLayer。这一篇会把系统开关、事件监听、遮罩设置和 UI 覆盖层一起讲清楚。

本篇目标

  • 理解 DLP Anti Peep 在相册详情页中的启动和停止时机。
  • 掌握 dlpAntiPeep.ongetDlpAntiPeepInfosetAntiPeepMaskLayer 的协作。
  • 理解防窥状态如何同步到页面提示和遮罩层。
  • 学会处理权限缺失、设备不支持和系统开关未开启。

对应源码位置

  • superImage/entry/src/main/ets/pages/Index.ets
  • superImage/entry/src/main/module.json5

防窥不是弹窗,而是浏览过程保护

相册详情页里显示的是用户刚拍下的真实记忆,可能包含地点、人物和前后镜头组合。防窥能力的目标不是阻止用户打开照片,而是在系统判断有窥屏风险时,把当前窗口临时遮挡。

运行层面,用户仍然可以正常进入相册详情。只有系统防窥状态触发后,页面才显示“防偷窥保护中”这样的覆盖层,并配合系统遮罩保护当前窗口。这样体验不会过度打扰,但风险出现时能及时收住内容。

相册详情页承载 DLP Anti Peep 防窥状态提示

相册详情页承载 DLP Anti Peep 防窥状态提示

事件回调只把状态交给统一处理函数

galleryAntiPeepCallback 很短,只把系统回调状态交给 applyGalleryAntiPeepStatus。这种写法让事件入口保持轻量,真正的状态判断、UI 文案和遮罩设置集中在一个函数里。

项目还用 galleryAntiPeepSubscribed 记录是否已经订阅。防窥能力和近场分享一样属于系统监听类能力,必须避免重复订阅和漏清理。这个布尔状态就是后续 start/stop 闭环的基础。

galleryAntiPeepCallback 将系统状态交给统一处理函数

galleryAntiPeepCallback 将系统状态交给统一处理函数

  private readonly galleryAntiPeepCallback = (status: dlpAntiPeep.DlpAntiPeepStatus): void => {
    void this.applyGalleryAntiPeepStatus(status, true);
  };
  private galleryAntiPeepSubscribed: boolean = false;

启动监听前先检查系统开关

startGalleryAntiPeepProtection 会先重置 ready、active 和状态文案,然后调用 isDlpAntiPeepSwitchOn。如果系统防窥未开启,就直接展示状态并返回,不继续订阅事件。

系统开关开启后,函数设置 ready 状态,注册 dlpAntiPeep 事件,并立即读取当前状态。立即读取很重要:如果进入详情页时系统已经处于 HIDE 状态,页面不能等下一次事件才遮挡。

startGalleryAntiPeepProtection 检查系统开关并订阅防窥事件

startGalleryAntiPeepProtection 检查系统开关并订阅防窥事件

  private async startGalleryAntiPeepProtection(): Promise<void> {
    this.galleryAntiPeepReady = false;
    this.galleryAntiPeepActive = false;
    this.galleryAntiPeepStatusText = '';
    try {
      const enabled = await dlpAntiPeep.isDlpAntiPeepSwitchOn();
      if (!enabled) {
        this.galleryAntiPeepStatusText = '系统防窥未开启';
        return;
      }

      this.galleryAntiPeepReady = true;
      this.galleryAntiPeepStatusText = '防窥保护已开启';
      if (!this.galleryAntiPeepSubscribed) {
        dlpAntiPeep.on('dlpAntiPeep', this.galleryAntiPeepCallback);
        this.galleryAntiPeepSubscribed = true;
      }

      const currentStatus = dlpAntiPeep.getDlpAntiPeepInfo();
      await this.applyGalleryAntiPeepStatus(currentStatus, false);
    } catch (error) {
      const err = error as BusinessError;
      this.galleryAntiPeepReady = false;
      this.galleryAntiPeepActive = false;
      this.galleryAntiPeepStatusText = this.getGalleryAntiPeepErrorText(err.code ?? -1);
      console.warn(`Gallery anti-peep unavailable: ${err.code ?? -1} ${err.message ?? ''}`);
    }
  }

停止监听要恢复页面状态

stopGalleryAntiPeepProtection 在页面隐藏或退出详情时调用。它先尝试 off 事件,再把 subscribed、active、ready 和状态文案全部清空。即使 off 失败,也会输出 warning 并继续恢复本地状态。

这一步能避免一个常见问题:用户离开照片详情后,防窥状态仍然影响其他页面。系统能力监听如果没有生命周期边界,会让后续页面出现难以排查的状态残留。

stopGalleryAntiPeepProtection 退出时清理订阅和 UI 状态

stopGalleryAntiPeepProtection 退出时清理订阅和 UI 状态

  private stopGalleryAntiPeepProtection(): void {
    if (this.galleryAntiPeepSubscribed) {
      try {
        dlpAntiPeep.off('dlpAntiPeep', this.galleryAntiPeepCallback);
      } catch (error) {
        const err = error as BusinessError;
        console.warn(`Gallery anti-peep off failed: ${err.code ?? -1} ${err.message ?? ''}`);
      }
      this.galleryAntiPeepSubscribed = false;
    }
    this.galleryAntiPeepActive = false;
    this.galleryAntiPeepReady = false;
    this.galleryAntiPeepStatusText = '';
  }

HIDE 状态触发窗口遮罩

applyGalleryAntiPeepStatus 判断系统状态是否为 HIDE。命中后把 galleryAntiPeepActive 设为 true,更新文案,再调用 showGalleryAntiPeepMaskLayer。遮罩函数拿到当前 windowId 后调用 setAntiPeepMaskLayer

页面 UI 也会根据 galleryAntiPeepActive 展示覆盖层。这样系统窗口遮罩和 ArkUI 页面提示同时生效:系统负责保护内容,页面负责告诉用户当前照片已进入防窥保护状态。

检测到 HIDE 后设置页面状态并调用系统遮罩

检测到 HIDE 后设置页面状态并调用系统遮罩

  private async applyGalleryAntiPeepStatus(
    status: dlpAntiPeep.DlpAntiPeepStatus,
    fromCallback: boolean
  ): Promise<void> {
    this.galleryAntiPeepReady = true;
    if (status === dlpAntiPeep.DlpAntiPeepStatus.HIDE) {
      this.galleryAntiPeepActive = true;
      this.galleryAntiPeepStatusText = '检测到窥屏,已保护';
      await this.showGalleryAntiPeepMaskLayer(fromCallback);
      return;
    }
    this.galleryAntiPeepActive = false;
    this.galleryAntiPeepStatusText = '防窥保护已开启';
  }

  private async showGalleryAntiPeepMaskLayer(fromCallback: boolean): Promise<void> {
    try {
      const currentWindow = await window.getLastWindow(this.getAbilityContext());
      const windowId = currentWindow.getWindowProperties().id;
      await dlpAntiPeep.setAntiPeepMaskLayer(windowId);
    } catch (error) {
      const err = error as BusinessError;
      if (!fromCallback && err.code === 1020600002) {
        this.galleryAntiPeepStatusText = '系统防窥未开启';
      }
      console.warn(`Gallery anti-peep mask failed: ${err.code ?? -1} ${err.message ?? ''}`);
    }
  }

工程检查清单

  • module.json5 中声明 DLP 防窥相关权限。
  • 进入详情页后检查系统防窥开关再订阅事件。
  • 读取初始状态,不能只等待下一次回调。
  • 页面隐藏时取消订阅并清空 active/ready 状态。

今日练习

  1. 关闭系统防窥开关后进入详情页,观察状态文案。
  2. 临时把状态模拟为 HIDE,确认覆盖层和遮罩函数都会触发。
  3. 从详情页返回相册,检查防窥状态是否被清空。

训练营里的每一篇都建议按同一个节奏复盘:先看页面行为,再回到源码定位状态和服务层,最后自己改一个很小的参数验证结果。这样写文章时不会停留在 API 名词,读者也能沿着真实工程把功能跑通。

Logo

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

更多推荐