HarmonyOS 隐私防窥保护开发指南:给 App 加上系统级「防偷瞄」盾牌
本文介绍了HarmonyOS 6.1引入的DLP Anti-Peep(防窥保护)功能,该系统级隐私保护能力通过前置摄像头和人脸识别技术,自动检测屏幕前是否存在机主与非机主同时注视的情况。当检测到被窥视时,系统会触发蒙层遮盖窗口内容,无需开发者自行实现复杂的摄像头检测方案。文章详细说明了防窥保护的工作原理、两种典型场景(机主独自看屏和被窥视状态)、使用前提条件以及完整的API接口说明,包括状态监听、
一、背景与问题
你一定有过这样的经历——在地铁上、咖啡厅里打开银行 App 查账单,或者正在浏览私密聊天记录,突然感觉到旁边有人在往你的屏幕上看。那一刻的紧张感,不是"关掉手机"就能完全消除的。
在 API 20 之前,如果开发者想要实现"有人看屏幕就自动遮盖内容",基本只有两条路:
- 自己做摄像头检测方案:通过调用前置摄像头做人脸检测,判断屏幕前有几个人。但这不仅开发成本高,而且存在严重的隐私合规风险——你凭什么调用户的摄像头?
- 手动添加隐私开关:让用户自己判断什么时候需要隐藏,体验上完全依赖用户自觉。
这两条路都不理想。HarmonyOS 6.1 引入了 Device Security Kit 的 DLP Anti-Peep(防窥保护)能力,彻底改变了这个局面。系统层面通过前置摄像头和已录入的人脸数据,自动识别机主与非机主是否同时注视屏幕,一旦检测到被窥视,应用可以拉起系统级蒙灰层直接遮盖窗口——不需要自己调摄像头,不需要自己做人脸算法,一切由系统底层完成。
💡 核心价值:防窥保护解决的不是"谁能进入App"(那是身份验证的事),而是**“进入后旁边有没有人在看”**。两者是互补关系,建议组合使用。
二、核心概念
2.1 DLP Anti-Peep 是什么?
DLP Anti-Peep(Data Leakage Prevention Anti-Peeping,防数据泄露防窥保护)是 @kit.DeviceSecurityKit 提供的一项系统级隐私保护能力。它的工作原理是:
┌─────────────────────────────────────────────────────┐
│ 设备硬件层 │
│ 前置摄像头 + 已录入人脸数据 + 人脸识别引擎 │
└──────────────────────┬──────────────────────────────┘
│ 实时检测注视状态
▼
┌─────────────────────────────────────────────────────┐
│ Device Security Kit │
│ dlpAntiPeep 模块(开发者接口层) │
│ │
│ ┌──────────┐ ┌───────────┐ ┌────────────────┐ │
│ │ 状态监听 │ │ 状态查询 │ │ 蒙层控制 │ │
│ │ on/off │ │ getInfo │ │ setMaskLayer │ │
│ └──────────┘ └───────────┘ └────────────────┘ │
└──────────────────────┬──────────────────────────────┘
│ 回调通知 / API 调用
▼
┌─────────────────────────────────────────────────────┐
│ HAP 应用层 │
│ 你的 App 在此接收状态并执行保护动作 │
└─────────────────────────────────────────────────────┘
2.2 两种窥视场景
| 场景 | 说明 | 系统判定 |
|---|---|---|
| 机主独自看屏 | 只有设备机主一个人在看屏幕 | PASS — 正常展示 |
| 机主+他人同时看屏 | 机主和非机主同时注视同一屏幕 | HIDE — 需要触发保护 |
系统通过前置摄像头实时检测,只有当机主和非机主的面部同时出现在摄像头视野内且都朝向屏幕时,才会判定为被窥视状态。误触率经过大量工程优化,日常使用中几乎不会出现"明明没人看我却被遮挡"的情况。
2.3 前置条件
防窥功能不是所有设备都能用,需要满足以下硬性前提:
| 前置条件 | 说明 | 如何校验 |
|---|---|---|
| 设备支持人脸识别 | 必须有前置摄像头且支持人脸识别能力 | userAuth.getAvailableStatus(FACE) → 错误码 12500005 |
| 用户已录入人脸数据 | 至少录入了一组人脸用于身份比对 | userAuth.getAvailableStatus(FACE) → 错误码 12500010 |
| 系统防窥开关已开启 | 用户在「设置 > 隐私与安全 > 防窥保护」中打开了开关 | dlpAntiPeep.isDlpAntiPeepSwitchOn() |
| 应用获得受限权限 | 声明并获得 ohos.permission.DLP_GET_HIDE_STATUS 权限 |
abilityAccessCtrl.checkAccessToken() |
三、完整工作流程(时序图)
下面这张图展示了从用户开启防窥到最终释放资源的完整交互流程。理解它是正确接入的前提:

📌 上图说明:整个流程分为 7 个关键步骤:
- 开启开关:用户在 App 内点击防窥保护开关
- 查询状态:App 向 Device Security Kit 查询当前应用的防窥保护是否已开启
- 注册通知:订阅防窥状态变化回调(此时开始实时监听)
- 3.1 机主独自看屏 → 回调返回非窥视状态
- 3.2 机主+他人同时看屏 → 回调返回被窥视状态
- 查询快照:同步获取当前窥视状态的即时值(兜底机制)
- 拉起蒙层:当状态为被窥视时,调用接口拉起系统级灰色蒙层覆盖窗口
- 状态恢复:窥视者离开后,状态恢复为非窥视,蒙层自动消失
- 解注册:离开敏感页面或关闭功能时,取消监听释放资源
四、接口说明
4.1 模块导入
import { dlpAntiPeep } from '@kit.DeviceSecurityKit';
4.2 核心接口一览
| 接口/属性 | 类型 | 说明 | API Level |
|---|---|---|---|
isDlpAntiPeepSwitchOn() |
() => Promise<boolean> |
查询当前应用的系统级防窥保护开关是否已开启 | API 20+ |
requestAntiPeepOptions(context) |
(context: UIAbilityContext) => Promise<AntiPeepOptionsResult> |
拉起系统设置弹窗,引导用户为当前应用开启防窥保护 | API 20+ |
on('dlpAntiPeep', callback) |
(callback: (status: DlpAntiPeepStatus) => void) => void |
注册防窥状态变化监听 | API 20+ |
off('dlpAntiPeep') |
() => void |
取消防窥状态变化监听 | API 20+ |
getDlpAntiPeepInfo() |
() => DlpAntiPeepStatus |
同步获取当前防窥状态的快照(非回调方式) | API 20+ |
setAntiPeepMaskLayer(windowId) |
(windowId: number) => Promise<void> |
在指定窗口拉起系统级蒙灰层 | API 20+ |
4.3 枚举类型
DlpAntiPeepStatus(防窥状态枚举)
| 枚举值 | 含义 | 触发条件 |
|---|---|---|
PASS |
非窥视状态 | 只有机主一人在看屏幕,或无人看屏幕 |
HIDE |
被窥视状态 | 检测到机主和非机主同时注视屏幕 |
AntiPeepOptionsResult(引导设置结果枚举)
| 枚举值 | 含义 |
|---|---|
SUCCESS |
用户在弹窗中成功开启了防窥保护 |
ALREADY_ON |
当前应用的防窥保护之前已经处于开启状态 |
FAIL |
用户未在弹窗中开启(取消了操作) |
4.4 所需权限
// entry/src/main/module.json5
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.DLP_GET_HIDE_STATUS",
"reason": "$string:permission_anti_peep_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
⚠️ 注意:
ohos.permission.DLP_GET_HIDE_STATUS是受限权限,不能只声明就使用。发布前需按官方流程完成受限权限申请,并在隐私政策中明确说明使用目的。
对应的多语言文案(resources/base/element/string.json):
{
"string": [
{
"name": "permission_anti_peep_reason",
"value": "用于在检测到非机主窥视屏幕时,拉起系统级蒙层保护您的隐私内容"
}
]
}
4.5 错误码速查表
| 错误码 | 含义 | 触发场景 | 处理建议 |
|---|---|---|---|
201 |
未获得受限权限 | 权限申请未通过或被用户拒绝 | 引导用户授权或提示暂不可用 |
801 |
设备不支持 | 模拟器或不支持防窥的旧设备 | 友好提示"当前设备不支持" |
12500005 |
认证类型不支持 | 设备无前置摄像头或不支持人脸识别 | 提示"设备不支持该功能" |
12500010 |
人脸数据未录入 | 支持人脸但用户从未录入过 | 引导去设置页录入人脸 |
1020600001 |
窗口状态异常 | 页面刚切入时窗口尚未就绪 | 可自动重试(最多3次) |
1020600002 |
窗口状态异常 | 同上 | 可自动重试(最多3次) |
1020600003 |
窗口状态异常 | 同上 | 可自动重试(最多3次) |
1020600004 |
人脸未录入 | 与 12500010 类似的另一种表现 | 提示用户先录入人脸 |
五、使用场景
场景一:金融/支付类 App —— 交易详情页防偷窥
业务背景:用户在查看银行卡余额、交易记录、付款二维码等高度敏感信息时,如果在公共场所,旁边的陌生人可能窥见关键信息。
为什么选防窥保护:传统的"进入需输入密码"只能验证身份,无法防止用户已登录后旁人的视线。防窥保护填补了这一空白——登录后仍然持续保护。
预期效果:当用户在余额页面查看时,一旦检测到旁边有人在看屏幕,系统立即弹出一个不透明的灰色蒙层覆盖整个窗口;窥视者移开视线后蒙层自动消失,用户体验丝滑无感。
场景二:社交/IM 类 App —— 私密聊天消息防泄露
业务背景:微信/QQ类应用中的私密对话、语音消息文字转写、图片消息等,在办公室、地铁等环境中容易被同事或路人看到。
为什么选防窥保护:消息类 App 不可能每次查看消息都要求重新验证身份,那样会彻底破坏使用体验。防窥保护提供了无感的持续守护。
预期效果:用户打开某个私密会话窗口后,防窥功能自动激活;有人靠近时蒙层瞬间遮盖,人走开后自动恢复显示。
场景三:企业办公/笔记类 App —— 敏感文档防窥
业务背景:企业 OA 系统、私人笔记 App(如有样 AI 科技的《时光旅记》)中的日记、工作计划、商业机密等内容,在咖啡厅办公或出差途中极易被窥视。
为什么选防窥保护:这类内容的敏感度极高但访问频率也高,不可能每次都做二次验证。按文档/笔记本维度启用防窥保护,既保证了安全性又不影响正常使用。
预期效果:用户可以为单个笔记或文件夹开启"防窥模式";进入该页面后自动受保护,退出后自动释放。
六、完整示例
示例一:基础接入 —— 最小可用 Demo
这是一个从零开始的完整示例,展示如何在一个页面中接入防窥保护的最小可行路径:
// 文件:pages/AntiPeepDemoPage.ets
// 功能:演示防窥保护的基础接入流程
// 运行环境:DevEco Studio 5.x,API Level 20+
// 所需权限:ohos.permission.DLP_GET_HIDE_STATUS(受限权限)
import { dlpAntiPeep } from '@kit.DeviceSecurityKit';
import { userAuth } from '@kit.UserAuthenticationKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { abilityAccessCtrl, common } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
@Entry
@Component
struct AntiPeepDemoPage {
// UI 状态
@State private isProtected: boolean = false;
@State private statusText: string = '当前状态:未开启';
@State private logMessages: string[] = [];
// 追加日志(方便调试)
private addLog(msg: string): void {
this.logMessages = [...this.logMessages.slice(-8), `${new Date().toLocaleTimeString()} - ${msg}`];
}
aboutToAppear(): void {
this.addLog('页面初始化完成');
}
// ========== 第一步:前置条件校验 ==========
private async checkPrerequisites(): Promise<boolean> {
try {
// 校验设备是否支持人脸识别 && 用户是否已录入人脸
userAuth.getAvailableStatus(userAuth.UserAuthType.FACE, userAuth.AuthTrustLevel.ATL1);
this.addLog('✅ 前置条件校验通过(支持人脸识别 & 已录入人脸)');
return true;
} catch (error) {
const err = error as BusinessError;
if (err.code === 12500010) {
this.addLog('❌ 前置条件失败:请先在系统设置中录入人脸数据');
} else if (err.code === 12500005) {
this.addLog('❌ 前置条件失败:当前设备不支持人脸识别');
} else {
this.addLog(`❌ 前置条件校验异常:code=${err.code}`);
}
return false;
}
}
// ========== 第二步:申请权限 + 检查系统开关 ==========
private async requestPermissionAndCheckSwitch(): Promise<boolean> {
const context = getContext(this) as common.UIAbilityContext;
const atManager = abilityAccessCtrl.createAtManager();
try {
// 动态申请权限
const grantResult = await atManager.requestPermissionsFromUser(
context,
['ohos.permission.DLP_GET_HIDE_STATUS']
);
if (grantResult.authResults[0] !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
this.addLog('❌ 用户拒绝了防窥保护权限');
return false;
}
this.addLog('✅ 权限授予成功');
// 检查系统级防窥开关是否已开启
const isSwitchOn = await dlpAntiPeep.isDlpAntiPeepSwitchOn();
if (!isSwitchOn) {
this.addLog('⚠️ 系统防窥开关未开启,尝试引导用户...');
// 引导用户跳转到系统设置
await dlpAntiPeep.requestAntiPeepOptions(context);
this.addLog('已弹出系统设置引导');
// 注意:此处应等待用户操作完成后再次检查
return false;
}
this.addLog('✅ 系统防窥开关已开启');
return true;
} catch (error) {
const err = error as BusinessError;
if (err.code === 801) {
this.addLog('❌ 当前设备不支持防窥保护功能');
} else {
this.addLog(`❌ 权限/开关检查失败:code=${err.code}`);
}
return false;
}
}
// ========== 第三步:注册防窥监听 ==========
private registerAntiPeepListener(): void {
try {
dlpAntiPeep.on('dlpAntiPeep', (status: dlpAntiPeep.DlpAntiPeepStatus) => {
if (status === dlpAntiPeep.DlpAntiPeepStatus.HIDE) {
this.statusText = '🚨 当前状态:检测到被窥视!';
this.addLog('🛡️ 收到 HIDE 回调 → 准备拉起蒙层...');
this.triggerMaskLayer();
} else {
this.statusText = '✅ 当前状态:安全(非窥视)';
this.addLog('✅ 收到 PASS 回调 → 正常显示');
}
});
this.addLog('✅ 防窥状态监听已注册');
// 兜底:立即同步一次状态快照
this.syncStatusSnapshot();
} catch (error) {
const err = error as BusinessError;
this.addLog(`❌ 注册监听失败:code=${err.code}, msg=${err.message}`);
}
}
// ========== 第四步:同步状态快照(兜底机制)==========
private syncStatusSnapshot(): void {
try {
const status = dlpAntiPeep.getDlpAntiPeepInfo();
this.addLog(`📸 状态快照:${status === dlpAntiPeep.DlpAntiPeepStatus.HIDE ? 'HIDE(被窥视)' : 'PASS(安全)'}`);
if (status === dlpAntiPeep.DlpAntiPeepStatus.HIDE) {
this.triggerMaskLayer();
}
} catch (error) {
const err = error as BusinessError;
this.addLog(`⚠️ 获取状态快照失败:code=${err.code}`);
}
}
// ========== 第五步:拉起系统级蒙层 ==========
private async triggerMaskLayer(): Promise<void> {
try {
const windowStage = AppStorage.get<window.WindowStage>('windowStage');
const windowId = windowStage?.getMainWindowSync()?.getWindowProperties().id;
if (!windowId) {
this.addLog('❌ 无法获取 windowId');
return;
}
await dlpAntiPeep.setAntiPeepMaskLayer(windowId);
this.addLog('✅ 系统级蒙灰层已成功拉起!');
} catch (error) {
const err = error as BusinessError;
this.addLog(`❌ 拉起蒙层失败:code=${err.code}, msg=${err.message}`);
}
}
// ========== 第六步:取消防窥(清理资源)==========
private disableAntiPeep(): void {
try {
dlpAntiPeep.off('dlpAntiPeep');
this.addLog('✅ 防窥监听已取消');
} catch (error) {
const err = error as BusinessError;
// off 也可能抛 801(设备不支持),忽略即可
if (err.code !== 801) {
this.addLog(`⚠️ 取消监听异常:code=${err.code}`);
}
}
this.isProtected = false;
this.statusText = '当前状态:未开启';
}
// ========== 开关切换入口 ==========
private async onToggleProtection(enabled: boolean): Promise<void> {
if (enabled) {
// 开启流程
if (!(await this.checkPrerequisites())) {
this.isProtected = false; // 强制回弹
return;
}
if (!(await this.requestPermissionAndCheckSwitch())) {
this.isProtected = false;
return;
}
this.isProtected = true;
this.registerAntiPeepListener();
} else {
// 关闭流程
this.disableAntiPeep();
}
}
build() {
Column({ space: 16 }) {
// 标题区域
Text('🛡️ 防窥保护 Demo')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 8 })
Text('演示 Device Security Kit 的 DLP Anti-Peep 能力')
.fontSize(14)
.fontColor('#666666')
Divider()
// 开关控制
Row() {
Text('启用防窥保护')
.fontSize(16)
Toggle({ type: ToggleType.Switch, isOn: this.isProtected })
.onChange((isOn: boolean) => {
this.onToggleProtection(isOn);
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
// 状态显示
Text(this.statusText)
.fontSize(16)
.fontColor(this.isProtected ? '#007DFF' : '#999999')
.padding(12)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.width('100%')
Divider()
Text('📋 运行日志')
.fontSize(14)
.fontWeight(FontWeight.Medium)
// 日志列表
List({ space: 4 }) {
ForEach(this.logMessages, (log: string) => {
ListItem() {
Text(log)
.fontSize(12)
.fontColor('#444444')
}
})
}
.layoutWeight(1)
.width('100%')
.padding(8)
.backgroundColor('#FAFAFA')
.borderRadius(8)
}
.width('100%')
.height('100%')
.padding(20)
.alignItems(HorizontalAlign.Start)
}
}
运行效果:
- 进入页面后显示一个 Toggle 开关和日志面板
- 点击开启 → 自动完成前置校验 → 权限申请 → 开关检查 → 注册监听的完整链路
- 当检测到被窥视时,日志显示
🚨 当前状态:检测到被窥视!并拉起蒙层 - 点击关闭 → 自动取消监听并清理资源
关键点说明:
| 步骤 | 为什么必须这样做 |
|---|---|
| 前置校验 | 不校验就直接注册监听,会在不支持设备上报 801 异常,体验很差 |
| 权限动态申请 | 受限权限即使用户安装时同意了,仍需运行时二次确认 |
| 状态快照兜底 | 订阅回调是异步的,进入页面瞬间可能还没收到回调但实际已是 HIDE 状态 |
| off 时捕获 801 | 设备不支持的情况下 off 也会报错,不应视为真正异常 |
示例二:进阶用法 —— 封装可复用的防窥 Kit
在生产项目中,建议将防窥能力封装为独立的工具类,实现按需启停、多重保障、自动重试:
// 文件:kit/antipeep/AntiPeepKit.ets
// 功能:生产级防窥保护封装,支持多页面复用
// 设计原则:按需启停 + 回调/快照/轮询三重保障 + 蒙层自动重试
import { Context } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { dlpAntiPeep } from '@kit.DeviceSecurityKit';
export type PeepStatusSource = 'callback' | 'snapshot' | 'poll';
export interface AntiPeepKitOptions {
/** 获取上下文的方法 */
getContext: () => Context | undefined;
/** 消息回调(用于 Toast 提示) */
onMessage?: (message: string) => void;
/** 状态轮询间隔(ms),默认 1500 */
pollIntervalMs?: number;
/** 蒙层重试间隔(ms),默认 300 */
maskRetryDelayMs?: number;
/** 蒙层最大重试次数,默认 3 */
maxMaskRetryCount?: number;
}
export class AntiPeepKit {
private readonly getContext: () => Context | undefined;
private readonly onMessage?: (message: string) => void;
private readonly pollIntervalMs: number;
private readonly maskRetryDelayMs: number;
private readonly maxMaskRetryCount: number;
private active: boolean = false;
private hasShownMask: boolean = false;
private maskRetryCount: number = 0;
private maskRetryTimer: number = -1;
private statusPollTimer: number = -1;
private lastStatus: dlpAntiPeep.DlpAntiPeepStatus = dlpAntiPeep.DlpAntiPeepStatus.PASS;
constructor(options: AntiPeepKitOptions) {
this.getContext = options.getContext;
this.onMessage = options.onMessage;
this.pollIntervalMs = options.pollIntervalMs ?? 1500;
this.maskRetryDelayMs = options.maskRetryDelayMs ?? 300;
this.maxMaskRetryCount = options.maxMaskRetryCount ?? 3;
}
/**
* 确保系统防窥开关已开启
* 如果未开启,自动拉起系统设置引导弹窗
* @returns 是否已就绪
*/
public async ensureSwitchEnabled(): Promise<boolean> {
try {
const isOpen = await dlpAntiPeep.isDlpAntiPeepSwitchOn();
if (isOpen) return true;
const hostContext = this.getContext();
if (hostContext === undefined) return false;
const result = await dlpAntiPeep.requestAntiPeepOptions(hostContext);
if (result === dlpAntiPeep.AntiPeepOptionsResult.SUCCESS ||
result === dlpAntiPeep.AntiPeepOptionsResult.ALREADY_ON) {
return true;
}
this.emitMessage(this.resolveEnableResultMessage(result));
} catch (error) {
const err = error as BusinessError;
console.error(`[AntiPeep] ensureSwitch failed: ${err.code} - ${err.message}`);
this.emitMessage(this.resolveErrorMessage(err));
}
return false;
}
/**
* 更新防窥功能的启用/禁用状态
* 由外部业务层根据场景判断后调用
*/
public async updateEnabled(shouldBeActive: boolean): Promise<void> {
if (shouldBeActive && !this.active) {
await this.activate();
return;
}
if (!shouldBeActive && this.active) {
this.dispose();
}
}
/**
* 页面显示时调用(从后台切回前台)
* 恢复状态快照检查 + 重启轮询
*/
public handlePageShow(): void {
if (!this.active) return;
this.hasShownMask = false; // 重置蒙层标记
void this.syncCurrentStatus('snapshot'); // 立即快照
this.scheduleStatusPolling(); // 启动轮询
}
/**
* 页面销毁或离开敏感页面时调用
* 取消所有监听、清理定时器
*/
public dispose(): void {
this.clearMaskRetry();
this.clearStatusPolling();
this.lastStatus = dlpAntiPeep.DlpAntiPeepStatus.PASS;
this.hasShownMask = false;
if (!this.active) return;
try {
dlpAntiPeep.off('dlpAntiPeep', this.onStatusChange);
this.active = false;
console.info('[AntiPeep] disposed successfully');
} catch (error) {
const err = error as BusinessError;
console.error(`[AntiPeep] off failed: ${err.code} - ${err.message}`);
}
}
// ==================== 内部方法 ====================
/** 状态变更回调 */
private onStatusChange = async (status: dlpAntiPeep.DlpAntiPeepStatus): Promise<void> => {
await this.handleStatus(status, 'callback');
};
/** 激活防窥保护 */
private async activate(): Promise<boolean> {
if (!(await this.ensureSwitchEnabled())) return false;
try {
dlpAntiPeep.on('dlpAntiPeep', this.onStatusChange);
this.active = true;
console.info('[AntiPeep] activated');
// 三重保障:注册后立即快照 + 启动轮询
await this.syncCurrentStatus('snapshot');
this.scheduleStatusPolling();
return true;
} catch (error) {
const err = error as BusinessError;
console.error(`[AntiPeep] activate failed: ${err.code} - ${err.message}`);
this.emitMessage(this.resolveErrorMessage(err));
}
return false;
}
/** 处理状态变化 */
private async handleStatus(
status: dlpAntiPeep.DlpAntiPeepStatus,
source: PeepStatusSource
): Promise<void> {
this.lastStatus = status;
if (status === dlpAntiPeep.DlpAntiPeepStatus.PASS) {
// 安全状态:清除蒙层重试标记
this.clearMaskRetry();
this.hasShownMask = false;
return;
}
if (status === dlpAntiPeep.DlpAntiPeepStatus.HIDE && !this.hasShownMask) {
// 被窥视 且 蒙层还未拉起过 → 触发保护
await this.setMaskLayer();
}
}
/** 同步获取状态快照 */
private async syncCurrentStatus(source: PeepStatusSource = 'snapshot'): Promise<void> {
try {
const status = dlpAntiPeep.getDlpAntiPeepInfo();
console.info(`[AntiPeep] snapshot(${source}): ${status}`);
await this.handleStatus(status, source);
} catch (error) {
const err = error as BusinessError;
console.warn(`[AntiPeep] getDlpAntiPeepInfo failed: ${err.code}`);
}
}
/** 拉起系统级蒙层(含重试机制) */
private async setMaskLayer(): Promise<void> {
try {
const context = this.getContext();
if (context === undefined) return;
const windowClass = await window.getLastWindow(context);
const windowId = windowClass.getWindowProperties().id;
await dlpAntiPeep.setAntiPeepMaskLayer(windowId);
// 成功:清理重试状态
this.clearMaskRetry();
this.hasShownMask = true;
console.info('[AntiPeep] mask layer shown successfully');
} catch (error) {
const err = error as BusinessError;
const errorCode = Number(err.code);
console.error(`[AntiPeep] setMaskLayer failed: ${errorCode}`);
// 窗口状态异常错误码 → 触发自动重试
if ([1020600001, 1020600002, 1020600003].includes(errorCode)) {
this.scheduleMaskRetry();
}
}
}
/** 安排蒙层重试(最多 maxMaskRetryCount 次) */
private scheduleMaskRetry(): void {
if (this.maskRetryCount >= this.maxMaskRetryCount) {
console.warn('[AntiPeep] mask retry exhausted');
return;
}
this.maskRetryCount++;
this.maskRetryTimer = setTimeout(() => {
void this.setMaskLayer();
}, this.maskRetryDelayMs) as unknown as number;
}
/** 清除蒙层重试定时器 */
private clearMaskRetry(): void {
if (this.maskRetryTimer !== -1) {
clearTimeout(this.maskRetryTimer);
this.maskRetryTimer = -1;
}
this.maskRetryCount = 0;
}
/** 启动状态轮询 */
private scheduleStatusPolling(): void {
this.clearStatusPolling();
this.statusPollTimer = setInterval(() => {
void this.syncCurrentStatus('poll');
}, this.pollIntervalMs) as unknown as number;
}
/** 清除状态轮询 */
private clearStatusPolling(): void {
if (this.statusPollTimer !== -1) {
clearInterval(this.statusPollTimer);
this.statusPollTimer = -1;
}
}
/** 发送消息(Toast) */
private emitMessage(message: string): void {
if (this.onMessage) {
this.onMessage(message);
}
}
/** 解析开启结果 */
private static resolveEnableResultMessage(result: dlpAntiPeep.AntiPeepOptionsResult): string {
switch (result) {
case dlpAntiPeep.AntiPeepOptionsResult.FAIL:
return '您取消了防窥保护开启操作';
default:
return '防窥保护开启失败,请稍后再试';
}
}
/** 解析错误码为用户友好提示 */
static resolveErrorMessage(error: BusinessError): string {
const code = Number(error.code);
const map: Record<number, string> = {
201: '当前应用未获得防窥保护受限权限',
801: '当前设备或系统版本暂不支持防窥保护',
1020600004: '请先在系统设置中录入人脸后再开启',
};
return map[code] ?? '防窥保护暂时不可用,请稍后再试';
}
}
关键设计解读:
| 设计决策 | 原因 |
|---|---|
| 三重保障(回调 + 快照 + 轮询) | 回调可能丢失(极端情况),快照弥补进入瞬间的盲区,轮询作为最后防线 |
| 蒙层重试机制 | 页面刚切入时窗口可能未完全就绪,setAntiPeepMaskLayer 会失败,重试 3 次可大幅降低漏保护概率 |
| 按需启停而非全局常驻 | 只在敏感页面激活,离开即释放,节省系统资源 |
| hasShownMask 去重 | 避免 HIDE 状态下反复调用 setAntiPeepMaskLayer 导致闪烁 |
示例三:生命周期集成 —— EntryAbility 全局协调
防窥保护不只是页面级别的事情,还需要在 Ability 层面做状态持久化与恢复:
// 文件:entry/src/main/ets/entryability/EntryAbility.ets
// 功能:WindowStage 保存 + onForeground 权限/开关二次校验
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { dlpAntiPeep } from '@kit.DeviceSecurityKit';
import { abilityAccessCtrl } from '@kit.AbilityKit';
import { preferences } from '@kit.DataArkDataManagementKit';
const TAG = '[EntryAbility]';
export default class EntryAbility extends UIAbility {
private windowStage: window.WindowStage | null = null;
private dataPreferences: preferences.Preferences | null = null;
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(0x0000, TAG, 'Ability onCreate');
}
onDestroy(): void {
hilog.info(0x0000, TAG, 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(0x0000, TAG, 'onWindowStageCreate');
// ⭐ 关键:保存 WindowStage 到全局存储,供防窥等功能获取 windowId
this.windowStage = windowStage;
AppStorage.setOrCreate<window.WindowStage>('windowStage', windowStage);
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, TAG, 'Failed to load content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(0x0000, TAG, 'Succeeded in loading content.');
});
// 初始化偏好存储(用于保存防窥开关状态)
this.initDataPreferences();
}
onWindowStageDestroy(): void {
hilog.info(0x0000, TAG, 'onWindowStageDestroy');
}
onForeground(): void {
hilog.info(0x0000, TAG, 'onForeground');
// ⭐ 关键:从后台回到前台时,延迟检查防窥保护状态
// 用户可能在后台撤销了权限 or 关了系统开关
setTimeout(() => {
this.checkAntiPeepStateOnResume();
}, 100); // 给一点时间让环境准备就绪
}
onBackground(): void {
hilog.info(0x0000, TAG, 'onBackground');
}
// ==================== 防窥相关 ====================
/** 初始化偏好存储 */
private async initDataPreferences(): Promise<void> {
try {
this.dataPreferences = await preferences.getPreferences(getContext(this), 'app_settings');
} catch (e) {
hilog.error(0x0000, TAG, 'initDataPreferences failed');
}
}
/**
* 从后台恢复时的防窥状态校验
*
* 检查三项:
* 1. 应用内的防窥开关是否还是开启状态
* 2. 受限权限是否被撤销
* 3. 系统级防窥开关是否被关闭
*
* 任一项不满足 → 自动禁用防窥功能
*/
private async checkAntiPeepStateOnResume(): Promise<void> {
if (!this.dataPreferences) return;
try {
// 读取用户之前设置的开关状态
const isEnabled = this.dataPreferences.getValueSync('isAntiPeepEnabled', false) as boolean;
if (!isEnabled) return; // 用户本来就没开,无需检查
const atManager = abilityAccessCtrl.createAtManager();
const appInfo = getContext(this).applicationInfo;
// ① 检查权限是否还在
const grantStatus = await atManager.checkAccessToken(
appInfo.accessTokenId,
'ohos.permission.DLP_GET_HIDE_STATUS'
);
if (grantStatus !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
hilog.warn(0x0000, TAG, '防窥权限已被撤销,自动关闭防窥保护');
this.disableAntiPeepGlobally();
return;
}
// ② 检查系统级开关是否还在
const isSysSwitchOn = await dlpAntiPeep.isDlpAntiPeepSwitchOn();
if (!isSysSwitchOn) {
hilog.warn(0x0000, TAG, '系统防窥开关已关闭,自动关闭应用内防窥保护');
this.disableAntiPeepGlobally();
return;
}
// ③ 都正常 → 通知业务页面重新注册监听(通过事件中心或 AppStorage)
hilog.info(0x0000, TAG, '防窥保护状态校验通过 ✅');
AppStorage.setOrCreate<boolean>('antiPeepShouldRecover', true);
} catch (e) {
hilog.error(0x0000, TAG, 'checkAntiPeepStateOnResume exception: %{public}', JSON.stringify(e));
}
}
/** 全局禁用防窥保护 */
private disableAntiPeepGlobally(): void {
if (this.dataPreferences) {
this.dataPreferences.putSync('isAntiPeepEnabled', false);
this.dataPreferences.flush();
}
try {
dlpAntiPeep.off('dlpAntiPeep');
} catch (e) {
// 忽略 801 等非关键错误
}
// 通知 UI 层更新
AppStorage.setOrCreate<boolean>('antiPeepEnabled', false);
}
}
为什么 onForeground 中需要延迟 100ms?
- Ability 从后台恢复到前台是一个异步过程
onForeground回调触发时,窗口可能还在重建中- 过早调用
checkAccessToken或isDlpAntiPeepSwitchOn可能拿到过期缓存 - 100ms 是经验值,足以保证环境稳定,又不会让用户感知到明显延迟
七、常见问题与注意事项
❌ 常见错误一:直接用 Error 接收异常导致编译失败
错误现象:catch (error) 后访问 error.code 编译报错
根本原因:ArkTS 是强类型语言,系统 API 抛出的是 BusinessError(来自 @kit.BasicServicesKit),不是原生 Error。必须显式断言类型才能访问 .code 和 .message 属性。
// ❌ 错误写法
try {
await dlpAntiPeep.isDlpAntiPeepSwitchOn();
} catch (error) {
console.log(error.code); // 编译报错:Property 'code' does not exist on type 'Error'
}
// ✅ 正确写法
try {
await dlpAntiPeep.isDlpAntiPeepSwitchOn();
} catch (error) {
const err = error as BusinessError; // 断言为 BusinessError
console.log(err.code); // ✅ 可以正常访问
}
❌ 常见错误二:Toggle 开关回弹失败
错误现象:用户拒绝权限后,UI 上的 Toggle 开关还停留在"开启"位置
根本原因:没有在权限拒绝/前置条件不满足时强制修改状态变量
// ❌ 错误写法
Toggle({ type: ToggleType.Switch, isOn: this.isEnabled })
.onChange(async (isOn: boolean) => {
if (isOn) {
const ok = await this.requestPermission(); // 可能返回 false
// 这里忘了处理 false 的情况!
}
})
// ✅ 正确写法
Toggle({ type: ToggleType.Switch, isOn: this.isEnabled })
.onChange(async (isOn: boolean) => {
if (isOn) {
const ok = await this.requestPermission();
if (!ok) {
this.isEnabled = false; // ← 关键:强制回弹!
return;
}
}
this.isEnabled = isOn;
})
更优雅的做法是用 $$this 双向绑定配合 @Watch 监听:
@State @Watch('onProtectionChanged') isEnabled: boolean = false;
private onProtectionChanged(): void {
// 在 Watch 里统一处理异步校验逻辑
// 校验不通过时在这里强制改回 false
}
❌ 常见错误三:忘记保存 WindowStage 导致获取 windowId 失败
错误现象:调用 setAntiPeepMaskLayer 时 windowId 为 undefined
根本原因:windowId 需要通过 WindowStage.getMainWindowSync().getWindowProperties().id 获取,而 WindowStage 只在 onWindowStageCreate 回调中能拿到。如果不保存到全局,后续页面就拿不到了。
解决方案:如示例三所示,在 EntryAbility.onWindowStageCreate 中保存:
// EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage): void {
// 保存到 AppStorage,全局可用
AppStorage.setOrCreate<window.WindowStage>('windowStage', windowStage);
}
// 任意页面中获取
const ws = AppStorage.get<window.WindowStage>('windowStage');
const windowId = ws?.getMainWindowSync()?.getWindowProperties().id;
❌ 常见错误四:全局常驻监听导致性能浪费
错误现象:在 EntryAbility.onCreate 中注册了防窥监听,然后从不取消
后果:
- 即使在不需要保护的首页,系统也在持续做人脸检测
- 电量消耗增加
- 可能与其他需要摄像头的功能冲突(如扫码)
正确做法:按需启停
进入敏感页面 → registerAntiPeepListener()
离开敏感页面 → dispose()(取消监听 + 清理定时器)
❌ 常见错误五:off() 时未捕获 801 异常导致崩溃
错误现象:在不支持防窥的设备上调用 dlpAntiPeep.off() 导致未捕获异常
原因:off() 方法本身也会检查设备能力,不支持时会抛出 801 错误
// ❌ 可能崩溃
dlpAntiPeep.off('dlpAntiPeep');
// ✅ 安全做法
try {
dlpAntiPeep.off('dlpAntiPeep');
} catch (e) {
const err = e as BusinessError;
if (err.code !== 801) {
console.error('off failed:', err); // 只记录非 801 的真正错误
}
// 801 直接忽略——设备本来就不支持,off 失败也无所谓
}
八、性能建议
建议 1:只在必要时激活监听
防窥保护依赖前置摄像头进行持续人脸检测,属于高功耗操作。务必遵循以下原则:
| 做法 | 推荐 | 原因 |
|---|---|---|
| 全局常驻监听 | ❌ | 持续消耗 CPU + 摄像头资源 |
| 仅在敏感页面激活 | ✅ | 最小化资源占用 |
| 按"每篇笔记/每个会话"粒度控制 | ✅✅ | 最佳实践,精细化管理 |
建议 2:合理设置轮询参数
如果你使用了示例二中的轮询兜底机制,参数不宜过于激进:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 轮询间隔 | 1500ms | 平衡实时性与性能,不建议低于 1000ms |
| 蒙层重试次数 | 3 次 | 大多数窗口状态问题会在 900ms(3×300ms)内恢复 |
| 蒙层重试间隔 | 300ms | 足够等待窗口状态稳定 |
建议 3:蒙层出现/消失的 UI 适配
系统级蒙层是全窗口覆盖的不透明灰层,你的 App 对此无需额外处理。但需要注意:
- 蒙层的出现/消失是瞬时的,不会产生动画过渡
- 如果你在页面上有定时刷新的内容(如倒计时),蒙层覆盖期间定时器仍在运行
- 建议在收到 HIDE 回调时暂停非必要动画,收到 PASS 后恢复
九、总结
HarmonyOS 6.1 的 DLP Anti-Peep 防窥保护能力填补了移动端隐私安全的最后一公里——它解决的不再是"谁能进入 App"(身份验证),而是**“进入后有没有人在偷看”**。对于金融支付、私密社交、企业笔记等涉及敏感信息的 App 来说,这是一项值得投入的低成本、高回报的安全特性。
快速接入清单:
- 1. module.json5 中声明
ohos.permission.DLP_GET_HIDE_STATUS - 2. EntryAbility.onWindowStageCreate 中保存 WindowStage 到 AppStorage
- 3. 封装防窥工具类(推荐 AntiPeepKit 模式)
- 4. 业务页面按需调用
updateEnabled(true/false) - 5. onPageShow / aboutToDisappear 中分别调用 handlePageShow() / dispose()
- 6. onForeground 中做权限和系统开关的状态恢复校验
- 7. 真机测试(模拟器不支持防窥功能)
- 8. 完成受限权限申请流程 + 更新隐私政策
适用 App 类型:金融/支付、社交/IM、企业协作、健康医疗、笔记/日记等任何包含用户隐私信息的场景。
更多推荐


所有评论(0)