适合谁看
-
想读懂防窥插件内部结构的人
-
想写事件型、状态型鸿蒙原生插件的人
-
想看一个比命令型插件更复杂的例子的人
问题背景
防窥保护的复杂度不在 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);
}
}
关键设计:
-
先标记状态,再做异步操作 ---
isProtectionActive = true在await之前设置 -
出错也返回 success --- 防窥是增强功能,出错不应该阻塞 Flutter 页面
-
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}`);
}
}
蒙层设置需要两步:
-
获取当前窗口的
windowId -
调用
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 做的事:
-
重置所有状态变量
-
如果已订阅,取消订阅
dlpAntiPeep.off() -
标记
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 调用。核心设计是:
-
4 个防重复状态变量 --- 确保每个操作只执行一次
-
完整订阅流程 --- 检查开关 → 请求选项 → 订阅事件 → 获取初始状态
-
状态回调区分 PASS/HIDE --- PASS 重置蒙层标记,HIDE 设置蒙层
-
cleanup 重置所有状态 --- 取消订阅 + 清空标记
-
出错不阻塞 Flutter --- handleActivate 出错也返回 success
这也是为什么它必须独立设计,而不是塞进命令型通道里。命令型插件(TTS/ASR)追踪的是一次命令的生命周期;状态型插件(防窥)追踪的是持续状态的变化。两者的复杂度模型完全不同。
