拒绝社死!旁边有人偷瞄?教你给App加上鸿蒙系统级“防窥”黑科技!

0 阅读3分钟

拒绝社死!旁边有人偷瞄?手把手教你给App加上鸿蒙系统级“防窥”黑科技!

大家好,我是青蓝逐码的云杰。

随着智能手机和移动应用的普及,用户对隐私安全的关注度越来越高。特别是在一些涉及个人敏感信息(如聊天记录、支付密码、相册照片等)的场景下,如何防止旁人“偷瞄”成为了一个重要的需求。

在鸿蒙系统(HarmonyOS)中,官方为开发者提供了强大的 DeviceSecurityKit(设备安全服务),其中就包含了一项非常实用的功能:防窥保护(DLP Anti-Peep)

今天,我就以我的应用**「取件伙伴」为例,手把手教大家如何优雅地在应用中接入防窥保护功能,并在被旁人窥视时,自动拉起系统级的蒙灰层**遮盖敏感内容。

先来看看最终在「取件伙伴」中的实现效果:

开启防窥保护开关效果图

被旁人偷瞄触发系统级蒙灰层效果图


一、防窥保护是什么?

简单来说,当设备开启了人脸识别并打开了防窥保护开关后,系统会通过传感器(如前置摄像头)智能判断当前注视屏幕的人是否是“机主”。

  • 如果只有机主在看,屏幕正常显示。
  • 如果检测到机主和非机主同时注视屏幕(即被旁人偷瞄),系统就会触发“被窥视”状态。

对于开发者而言,我们可以订阅这个状态,并在被窥视时调用系统 API 拉起一个系统级的蒙层,从而保护用户的隐私数据不被泄露。


二、接入防窥保护的完整流程

要在应用中完整接入防窥保护,通常需要经过以下几个关键步骤:

  1. 权限申请:获取防窥状态所需的系统权限。
  2. 前置条件校验:检查设备是否支持人脸识别,用户是否已录入人脸数据。
  3. 状态检查与引导:检查系统级防窥开关是否打开,若未打开则引导用户去系统设置中开启。
  4. 功能开启与事件订阅:开启应用内开关,订阅防窥状态变化,并在被窥视时拉起蒙层。
  5. 生命周期同步:处理用户在系统设置中撤销权限或关闭开关的情况,确保应用内状态同步。

接下来,我们将结合代码一步步实现。

1. 声明必要的权限

首先,我们需要在项目的 module.json5 文件中声明获取防窥状态的权限 ohos.permission.DLP_GET_HIDE_STATUS

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.DLP_GET_HIDE_STATUS",
        "reason": "$string:dlp_anti_peep_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "inuse"
        }
      }
    ]
  }
}

注:别忘了在 string.json 中配置对应的 reason 文案,例如:“用于在检测到非机主窥视屏幕时保护您的隐私数据”。

2. 前置条件校验:人脸识别与系统开关

在用户点击应用内的“防窥保护”开关时,我们不能直接去申请权限,而是要先检查设备的硬件能力和用户的设置状态

这里我们使用 @kit.UserAuthenticationKit 来检查人脸识别的录入状态。

import { dlpAntiPeep } from '@kit.DeviceSecurityKit';
import { userAuth } from '@kit.UserAuthenticationKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { abilityAccessCtrl } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';

// ... 在 Toggle 组件的 onChange 事件中处理开启逻辑 ...

// 1. 先检查设备是否支持并已录入人脸
try {
  // 调用 getAvailableStatus,如果设备支持但未录入,或根本不支持,会抛出异常
  userAuth.getAvailableStatus(userAuth.UserAuthType.FACE, userAuth.AuthTrustLevel.ATL1);
} catch (error) {
  const err = error as BusinessError;
  // 错误码 12500010: 该类型的凭据没有录入
  if (err.code === 12500010) {
    this.getUIContext().showAlertDialog({
      title: '提示',
      message: '需要在设备开启人脸识别。请先在系统设置中录入人脸数据,再开启防窥保护。',
      // ... 按钮配置 ...
    });
    this.isDlpAntiPeepEnabled = false; // 强制回退开关状态
    return;
  }

  // 错误码 12500005: 认证类型不支持
  if (err.code === 12500005) {
    this.getUIContext().showAlertDialog({
      title: '提示',
      message: '当前设备不支持人脸识别功能,无法开启防窥保护。',
      // ... 按钮配置 ...
    });
    this.isDlpAntiPeepEnabled = false;
    return;
  }
}

人脸识别拦截弹窗

避坑指南: ArkTS 中捕获的错误需要使用 BusinessError 类型进行断言,才能正确获取到 code 属性。直接使用 Error 类型会导致编译报错。

3. 申请权限与引导系统设置

人脸识别校验通过后,我们开始正式申请 ohos.permission.DLP_GET_HIDE_STATUS 权限。

如果权限申请成功,我们还需要检查系统级别的防窥保护开关(位于“设置 > 隐私与安全 > 防窥保护”)是否已经打开。如果没有打开,我们需要弹窗引导用户去开启。

const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
const permissionManager = abilityAccessCtrl.createAtManager();

try {
  const grantStatus = await permissionManager.requestPermissionsFromUser(context, ['ohos.permission.DLP_GET_HIDE_STATUS']);
  
  if (grantStatus.authResults[0] === 0) { // 权限授予成功
    // 检查系统级防窥开关是否开启
    const isSysSwitchOn = await dlpAntiPeep.isDlpAntiPeepSwitchOn();
    
    if (!isSysSwitchOn) {
      // 引导用户去系统设置中开启
      this.getUIContext().showAlertDialog({
        title: '开启防窥保护',
        message: '需要在设备开启人脸识别。在设备上选择“设置 > 隐私与安全 > 防窥保护”,开启防窥保护开关。',
        buttons: [
          {
            value: '去设置',
            action: async () => {
              try {
                // 拉起系统防窥设置页面
                await dlpAntiPeep.requestAntiPeepOptions(context);
              } catch (err) {
                console.error('跳转防窥设置失败', err);
              }
              this.isDlpAntiPeepEnabled = false; // 用户从设置回来后需要重新验证
            }
          }
        ]
      });
      return;
    } else {
      // 一切就绪,正式开启应用内防窥功能
      this.isDlpAntiPeepEnabled = true;
      // 保存用户偏好设置...
      
      // 注册防窥通知(见下文)
      this.registerAntiPeepListener();
    }
  } else {
    // 权限被拒绝
    this.isDlpAntiPeepEnabled = false;
  }
} catch (err) {
  const e = err as BusinessError;
  if (e.code === 801) {
    // 801: Capability not supported (设备不支持防窥能力)
    this.getUIContext().showAlertDialog({
      title: '提示',
      message: '当前设备不支持防窥保护功能'
    });
  }
  this.isDlpAntiPeepEnabled = false;
}

系统级防窥未开启弹窗

4. 核心:订阅状态并拉起系统级蒙层

一切准备就绪后,我们需要订阅防窥状态的变化。当检测到“被窥视”时,获取当前主窗口的 ID,并调用 setAntiPeepMaskLayer 接口拉起系统蒙层。

private registerAntiPeepListener() {
  try {
    dlpAntiPeep.on('dlpAntiPeep', (status) => {
      console.log('防窥状态变化:', status);
      
      // 注意:status 返回的是一个枚举值,1 代表 BE_PEEPED (被窥视)
      if (status === 1) {
        // 获取当前主窗口 ID
        // 假设我们在 EntryAbility 中已将 windowStage 保存到 AppStorage
        const windowStage = AppStorage.get<window.WindowStage>('windowStage');
        const windowId = windowStage?.getMainWindowSync()?.getWindowProperties().id;
        
        if (windowId) {
          // 拉起系统级蒙灰层
          dlpAntiPeep.setAntiPeepMaskLayer(windowId).then(() => {
            console.info('成功拉起系统级蒙灰层');
          }).catch((e: Error) => {
            console.error('拉起系统级蒙灰层失败:', e);
          });
        }
      }
    });
  } catch (e) {
    console.error('订阅防窥通知失败:', e);
  }
}

关键点:如何获取 WindowId? 在鸿蒙开发中,windowId 需要通过 WindowStage 来获取。为了方便全局调用,我们通常会在 EntryAbility.etsonWindowStageCreate 生命周期中将其保存下来:

// EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage): void {
  this.windowStage = windowStage;
  // 保存到全局状态
  AppStorage.setOrCreate<window.WindowStage>('windowStage', windowStage);
  // ...
}

5. 完善生命周期:处理权限和开关的外部变更

用户的操作往往是不可预期的。他们可能会在系统设置中悄悄关闭防窥开关,或者撤销我们应用的权限。为了保证应用状态的一致性,我们需要在应用每次回到前台时(onForeground)进行状态同步。

// EntryAbility.ets

onForeground(): void {
  // ...
  // 延迟检查防窥权限状态,确保应用环境已准备好
  setTimeout(() => {
    this.checkDlpAntiPeepPermissionAndUpdateSetting();
  }, 100);
}

private async checkDlpAntiPeepPermissionAndUpdateSetting(): Promise<void> {
  const isEnabled = preferenceManager.getValueSync('isDlpAntiPeepEnabled', false) as boolean;
  if (!isEnabled) return;

  const permissionManager = abilityAccessCtrl.createAtManager();
  const appInfo = this.context.applicationInfo;
  
  // 1. 检查权限是否还在
  const grantStatus = await permissionManager.checkAccessToken(appInfo.accessTokenId, 'ohos.permission.DLP_GET_HIDE_STATUS');
  
  if (grantStatus !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
    console.warn('检测到防窥保护权限已被撤销,自动关闭防窥保护功能');
    this.disableAntiPeep();
    return;
  }
  
  // 2. 检查系统级开关是否还在
  const isSysSwitchOn = await dlpAntiPeep.isDlpAntiPeepSwitchOn();
  if (!isSysSwitchOn) {
    console.warn('检测到系统级防窥保护已关闭,自动关闭应用内防窥保护功能');
    this.disableAntiPeep();
    return;
  }
  
  // 3. 确保监听器已注册(冷启动恢复等情况)
  this.registerAntiPeepListener();
}

private disableAntiPeep() {
  preferenceManager.setValue('isDlpAntiPeepEnabled', false);
  try {
    dlpAntiPeep.off('dlpAntiPeep');
  } catch (e) {
    const err = e as BusinessError;
    if (err.code !== 801) { // 忽略不支持该能力的报错
      console.error('关闭防窥通知失败', err);
    }
  }
}

同时,在设置页面的 onPageShow 中,也要重新读取本地存储的开关状态,确保 UI 能够及时刷新回弹:

// SettingsPage.ets
onPageShow(): void {
  this.isDlpAntiPeepEnabled = preferenceManager.getValueSync('isDlpAntiPeepEnabled', false);
}

三、踩坑总结

  1. ArkTS 错误处理规范:鸿蒙提供的系统 API 在调用失败(如未录入人脸、设备不支持)时,往往会抛出异常而不是返回错误结果。在 catch 块中,必须将 error 断言为 BusinessError 才能读取 code
  2. Capability not supported (801错误):在模拟器或不支持防窥的旧设备上调用 dlpAntiPeep 相关 API 时,会抛出 801 错误。在业务逻辑中需要捕获并处理该错误,给用户友好的提示,并避免将正常错误日志淹没。
  3. Toggle 组件的双向绑定:当权限被拒绝或前置条件不满足时,我们需要将开关强制回弹到“关闭”状态。此时必须使用 $$this.isDlpAntiPeepEnabled 进行双向绑定,否则 UI 状态可能会卡在“开启”位置。

结语

通过接入 DeviceSecurityKit 提供的防窥保护能力,我们只需寥寥数行代码,就能为应用赋予极具科技感的隐私保护特性。这不仅提升了应用的安全性,也极大地增强了用户的信任感。

希望这篇文章能帮助大家在鸿蒙开发中少走弯路。如果你觉得有用,欢迎点赞分享!如果有任何疑问,也欢迎在评论区交流。