Ohhnews

分类导航

$ cd ..
foojay原文

Codename One发布VoIP与VPN原生API,构建器自动完成平台集成

#codename one#voip#vpn#跨平台开发#原生集成

2019年,一位读者询问我们的 WhatsApp 克隆是否包含 VoIP。我说没有。那需要原生集成,而当时的配置对那个项目来说太重了。多年来,这就是我们的界限。

[LOADING...]

Codename One 是什么? Codename One 是一个开源框架,可用同一套 Java 或 Kotlin 代码库构建原生 iOS、Android、桌面和 Web 应用。了解更多请访问 codenameone.com。

VPN 也位于同一条线之后。这些功能并不整齐地存在于应用进程内部。在 iOS 上,VoIP 推送可能在应用安装 Java 监听器之前到达,但 CallKit 期望来电在其截止时间前到达锁屏。Call Directory 集成是一个单独的扩展可执行文件,有自己的生命周期和签名身份。托管 VPN 会增加 Network Extension 权益和配置文件规则。Android 通过 ConnectionService、VpnManager、VpnService、清单声明和前台服务规则来解决同样的任务。

用 Java 包装这些 API 只是小部分。跨平台运行时无法在安装前添加服务、在应用就绪前响应原生回调,也无法在应用包内创建并签名另一个可执行文件。一旦每个团队都必须在 Xcode 和 Android Studio 中维护这些部分,共享抽象就在最难的地方止步了。

Codename One 针对这个问题有一件秘密武器:构建器。

构建器已经负责组装原生产物,因此它们可以根据应用实际使用的 Java 包来改变其形态。它们链接框架、注入权限和后台模式、生成原生委托和服务,并在需要时创建扩展目标。不引入某个包,其原生机制也不会被引入。

PR #5604 就是我们跨越那条旧界限的地方。它引入了一等公民的呼叫管理、VoIP 推送处理、托管 VPN,以及数据包隧道 API 和 Android 宿主。PR #5693 通过生成单独翻译并签名的 Network Extension 补全了 iOS 路径。Java API 是可见的结果。更大的变化在于,构建器围绕它组装系统集成,而无需让每个应用团队都成为两个原生构建项目的维护者。

简要说明

  • Calls 将应用呼叫状态连接到 iOS 上的 CallKit 和 Android 上自管理的 ConnectionService。你的媒体和信令栈仍由你掌控。
  • Vpn 在 iOS 和 Android 上安装并控制托管 IKEv2 配置文件。iOS 还支持托管 IPsec 配置文件。
  • VpnTunnel 在 Android 和 iOS 上为应用实现的隧道暴露原始数据包。iOS 构建器工作 会生成并签名一个单独的 Network Extension,其中包含翻译后的 Java 隧道。
  • 对话框和次级窗口 现在可以协同工作,包括可选启用的原生模态窗口。
  • 新的 AppKit 移植 构建真正的 Mac 应用,而不是通过 Catalyst 呈现 iOS 应用。
  • 短信一次性验证码自动填充 可填充验证码,而无需请求读取收件箱。
  • 每日平台观察 现在将 Apple 和 Google 的通知转化为源码级检查,在截止日期变成火灾之前采取行动。
  • 新的联系人选择器 返回所选联系人数据,而无需广泛的通讯录访问权限。

呼叫首先是一个系统会话,其次才是媒体会话

VoIP 应用有两条时间线。其信令服务器和媒体引擎有一条,操作系统有另一条。在 iOS 上,传入的 VoIP 推送必须立即报告给 CallKit。此时 Java 应用可能尚未运行。在 Android 上,即使应用自己的界面在别处,自管理连接也会参与系统呼叫路由和音频焦点。

新 API 将这些职责分开:

if (!Calls.isSupported()) {
    return;
}

Calls.configure(new CallConfiguration()
        .displayName("Acme Talk"));

Calls.addActionListener(new CallActionAdapter() {
    public void answerRequested(String callId, CallAction action) {
        signalling.accept(callId);
    }

    public void audioSessionActivated(CallAudioSession session) {
        media.start(session.getCallId());
    }

    public void providerReset() {
        media.stopEverything();
    }
});

Calls 不选择编解码器、不移动音频数据包,也不发明信令协议。它负责通往系统呼叫 UI 和生命周期的桥梁。应用只在原生音频会话激活后才启动媒体,而不是仅仅因为用户点击了“接听”。

包边界也有意义。com.codename1.call.session 添加核心系统呼叫集成。com.codename1.call.voip 添加 PushKit 和 iOS VoIP 后台模式。com.codename1.call.directory 添加目录集成,而不会悄悄引入自管理呼叫权限。Apple 可能拒绝声明了 VoIP 后台执行却不使用它的应用,因此引入更少代码就必须构建更少原生机制。

sequenceDiagram
    participant Push as Push service
    participant Native as Native call bridge
    participant OS as System call UI
    participant Java as Codename One app
    participant Media as App media engine
    Push->>Native: Incoming call payload
    Native->>OS: Report call before deadline
    OS->>Java: answerRequested(callId)
    Java->>Java: Accept through signaling server
    OS->>Java: audioSessionActivated(callId)
    Java->>Media: Start media

模拟器提供确定性的呼叫和音频会话事件。这样,在构建产物到达手机之前,就可以测试那些别扭的顺序。

托管 VPN 及一个重要的平台边界

许多企业应用并不实现 VPN 协议。它们安装操作系统配置文件,并请求平台连接它。这条路径现在有一个 Java API:

if (!Vpn.isSupported()) {
    return;
}

VpnProfile profile = new VpnProfile("vpn.example.com")
        .protocol(VpnProtocol.IKEV2)
        .remoteIdentifier("vpn.example.com")
        .localIdentifier("alice")
        .usernamePassword("alice", secret)
        .displayName("Acme Corporate");

Vpn.install(profile).onResult((ok, err) -> {
    if (err == null) {
        Vpn.start();
    }
});

iOS 和 Android 11 或更高版本支持托管 IKEv2。iOS 还支持使用预共享密钥的托管 IPsec。操作系统负责用户同意、凭据存储和活动配置。该 API 暴露这些边界,而不是把 VPN 密码存储在应用偏好设置中。

数据包隧道边界

对于不适合托管 VPN 配置文件的工作,VpnTunnel 让应用代码检查原始 IP 数据包,并决定转发还是丢弃它们:

public final class LocalTunnel extends VpnTunnel {
    public LocalTunnel() {
    }

    protected void onStart(TunnelConfiguration configuration) {
    }

    protected void onPacket(PacketBuffer packet) {
        if (packet.getFamily() == PacketBuffer.FAMILY_IPV4) {
            forward(packet);
        }
    }

    protected void onStop(TunnelStopReason reason) {
    }
}

同一个 Java 数据包循环在 Android 和 iOS 上运行,但进程边界不同。Android 将它托管在应用进程内的 VpnService 中。iOS 需要 NEPacketTunnelProvider Network Extension:一个单独的可执行文件,拥有自己的 bundle identifier、配置文件、内存限制和翻译后的 VM。PR #5693 中的构建器工作会创建该目标,从选定的 VpnTunnel 类而不是 UIKit 应用外壳进行翻译,添加框架和权益,并将签名后的扩展嵌入应用中。

iOS 扩展包含翻译后的隧道和 VM,但不包含 Codename One 网络栈。它可以在设备上检查、改写、丢弃和 forward 数据包。它不能通过 com.codename1.io.Socket、ConnectionRequest 或 ParparVM 的 java.net 打开远程连接。Android 可以中继数据包,因为其隧道运行在应用进程中。需要远程中继的协议应将这种传输放在平台特定接口之后,而不是假设两个宿主完全相同。

Apple 逐案授予 Network Extension 权益,因此该目标有意采用可选启用。一旦 App ID 拥有该权益,就在 codenameone_settings.properties 中指定隧道类:

codename1.arg.ios.vpn.tunnel=true
codename1.arg.ios.vpn.tunnel.class=com.acme.LocalTunnel
codename1.arg.ios.appext.CN1VpnTunnel.provisioningURL=https://example.com/CN1VpnTunnel.mobileprovision

宿主应用和 <packageName>.vpntunnel App ID 都需要 Apple 的 Network Extension 权益。该扩展还需要自己的配置文件。它从全新状态启动,需要可访问的无参构造函数,并且看不到应用静态成员或 Display。通过 TunnelSetup.data 传递本地配置,并在 VpnTunnel.onStart() 中读取它。这是一个真正的跨平台 API,而不假装两个操作系统具有相同的生命周期或网络设施。

构建器是运行时契约的一部分

浏览器应用无法注册 CallKit、自管理的 Android 连接服务或 Apple Network Extension。Flutter、React Native 和 .NET MAUI 可以通过原生插件和平台项目访问这些 API。困难的工作于是存在于目标成员资格、清单、权益、后台模式、原生委托和签名中。

构建器改变了这些困难工作所在的位置。构建服务器已经拥有原生产物图。它可以看见哪些 Java 包最终保留在应用中,并生成与之匹配的平台部件:

flowchart LR
    J[Referenced Java packages] --> C[Platform feature catalog]
    C --> I[iOS frameworks<br/>delegates and modes]
    C --> A[Android services<br/>permissions and manifest]
    C --> E[Extension targets<br/>and signing preflight]
    C --> S[Simulator implementation]
    U[Unused packages] --> N[No native baggage]

依赖注入无法完成这项工作。构建器可以添加必须在 Java 启动之前就存在的服务,编译满足操作系统截止时间的原生回调,并作为同一产物的一部分签名扩展。应用团队可以继续掌控其 UI、协议和业务逻辑,而无需维护一套并行的原生构建系统。

Dialog 可以变成原生模态窗口

上周的原生窗口发布列出了几个仍然假设每个顶层都是 Form 的组件。PR #5624 关闭了该列表中的大部分。Dialog、Sheet、ToastBar、组合框弹出菜单、浮动操作子菜单、进度覆盖层、工具提示和 HTMLComponent 现在都会解析包含它们的窗口。无障碍状态也按窗口进行跟踪。

Dialog 可以保持为其所有者内部的轻量级覆盖层,也可以选择启用真正的操作系统窗口:

Dialog.setDefaultNativeWindowMode(true);
boolean delete = Dialog.show(
        "Confirm deletion",
        "Delete Quarterly report.pdf?",
        "Delete",
        "Cancel");

窗口后续文章 涵盖了优先级、回退行为、锚定弹出窗口以及剩余限制。

基于 AppKit 构建的 Mac 应用

Mac Catalyst 帮助我们把 iOS 应用带到 macOS,但它的桌面行为仍受 UIKit 限制。它无法提供若干普通窗口操作,并且一个次级 4K 表面在图像被复制到位之前,每个中间帧可能需要大约 33 MB。

PR #5601 用 AppKit 替换默认 Mac 原生路径。生成的应用现在拥有 NSApplication、NSWindow、NSMenu、NSScreen,以及每个窗口一个 CAMetalLayer。始终置顶、实用工具窗口、最小化、恢复、最大化、原生模态和独立脏区域绘制都是真正的桌面操作。

AppKit 文章 解释了渲染变化、构建目标、测试证据以及通往 VoiceOver 的原生无障碍桥接。

无需收件箱权限的 OTP

PR #5642 添加了一次性验证码约束以及可复用的电话号码和验证组件。在 iOS、Android 和 Web 上,操作系统可以从短信中提供验证码,而无需授予应用读取消息的权限。

PhoneVerification verification = new PhoneVerification();
verification.setCodeSender((number, response) ->
        server.sendCode(number, response));
verification.setCodeVerifier((number, code, response) ->
        server.verifyCode(number, code, response));
verification.addVerifiedListener(evt -> showAccount());

Codename One 不发送或验证短信。你的服务器仍然负责过期、尝试次数限制、速率限制和会话签发。OTP 文章 包含完整组件和更底层的文本字段选项。

别再通过失败的构建来了解平台变化

直到本周之前,Apple 或 Google 的变更通常是通过社区报告传到我们这里的。这很有用,但它是在有人已经受到影响之后才开始计时。

我们现在每天运行一个定时的 Codex 任务,读取官方平台通知,提取要求和日期,追踪受影响的 Codename One 生产者,并检查是否存在已有 issue 或修复。它发现了在 PR #5673 中修复的 Android 16 返回导航漏洞,以及在 PR #5680 中促成 Contact Picker 的隐私方向。

该任务并不会让通知自动成真,也不能替代平台测试。它的职责是将一手来源的截止日期连接到必须更改的确切构建器、生成项目或 API。平台观察文章 展示了这条链。联系人选择有自己的一篇以代码为重点的文章,因为权限模型值得不止一段话。

四个影响范围很大的小改动

Google Play Billing 8

PR #5651 将默认 Android 计费依赖移到 8.0.0,并完成从已停用的 SKU API 到 ProductDetails 的迁移。Google 的计费弃用时间表 现在使版本 8 成为更新的最低要求。构建器吸收了生成项目迁移,大多数应用团队本不该自己去追这件事。

JavaScript 密度现在表示设备像素

PR #5634 修复了当浏览器设备像素比既不是 1 也不是 2 时的显示宽度、高度和毫米换算。一个在比例为 3 时宽度为 390 CSS 像素的视口,现在会报告 1,170 设备像素。

这可能会改变现有 JavaScript 应用中的布局和截图结果。当断点确实需要 CSS 像素时,请读取 CN.getProperty("browser.window.devicePixelRatio", "1")。

鼠标滚轮不再是伪造的拖拽

桌面移植版过去会把一次滚轮转动转换成一次按下、三次拖拽事件和一次释放。这可能在什么都没滚动时触发点击。PR #5660 直接路由原生滚轮事件。

内置滚动会查找最近的可滚动祖先。现在,消费滚轮输入的自定义组件应重写 mouseWheel(WheelEvent) 或注册监听器:

chart.addMouseWheelListener(evt -> {
    WheelEvent wheel = (WheelEvent) evt;
    if (wheel.isControlDown()) {
        zoomBy(wheel.getDeltaY());
        evt.consume();
    }
});

开发者指南变成了受维护的产品

我们移除了过时的 IDE 和 Ant 时代指南,将 Maven 工作流并入主书,恢复了缺失的示例,并重新调整了若干章节的顺序。更重要的是,CI 现在会检查指南的结构、交叉引用、承诺的代码块、提供的链接、图片、正文和生成输出。

文档漂移现在会导致构建失败,而不是留给未来某一周清理的项目。恢复的呼叫、VPN、桌面、安全、身份、事件线程、图形和性能材料是发布契约的一部分。## 以更窄的默认权限实现原生触达

本周,Codename One 在两个方向上同时拓展。通话、VPN 配置、AppKit 窗口、联系人选择和 OTP 更深入地接入各个操作系统。由包触发的构建器则让这种触达范围保持狭窄。应用不会因为另一个产品需要 VoIP 后台执行而获得该能力,也不会为了挑选一个人而获得广泛的联系人访问权限,更不会为了填写六位验证码而获得读取消息的权限。

默认安全编程依赖于小型公共 API、生成的原生集成、明确的平台限制,以及更少将便利功能变成永久访问权限的机会。你仍然掌控应用 UI 和协议选择。构建器则吸收了交付这些功能所需原生产品底层管线。

先阅读通话管理或 VPN章节,然后在发送原生构建之前,在模拟器中测试失败路径。
分享此页面

发现错误,或有内容要补充?在 GitHub 上编辑此页面 [LOADING...]
作者

Shai Almog

作者、开发者关系、博主、开源黑客、Java 摇滚明星、会议演讲者、讲师和企业家。

相关文章

Java [LOADING...]
Shai Almog2026年8月7日 499 次浏览

Push V3:从你的服务器向所有终端发送一条消息

Java Java [LOADING...]
Shai Almog2026年8月29日 169 次浏览

SQLite 跨越每个平台:同一份契约,同一种加密文件格式

Java Java [LOADING...]
Shai Almog2026年8月28日 165 次浏览

App Shield:你的服务器不应信任调用它的应用

Java Java [LOADING...]
Shai Almog2026年6月17日 238 次浏览

Mac 原生构建、实时协议,以及 350 以下的未解决问题

Java Java [LOADING...]
Shai Almog2026年6月15日 342 次浏览

核心中的 AI、OAuth 及其他平台 API

Java Java [LOADING...]
Shai Almog2026年6月3日 1,112 次浏览

NFC、加密技术、生物识别与新的构建云

Java

加入讨论

Java [LOADING...]
Shai Almog2026年8月7日 499 次浏览

Push V3:从你的服务器向所有终端发送一条消息

Java Java [LOADING...]
Shai Almog2026年8月29日 169 次浏览

SQLite 跨越每个平台:同一份契约,同一种加密文件格式

Java Java [LOADING...]
Shai Almog2026年8月28日 165 次浏览

App Shield:你的服务器不应信任调用它的应用

Java Java [LOADING...]
Shai Almog2026年6月17日 238 次浏览

Mac 原生构建、实时协议,以及 350 以下的未解决问题

Java Java [LOADING...]
Shai Almog2026年6月15日 342 次浏览

核心中的 AI、OAuth 及其他平台 API

Java Java [LOADING...]
Shai Almog2026年6月3日 1,112 次浏览

NFC、加密技术、生物识别与新的构建云

Java