欢迎加入 CPF-Flutter 社区,一起交流 Flutter 鸿蒙开发和三方库适配经验。

适配后的仓库地址: https://atomgit.com/oh-flutter/screen_security/tree/ohos-adaptation

我适配的是 screen_security。它原本已经支持 Android 和 iOS,作用也很好理解:进入登录、支付、证件展示这类敏感页面后,应用可以暂时禁止常规截图和录屏;离开页面时再恢复。Android 端用的是 FLAG_SECURE,iOS 端使用安全文本渲染层,但项目里没有鸿蒙实现。

我没有另起炉灶改 Dart API,而是保留原来的 enable()disable(),只在插件底层增加 OHOS 平台代码。最终结果是:关闭防护时页面可以正常被抓取,开启后系统抓到的应用区域会变成黑色;窗口服务里的 isPrivacyMode 也会从 false 变成 true。这两个结果放在一起,才能说明不是按钮只改了页面文案,而是系统窗口的隐私模式真的生效了。

在这里插入图片描述

图 1:Dart API、MethodChannel、ArkTS 插件和系统窗口隐私模式之间的调用关系。

一、先确认这个库值得适配

动手之前,我先在 Flutter 鸿蒙三方库适配清单中做了去重。检查时,screen_security 只出现在待适配清单里,没有出现在“适配中”和“已适配”清单;oh-flutter 组织下也没有同名仓库。这一步很重要,因为功能实现完才发现别人已经提交,前面的时间基本就白花了。

原项目的 Dart 层已经把接口封装好了,调用方只需要写:

final screenSecurity = ScreenSecurity();

await screenSecurity.enable();
await screenSecurity.disable();

再往下看,默认实现通过名为 kidpech_screen_securityMethodChannel 调用原生端,方法名分别是 enableScreenSecuritydisableScreenSecurity。所以鸿蒙端真正要补的是三件事:注册同名通道、接住这两个方法、把开关状态交给鸿蒙窗口 API。

二、本次实测环境

项目本次使用情况
电脑Apple Silicon Mac,arm64
Flutter OH3.41.10-ohos-1.0.1
Dart3.11.5
DevEco Studio26.0.0 Release
HarmonyOS SDKAPI 26
测试手机华为畅享 90 Pro Max,HarmonyOS 7.0.0.105
插件screen_security 1.1.2

项目代码已经放到 AtomGit:

https://atomgit.com/oh-flutter/screen_security/tree/ohos-adaptation

三、让 Flutter 识别 OHOS 插件

第一处改动在 pubspec.yaml。原来只注册了 Android 和 iOS,我增加了 OHOS 平台入口:

flutter:
  plugin:
    platforms:
      android:
        package: com.kidpech.screen_security
        pluginClass: KidpechScreenSecurityPlugin
      ios:
        pluginClass: KidpechScreenSecurityPlugin
      ohos:
        pluginClass: ScreenSecurityPlugin

这里的 pluginClass 必须和 ArkTS 导出的类名一致。少写这一段时,Dart 代码照样能通过静态检查,但运行到鸿蒙设备后找不到插件实现,调用通道就会失败。这类问题看起来像业务方法没写对,实际是插件根本没有注册进引擎。

OHOS 插件还需要自己的工程入口。我新建了 ohos/index.ets,只负责导出插件类:

import ScreenSecurityPlugin from './src/main/ets/components/plugin/ScreenSecurityPlugin';

export default ScreenSecurityPlugin;

同时补齐 oh-package.json5hvigorfile.tsbuild-profile.json5src/main/module.json5。这些文件看起来零碎,但职责很清楚:它们告诉 OHOS 构建系统这是一个 HAR 模块、入口在哪里、由哪个构建插件处理,以及需要什么系统权限。

四、ArkTS 端怎么接住两个方法

核心代码在 ScreenSecurityPlugin.ets。这个类同时实现 FlutterPluginMethodCallHandlerAbilityAware

export default class ScreenSecurityPlugin
  implements FlutterPlugin, MethodCallHandler, AbilityAware {
  private channel: MethodChannel | null = null;
  private abilityContext: common.UIAbilityContext | null = null;

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

  onAttachedToAbility(binding: AbilityPluginBinding): void {
    this.abilityContext =
      binding.getAbility().context as common.UIAbilityContext;
  }
}

onAttachedToEngine 负责建立通道,通道名称必须和 Dart 端一字不差。onAttachedToAbility 则保存当前 UIAbilityContext。之所以需要这个上下文,是因为后面要通过它找到应用当前正在显示的窗口。

生命周期也不能只管“连上”,还要管“断开”。插件离开 Flutter 引擎时要取消方法处理器,离开 Ability 时要清空上下文。否则插件被重新挂载后,旧对象还可能留在内存里,问题不一定马上出现,但调试起来很麻烦。

onDetachedFromEngine(binding: FlutterPluginBinding): void {
  this.channel?.setMethodCallHandler(null);
  this.channel = null;
}

onDetachedFromAbility(): void {
  this.abilityContext = null;
}

两个 Dart 方法来到 ArkTS 后,用一个 switch 分发即可:

onMethodCall(call: MethodCall, result: MethodResult): void {
  switch (call.method) {
    case 'enableScreenSecurity':
      this.setWindowPrivacyMode(true, result);
      break;
    case 'disableScreenSecurity':
      this.setWindowPrivacyMode(false, result);
      break;
    default:
      result.notImplemented();
      break;
  }
}

这里我没有复制两套开关逻辑,而是统一交给 setWindowPrivacyMode。开启和关闭只差一个布尔值,放在一起更不容易出现一边修了、另一边忘记改的情况。

五、真正打开鸿蒙窗口隐私模式

鸿蒙端的关键 API 是 setWindowPrivacyMode。调用前先使用 window.getLastWindow(context) 拿到当前窗口:

private async setWindowPrivacyMode(
  enabled: boolean,
  result: MethodResult,
): Promise<void> {
  const context = this.abilityContext;
  if (context === null) {
    result.error('NO_ABILITY', 'UIAbility is not available', null);
    return;
  }

  try {
    const currentWindow = await window.getLastWindow(context);
    await currentWindow.setWindowPrivacyMode(enabled);
    result.success(null);
  } catch (exception) {
    const error = exception as BusinessError;
    result.error(
      error.code.toString(),
      'Failed to update window privacy mode',
      error.message,
    );
  }
}

这段代码里有两个容易忽略的地方。第一,Ability 还没准备好时不能硬调窗口 API,所以我先判断上下文是否为空,并把 NO_ABILITY 返回给 Dart。第二,系统 API 是异步调用,失败后也不能只在 ArkTS 控制台打印一行日志,否则 Flutter 页面不知道发生了什么。通过 result.error 把错误码和信息传回去,调用方才有机会弹提示或做降级处理。

在这里插入图片描述

图 2:核心实现只有一条主线:获取当前窗口,再按参数打开或关闭隐私模式。

六、权限别漏掉

只写 API 还不够,模块需要声明 ohos.permission.PRIVACY_WINDOW

{
  "module": {
    "name": "screen_security",
    "type": "har",
    "deviceTypes": ["default", "tablet"],
    "requestPermissions": [
      {
        "name": "ohos.permission.PRIVACY_WINDOW"
      }
    ]
  }
}

插件最终会以 HAR 的形式进入示例应用。构建完成后,我直接检查 HAP 内的 module.json,可以看到 ohos.permission.INTERNETohos.permission.PRIVACY_WINDOW 都已经合并进去。这样比只看源码更可靠,因为源码里写了权限,不等于最终安装包里一定有。

在这里插入图片描述

图 3:从构建产物中检查权限,确认 PRIVACY_WINDOW 已进入最终模块配置。

七、先过静态检查和自动化测试

代码写完后,我先在插件根目录执行:

flutter analyze
flutter test

flutter analyze 返回 No issues found,Dart 测试共 26 项,全部通过。测试覆盖了通道名称、开启与关闭的方法调用、无参数调用、原生异常向 Dart 端传递,以及多次开关的调用顺序。

在这里插入图片描述

图 4:静态检查无报错,26 项 Dart 测试全部通过。

接着进入 example 构建 OHOS 调试包:

cd example
flutter build hap --debug --no-codesign

Hvigor 构建成功,生成了 build/ohos/hap/entry-default-unsigned.hap--no-codesign 适合检查代码和工程配置能不能正常编译;真机安装仍然要使用 DevEco Studio 配置过签名的 HAP。不要把“unsigned HAP 构建成功”直接写成“真机验证完成”,这是两回事。

在这里插入图片描述

图 5:OHOS 示例工程完成构建,Hvigor 正常输出 unsigned HAP。

八、真机上怎么判断防护真的生效

我把签名后的调试 HAP 安装到华为畅享 90 Pro Max。应用启动后,红色卡片显示 Screen security is OFF,按钮文字是 Enable Security。这时系统窗口信息中的 isPrivacyModefalse,抓取页面也能看到完整内容。

在这里插入图片描述

图 6:防护关闭时,示例页面可以正常被系统截图工具抓取。

点击 Enable Security 后,Flutter 页面内部会切换到开启状态,同时原生插件把窗口隐私模式设置为 true。我用窗口服务再次查询,得到:

WindowName: flutter_oh_demo0
isPrivacyMode: true

这时候再次通过系统抓图,应用内容区域已经变成黑色。状态栏仍然可见,是因为它属于系统窗口,不是当前 Flutter 应用窗口的一部分。

在这里插入图片描述

图 7:isPrivacyMode: true 后,应用窗口内容被系统抓图自动遮黑。

为了避免只拿一张黑图下结论,我把关闭和开启两次窗口查询放在一起对比:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8:同一窗口在调用前后由 false 变为 true,证明开关已经落到系统窗口层。

测试结束后,我又调用了一次 disable(),确认 isPrivacyMode 回到 false。这个收尾不能省,因为真实业务通常只需要在敏感页面临时开启。如果退出页面后没有恢复,用户在应用其他页面也无法截图,体验会很差。实际接入时可以在进入敏感流程时调用 enable(),离开时在合适的生命周期里调用 disable(),同时注意异常和页面跳转。

九、这次适配里最容易踩的几个坑

1. 通道名和方法名必须完全一致

kidpech_screen_securityenableScreenSecuritydisableScreenSecurity 都沿用原项目定义。这里哪怕只差一个大小写,Flutter 和 ArkTS 也接不上。

2. 不要在拿到 Ability 之前操作窗口

Flutter 引擎注册和 Ability 挂载不是一回事。保存 UIAbilityContext,并在调用前做空值检查,能把启动阶段的偶发问题变成明确错误。

3. 权限要检查最终产物

插件模块声明权限后,还要确认它确实被合并进应用 HAP。只检查 ohos/src/main/module.json5 不够,我更愿意再看一眼构建产物。

4. 开启状态的黑图不是应用崩了

当隐私模式生效后,系统截图看不到应用内容正是预期结果。为了和黑屏、渲染失败区分开,应该同时保留三份证据:开启前的正常页面、应用内部状态变化、窗口服务的 isPrivacyMode: true

5. 它不是万能防泄漏方案

窗口隐私模式可以限制常规系统截图和录屏,但挡不住另一部手机对着屏幕拍,也不能代替权限控制、身份认证、敏感字段脱敏和服务端鉴权。screen_security 更适合作为敏感页面的补充保护,不应该被当成 DRM 或完整的数据防泄漏系统。

十、总结

这次适配没有改动 screen_security 的上层用法,原有 Flutter 代码继续调用 enable()disable()。新增工作集中在 OHOS 插件注册、Ability 生命周期、MethodChannel 方法分发、窗口隐私模式调用和权限声明几个地方。

最后我用四层结果做了确认:Dart 静态检查通过、26 项自动化测试通过、OHOS HAP 构建通过、HarmonyOS API 26 真机上的窗口隐私模式可以在 falsetrue 之间切换。开启后系统抓图中的应用区域变黑,关闭后恢复正常,达到了这次适配的目标。

完整代码:

https://atomgit.com/oh-flutter/screen_security/tree/ohos-adaptation

欢迎加入 CPF-Flutter 社区,查看更多 Flutter 鸿蒙源码、示例和三方库适配进展。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐