拒绝社死!旁边有人偷瞄?手把手教你给App加上鸿蒙系统级“防窥”黑科技!
大家好,我是青蓝逐码的云杰。
随着智能手机和移动应用的普及,用户对隐私安全的关注度越来越高。特别是在一些涉及个人敏感信息(如聊天记录、支付密码、相册照片等)的场景下,如何防止旁人“偷瞄”成为了一个重要的需求。
在鸿蒙系统(HarmonyOS)中,官方为开发者提供了强大的 DeviceSecurityKit(设备安全服务),其中就包含了一项非常实用的功能:防窥保护(DLP Anti-Peep)。
今天,我就以我的应用**「取件伙伴」为例,手把手教大家如何优雅地在应用中接入防窥保护功能,并在被旁人窥视时,自动拉起系统级的蒙灰层**遮盖敏感内容。
先来看看最终在「取件伙伴」中的实现效果:
一、防窥保护是什么?
简单来说,当设备开启了人脸识别并打开了防窥保护开关后,系统会通过传感器(如前置摄像头)智能判断当前注视屏幕的人是否是“机主”。
- 如果只有机主在看,屏幕正常显示。
- 如果检测到机主和非机主同时注视屏幕(即被旁人偷瞄),系统就会触发“被窥视”状态。
对于开发者而言,我们可以订阅这个状态,并在被窥视时调用系统 API 拉起一个系统级的蒙层,从而保护用户的隐私数据不被泄露。
二、接入防窥保护的完整流程
要在应用中完整接入防窥保护,通常需要经过以下几个关键步骤:
- 权限申请:获取防窥状态所需的系统权限。
- 前置条件校验:检查设备是否支持人脸识别,用户是否已录入人脸数据。
- 状态检查与引导:检查系统级防窥开关是否打开,若未打开则引导用户去系统设置中开启。
- 功能开启与事件订阅:开启应用内开关,订阅防窥状态变化,并在被窥视时拉起蒙层。
- 生命周期同步:处理用户在系统设置中撤销权限或关闭开关的情况,确保应用内状态同步。
接下来,我们将结合代码一步步实现。
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.ets 的 onWindowStageCreate 生命周期中将其保存下来:
// 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);
}
三、踩坑总结
- ArkTS 错误处理规范:鸿蒙提供的系统 API 在调用失败(如未录入人脸、设备不支持)时,往往会抛出异常而不是返回错误结果。在
catch块中,必须将error断言为BusinessError才能读取code。 - Capability not supported (801错误):在模拟器或不支持防窥的旧设备上调用
dlpAntiPeep相关 API 时,会抛出801错误。在业务逻辑中需要捕获并处理该错误,给用户友好的提示,并避免将正常错误日志淹没。 - Toggle 组件的双向绑定:当权限被拒绝或前置条件不满足时,我们需要将开关强制回弹到“关闭”状态。此时必须使用
$$this.isDlpAntiPeepEnabled进行双向绑定,否则 UI 状态可能会卡在“开启”位置。
结语
通过接入 DeviceSecurityKit 提供的防窥保护能力,我们只需寥寥数行代码,就能为应用赋予极具科技感的隐私保护特性。这不仅提升了应用的安全性,也极大地增强了用户的信任感。
希望这篇文章能帮助大家在鸿蒙开发中少走弯路。如果你觉得有用,欢迎点赞分享!如果有任何疑问,也欢迎在评论区交流。