鸿蒙防窥保护的 Flutter ↔ ArkTS 双向事件流:从系统订阅到 Flutter UI 遮罩的完整链路

适合谁看

  • 正在做鸿蒙 Device Security Kit 接入的开发者

  • 想理解 ArkTS 侧系统事件如何推送到 Flutter 侧的开发者

  • 对"防窥保护"这个鸿蒙特色能力感兴趣的 Flutter 开发者

问题背景

防窥保护是鸿蒙系统级能力,它通过摄像头检测用户是否在窥屏。当检测到窥屏时,系统会触发 DlpAntiPeepStatus.HIDE 事件,应用需要在界面上显示遮罩保护隐私内容。

在纯 ArkTS 应用中,可以直接在 UI 层响应这个事件。但在 Flutter 鸿蒙项目中,UI 渲染在 Flutter 侧,系统事件在 ArkTS 侧,需要通过 MethodChannel 做事件桥接。

项目中的真实场景

食界探味在"我的收藏"页面启用了防窥保护。当用户打开收藏页面时,插件激活防窥检测;当用户离开时,插件停用检测。整个流程涉及:

  1. Flutter 页面调用 activateCollectionProtection() 激活

  2. ArkTS 侧订阅 dlpAntiPeep 系统事件

  3. 系统检测到窥屏时,ArkTS 通过 MethodChannel 推送 HIDE 事件

  4. Flutter 侧更新 ValueNotifier,页面通过 ValueListenableBuilder 显示遮罩

  5. 用户离开页面时,Flutter 调用 deactivateCollectionProtection() 停用

核心实现

ArkTS 侧:系统事件订阅与推送

复制代码
// AntiPeepProtectionPlugin.ets
private readonly onStatusChange = async (status: dlpAntiPeep.DlpAntiPeepStatus): Promise<void> => {
  if (!this.isProtectionActive) {
    return;
  }

  if (status === dlpAntiPeep.DlpAntiPeepStatus.PASS) {
    this.hasShownMask = false;
    console.info(TAG, 'Anti-peep status PASS');
    this.emitEvent('PASS');
    return;
  }

  if (status === dlpAntiPeep.DlpAntiPeepStatus.HIDE) {
    console.info(TAG, 'Anti-peep status HIDE');
    this.emitEvent('HIDE');
    if (!this.hasShownMask) {
      await this.setMaskLayer();
    }
  }
};

状态处理逻辑:

  • PASS:用户正在正常使用,清除遮罩标记,推送 PASS 事件

  • HIDE:检测到窥屏,推送 HIDE 事件,并调用 setMaskLayer() 显示系统级遮罩

订阅管理

复制代码
// AntiPeepProtectionPlugin.ets
private async ensureSubscription(): Promise<void> {
  if (this.isSubscribed) {
    return;
  }

  // 1. 检查系统开关是否已开启
  const isOpen = await dlpAntiPeep.isDlpAntiPeepSwitchOn();
  if (!isOpen) {
    // 2. 如果未开启,弹出系统设置弹窗
    if (this.hasRequestedOptionsForActivation) {
      console.info(TAG, 'Anti-peep option dialog already requested');
      this.emitEvent('REQUEST_SKIPPED', 'dialog_already_requested');
      return;
    }

    this.hasRequestedOptionsForActivation = true;
    this.emitEvent('REQUEST_OPTIONS');
    const requestResult = await dlpAntiPeep.requestAntiPeepOptions(this.getAbilityContext());
    if (
      requestResult !== dlpAntiPeep.AntiPeepOptionsResult.SUCCESS &&
      requestResult !== dlpAntiPeep.AntiPeepOptionsResult.ALREADY_ON
    ) {
      console.info(TAG, `Anti-peep not enabled, request result: ${requestResult}`);
      this.emitEvent('REQUEST_RESULT', `${requestResult}`);
      return;
    }
    this.emitEvent('REQUEST_RESULT', `${requestResult}`);
  }

  // 3. 订阅系统事件
  dlpAntiPeep.on('dlpAntiPeep', this.onStatusChange);
  this.isSubscribed = true;
  console.info(TAG, 'Anti-peep status subscription registered');
  this.emitEvent('SUBSCRIBED');

  // 4. 获取初始状态
  try {
    await this.onStatusChange(dlpAntiPeep.getDlpAntiPeepInfo());
  } catch (err) {
    const error = err as BusinessError;
    console.error(TAG, `Failed to fetch initial status: ${error.code}, ${error.message}`);
  }
}

订阅流程的关键点:

  • isDlpAntiPeepSwitchOn 检查系统级开关

  • requestAntiPeepOptions 弹出系统设置弹窗让用户开启

  • hasRequestedOptionsForActivation 防止重复弹窗

  • 订阅后立即获取初始状态,避免遗漏

事件推送:emitEvent

复制代码
// AntiPeepProtectionPlugin.ets
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);
}

事件类型汇总:

事件 触发时机 Flutter 侧处理
ACTIVATE 调用激活方法 记录激活状态
HIDE 检测到窥屏 显示遮罩
PASS 用户正常观看 隐藏遮罩
DEACTIVATE 调用停用方法 隐藏遮罩并重置
REQUEST_OPTIONS 弹出系统设置 记录日志
REQUEST_RESULT 设置结果返回 记录结果
SUBSCRIBED 订阅成功 记录状态
MASK_SHOWN 系统遮罩已显示 记录状态

Flutter 侧:ValueNotifier 驱动 UI 遮罩

复制代码
// anti_peep_protection_channel.dart
enum AntiPeepVisibilityState { visible, hidden }

class AntiPeepProtectionChannel {
  static const _channel = MethodChannel('com.foodvoyage.anti_peep_protection');
  static bool _isInitialized = false;
  static final ValueNotifier<AntiPeepVisibilityState> visibilityState =
      ValueNotifier(AntiPeepVisibilityState.visible);

  static void initialize() {
    if (_isInitialized) return;
    _isInitialized = true;

    _channel.setMethodCallHandler((call) async {
      if (call.method != 'onAntiPeepEvent') {
        AppLogger.info('Anti-peep event received: ${call.method}');
        return;
      }

      final arguments = call.arguments;
      if (arguments is! Map) {
        AppLogger.warning('Anti-peep event payload is invalid: $arguments');
        return;
      }

      final event = arguments['event'] as String? ?? 'unknown';
      final detail = arguments['detail'] as String?;

      switch (event) {
        case 'HIDE':
          visibilityState.value = AntiPeepVisibilityState.hidden;
          break;
        case 'PASS':
        case 'DEACTIVATE':
          visibilityState.value = AntiPeepVisibilityState.visible;
          break;
      }

      if (event == 'HIDE') {
        AppLogger.warning('Anti-peek trigger detected');
      } else {
        AppLogger.info('Anti-peep event: $event');
      }
    });
  }

  static Future<void> activateCollectionProtection() async {
    await _invoke('activateCollectionProtection');
  }

  static Future<void> deactivateCollectionProtection() async {
    await _invoke('deactivateCollectionProtection');
  }

  static Future<void> _invoke(String method) async {
    try {
      await _channel.invokeMethod<void>(method);
    } on MissingPluginException {
      // 非鸿蒙平台,忽略
    } catch (e, stackTrace) {
      AppLogger.warning('Anti-peep channel call failed: $method', e, stackTrace);
    }
  }
}

选择 ValueNotifier 而不是 Stream 的原因:

  • 遮罩状态只有两种(visible/hidden),不需要 Stream 的复杂性

  • ValueNotifier 支持 ValueListenableBuilder,可以直接在 Widget 树中监听

  • 状态变更频率低(只有用户进出页面和窥屏检测时才变化)

  • 不需要 Stream 的取消订阅机制,ValueNotifier 的生命周期跟随应用

Flutter 页面中的遮罩实现

复制代码
// 收藏页面中的防窥遮罩
class CollectionPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ValueListenableBuilder<AntiPeepVisibilityState>(
        valueListenable: AntiPeepProtectionChannel.visibilityState,
        builder: (context, state, child) {
          return Stack(
            children: [
              // 正常内容
              child!,
              // 遮罩层
              if (state == AntiPeepVisibilityState.hidden)
                Container(
                  color: Colors.black,
                  child: Center(
                    child: Icon(
                      Icons.visibility_off,
                      color: Colors.white,
                      size: 64,
                    ),
                  ),
                ),
            ],
          );
        },
        child: _buildCollectionContent(),
      ),
    );
  }
}

关键代码位置

  • app/ohos/entry/src/main/ets/plugins/AntiPeepProtectionPlugin.ets --- ArkTS 侧完整实现

  • app/lib/core/platform/anti_peep_protection_channel.dart --- Flutter 侧事件监听与状态管理

  • 使用防窥保护的 Flutter 页面(收藏页面)

鸿蒙侧实现

鸿蒙侧的工作分为三层:

  1. 系统 API 调用层dlpAntiPeepisDlpAntiPeepSwitchOnrequestAntiPeepOptionson/off 订阅

  2. 状态管理层isProtectionActive(是否激活)、isSubscribed(是否已订阅)、hasShownMask(遮罩是否已显示)

  3. 事件推送层emitEvent 通过 MethodChannel 将事件推送到 Flutter

清理逻辑:

复制代码
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 subscription removed');
  } catch (err) {
    const error = err as BusinessError;
    console.error(TAG, `Failed to remove subscription: ${error.code}, ${error.message}`);
  } finally {
    this.isSubscribed = false;
  }
}

Flutter 侧实现

Flutter 侧的核心设计:

  • ValueNotifier<AntiPeepVisibilityState> 作为全局状态容器

  • initialize() 方法注册 MethodChannel 监听

  • activateCollectionProtection() / deactivateCollectionProtection() 提供 Flutter 页面调用的 API

  • MissingPluginException 静默处理,确保非鸿蒙平台不受影响

常见坑

  • 坑 1:订阅未清理导致内存泄漏dlpAntiPeep.on 注册的回调如果不调用 off 移除,会导致 ArkTS 侧内存泄漏。onDetachedFromEnginecleanup 必须调用 off

  • 坑 2:重复订阅ensureSubscription 中用 isSubscribed 防止重复订阅,但如果有并发调用,可能在 isSubscribed 设置为 true 之前重复订阅。

  • 坑 3:系统设置弹窗重复弹出hasRequestedOptionsForActivation 标记防止重复弹窗,但如果用户拒绝后再次激活,需要重新弹窗。当前实现中这个标记在 cleanup 时重置,所以每次激活都会重新检查。

  • 坑 4:Flutter 侧 MissingPluginException 被静默吞掉 。在非鸿蒙平台(Android/iOS)上调用 activateCollectionProtection 会抛出 MissingPluginException,当前实现静默处理。但如果调用方需要知道是否成功,需要额外的状态反馈。

  • 坑 5:系统遮罩和 Flutter 遮罩的层级关系setAntiPeepMaskLayer 设置的是系统级遮罩,Flutter 侧的 Container 遮罩是应用级遮罩。两者同时存在时,系统遮罩在最上层。

可复用模板

复制代码
// Flutter 侧 - 系统事件监听与状态同步模板
class SystemEventBridge {
  static const _channel = MethodChannel('com.example.system_events');
  static final ValueNotifier<String> latestEvent = ValueNotifier('');
  static bool _initialized = false;

  static void initialize() {
    if (_initialized) return;
    _initialized = true;

    _channel.setMethodCallHandler((call) async {
      if (call.method == 'onSystemEvent') {
        final args = call.arguments;
        if (args is Map) {
          final event = args['event'] as String? ?? '';
          latestEvent.value = event;
        }
      }
    });
  }

  static Future<void> activate() async {
    try {
      await _channel.invokeMethod<void>('activate');
    } on MissingPluginException {}
  }

  static Future<void> deactivate() async {
    try {
      await _channel.invokeMethod<void>('deactivate');
    } on MissingPluginException {}
  }
}

// 鸿蒙侧 - 系统事件订阅与推送模板
export default class SystemEventPlugin implements FlutterPlugin, MethodCallHandler {
  private channel: MethodChannel | null = null;
  private isActive = false;
  private isSubscribed = false;

  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.channel = new MethodChannel(binding.getBinaryMessenger(), 'com.example.system_events');
    this.channel.setMethodCallHandler(this);
  }

  onMethodCall(call: MethodCall, result: MethodResult): void {
    if (call.method === 'activate') {
      this.activate(result);
    } else if (call.method === 'deactivate') {
      this.deactivate(result);
    }
  }

  private async activate(result: MethodResult): Promise<void> {
    this.isActive = true;
    if (!this.isSubscribed) {
      // 订阅系统事件
      systemApi.on('event', this.onEvent);
      this.isSubscribed = true;
    }
    result.success(null);
  }

  private deactivate(result: MethodResult): void {
    this.isActive = false;
    if (this.isSubscribed) {
      systemApi.off('event', this.onEvent);
      this.isSubscribed = false;
    }
    result.success(null);
  }

  private onEvent = (status: number): void => {
    if (!this.isActive || !this.channel) return;
    const args = new Map<string, Object>();
    args.set('event', status.toString());
    this.channel.invokeMethod('onSystemEvent', args);
  };
}

本篇总结

防窥保护的 Flutter ↔ ArkTS 双向事件流,核心链路是:ArkTS 订阅系统事件 → 状态判断 → emitEvent 推送 → Flutter ValueNotifier 更新 → ValueListenableBuilder 驱动 UI 遮罩。这条链路体现了鸿蒙 Flutter 项目中"系统能力在 ArkTS、UI 渲染在 Flutter"的典型分工模式。