Ohhnews

分类导航

$ cd ..
foojay原文

Push V3:来自服务器的一条消息即可触达所有界面

#push v3#codename one#推送通知#跨平台开发#云服务

[LOADING...]

推送通知应作为应用基础设施,而不是一堆不断过期的证书和厂商专属 JSON。

本周,我们将 Push V3 合并到了 Codename One 核心中,并完成了其新的云端实现。它为应用提供了类型化消息模型、托管式厂商凭据、订阅、服务端分组、活动、分析,以及直达 Surfaces 的路径。

还有一件事,所有现有推送开发者现在都应该做:

将推送服务 URL 从 https://push.codenameone.com 改为 https://cloud.codenameone.com,并通过你现有的代码发送一条真实通知。

下周,我们计划下线旧推送服务,并将 push.codenameone.com 流量直接指向新实现。兼容端点接受现有请求格式,所以现有代码应该能继续工作。但这仍然是一台全新的服务器,“应该”并不等于测试结果。请在切换前测试,当前两条路由还可以轻松对比。

TL;DR

推送是今天的头条内容。在接下来的四篇文章中,我会详细拆解本次发布的其他变化:

  • 我们自己的 Maven 仓库:正在将 Codename One 发布迁移到 repo.codenameone.com。下周起,新项目会自动获得该配置。现有项目需要添加一小段 POM 配置,以免新版本不再出现在 Maven Central 上。
  • 设备端 AI 与 MCP:核心现在内置了可移植的 OCR、视觉、语言和 LiteRT API。MCP 服务器可以通过回环地址在移动端上检查并操作真实应用,并带有发布构建保护,因为其他本地进程也能访问该套接字。
  • 健康数据:新 API 覆盖 HealthKit、Health Connect、锻炼、营养、八种蓝牙健康传感器配置文件,以及一个确定性模拟器。该设计保留被拒绝的读取、缺失值、来源重叠和合规边界,而不是把它们折叠成方便的答案。
  • 循路地图路线Routing.showRoute(...) 现在可以把两个坐标转换为道路几何、距离、时长、路段和步骤。OSRM 提供默认测试路径,而 RouteService 将生产环境中的提供商选择保留在应用代码中。

立即测试新的推送服务器

如果你的服务器当前使用传统端点发送,请保持请求完全不变,只修改主机名:

$ diff
-https://push.codenameone.com/push/push
+https://cloud.codenameone.com/push/push

在你应用所支持的每个平台上发送到测试设备。测试可见通知、数据负载、冷启动,以及你使用的任何角标、声音、类别、图片或深链行为。将结果与旧主机进行对比,如果两者不一致,请提交 issue

新服务器包含一个传统兼容层。现有应用不需要在切换主机名之前采用 Java V3 客户端或新的 REST API。这种分离很重要:验证新传输是一个很小的运维变更,而采用 V3 模型则是可以自行安排计划的应用变更。

队列会记录每个目标的厂商响应。“已接受”意味着 APNs、FCM 或其他厂商接受了请求。但它并不能证明操作系统显示了通知、用户看到了通知,或者应用被打开。控制台将这些状态分开,因为一个定义错误但令人安心的数字,比没有数字更糟糕。

V3 让消息成为真正的类型

传统 API 将行为编码为数字推送类型和位置字符串。它能工作,但越来越难以表达厂商差异和新的目的地。

V3 使用不可变模式:

$ java
PushMessage message = PushMessage.builder()
        .title("Boarding changed")
        .body("Flight CN1 42 now leaves from gate C7")
        .deepLink("myapp://trip/CN142")
        .data("tripId", "CN142")
        .ttlSeconds(900)
        .build();

同一个信封可以携带可见内容、应用数据、图片、深链、折叠和生命周期规则、厂商专属选项,以及一个 surface 命令。传入消息在到达应用代码之前就会被解析,通过不可变映射暴露,并且在模式不受支持时被拒绝。

V3 还取代了旧的 PushCallback 契约。你的主应用类不再实现推送接口。你创建 PushClient 并为其提供一个 PushListener

旧 APIV3 API
PushCallback.push(String value)PushListener.onMessage(PushMessage message)
registeredForPush(String deviceId)onRegistration(PushSubscription subscription)
pushRegistrationError(...)onError(PushError error)

这不仅仅是方法重命名。监听器会收到已解析的 PushMessage,注册会返回订阅对象,错误会携带代码和重试信息。

$ java
private PushClient push;

public void init(Object context) {
    push = PushClient.builder("APP_KEY_FROM_CONSOLE")
            .listener(new PushListener() {
                public void onMessage(PushMessage message) {
                    Log.p("Push: " + message.getTitle());
                }

                public void onRegistration(PushSubscription subscription) {
                    Log.p("Registered " + subscription.getTransportId());
                }

                public void onError(PushError error) {
                    Log.p(error.getCode() + ": " + error.getMessage());
                }
            })
            .build();
}

public void start() {
    push.register();
}

init() 中创建一个客户端,保留它,并在 start() 中调用 register()。注册是幂等的。不要在 stop() 中注销,因为这会移除订阅,而不是暂停订阅。

运行自有推送基础设施的应用不会被托管服务限制。PushTransport 是用于自定义注册和投递的公开接口,而 PushRegistrationSink 允许应用将注册变更同步到自己的后端。

一条推送可更新通知或 Surface

锁屏通知只是目的地之一。桌面小组件、实时活动、灵动岛、手表复杂功能,以及其他 Surfaces 也需要最新状态。

V3 在同一个信封中保留了一个类型化的 surface 对象。原生引导代码可以在主应用 UI 运行之前路由 Surface 命令。

[LOADING...]

考虑一个配送应用,当顾客等待快递员时,应用并没有在运行。服务端推送可以更新其实时活动和灵动岛,显示“快递员还有 2 分钟到达”,而无需启动主 Codename One UI。在没有该 Surface 的平台上,同一活动可以投递普通通知作为替代。

消息视图仍会显示 APNs 或其他厂商是否接受了每次更新。当应用内部没有运行 PushListener 而 Surface 发生变化时,这一点很有用。

证书不再是你的服务器的问题

旧方案通常要求应用团队生成推送证书,将其放在自己的服务器上,跟踪过期日期,然后不断重复。这看起来是配置,其实是脆弱的基础设施。

新控制台会为每个应用和环境存储厂商凭据。APNs 可以使用 .p8 签名密钥,它没有旧证书工作流的年度过期周期。推送服务会为厂商请求签名,并将凭据与活动用户隔离开。

[LOADING...]

凭据在静态时加密,并在控制台中作为只写机密处理。读取应用设置不会返回机密值。这确实消除了应用服务器上的证书托管,但并不能免除常规的机密卫生习惯:使用范围狭窄的厂商密钥,在团队成员或系统边界变化时轮换密钥,并区分生产环境和开发环境。

无需将身份交给设备即可进行分组

控制台将应用、环境、订阅、受众、消息、活动和分析分开。

[LOADING...]

设备可以通过公共客户端端点注册其厂商令牌。但它不能声明外部用户身份,也不能给自己附加任意标签。这些操作需要经过认证的服务器 API。否则,被修改过的客户端可以直接给自己打上 premiumadministratorpatient-high-risk 的标签,进入本不属于它的分组。

[LOADING...]

已保存的分组会在服务器端根据应用作用域的订阅数据进行评估。一个分组可以选择语言区域、应用版本、平台,或者由你的后端分配的标签。受众在消息发送时解析,因此修正标签后无需重建静态邮件列表。

这是针对应用行为的分组,而不是广告画像。Codename One 不会出售订阅数据,也不会跨客户合并数据。该服务仍然必须保留投递所需的数据:厂商令牌、安装标识和可选的外部标识符、服务器分配的标签、消息负载、目标状态和厂商响应。

切勿将密码、访问令牌、医疗结果或其他机密放入通知负载。厂商和操作系统都参与投递,锁屏可能会显示可见文本,而通知数据也可能比你预期展示它的界面存活更久。

回答运维问题的监控

新的消息视图会暴露排队中、已接受、失败和无效目标,包括厂商错误信息。

[LOADING...]

这支持多种运维检查:

  • 队列是否在推进?
  • 是否有一个厂商失败,而其他厂商接受了消息?
  • 过期的设备令牌是否正在被移除?
  • 速率限制是否延迟了大型受众?
  • 这条消息来自哪个环境和活动?

分析数据保留 30 天。它们是投递运维分析,而不是注意力证明。应用打开次数或业务结果,仍应留在由你控制的、符合知情同意的产品分析中。

各套餐包含什么

推送发送和管理式厂商凭据在包括 Free 在内的所有订阅级别均可用。各套餐的区别在于每月数量、速率限制和持久化活动工具:

套餐每个席位每月投递量每分钟请求数每分钟接收人数持久化受众和活动自动化
Free1,00030100
Basic5,0001201,000
Pro1,000,00060010,000
Enterprise10,000,0003,000100,000

Free 和 Basic 应用可以通过同一条持久化厂商管道发送。Pro 增加保存的模板、分组、活动和分析。Enterprise 增加自动化和更高的运维限制。配额是面向组织和席位的,因此团队可以查看一次通知运行消耗了哪项额度。

这些数字只是初始策略,而不是在说每个应用都需要一百万次通知。从小而明确的受众开始。一条帮助到 200 人的精准通知,好过一场让 20 万人学会关闭通知的模糊轰炸。

Maven 仓库迁移:第一阶段

我们还合并了从 Maven Central 迁移到 Cloudflare R2 上 Codename One 仓库的第一阶段

Maven Central 完全有权设置商业使用限制并收取基础设施费用。Codename One 也有一种很难塞进这些限制的工作负载。一次发布目前会发布大量重复的 fat-jar 内容,导致我们的仪表盘数据显示:存储占用 2.12 GB,而软性指南约为 80 MB;文件数 19,962,而指南为 1,000;发布数 27,而指南为 7。

[LOADING...]

这些限制是软性指南,仪表盘同时提供开源调整方案和商业计划。我们离开不是因为 Sonatype 做错了什么。我们离开是因为我们每周、多平台的发布形态在那里托管成本太高,而我们可以在更适配的基础设施上免费为用户提供相同的 Maven 布局。

第一阶段将实测发布负载从 229.5 MB 减少到 76.9 MB。发布管道现在会把已签名的 Central 暂存树复制到 R2,因此不会执行可能导致字节不一致的第二次构建。

[LOADING...]

现有项目可以通过在两个 Maven 解析路径中添加该仓库,为切换后的版本做好准备:

$ xml

        codenameone
        https://repo.codenameone.com/maven2

        codenameone-plugins
        https://repo.codenameone.com/maven2

第二个配置块很重要。Maven 会单独解析构建插件和普通依赖。如果只添加 块,未来某个 ``codenameone-maven-plugin 版本可能无法被发现。

新项目会在 8 月 7 日自动获得该配置。现有版本仍保留在 Central 上。新仓库保证至少六个月的留存历史,而优化后的负载按当前发布频率大约可以容纳 1.6 年。

R2 对象存储和 Cloudflare 边缘网络应能改善依赖解析,并消除 Central 对我们的发布路径造成的节流。我们还期望 CI 和发布变得更快速、更稳定。这些是我们在双发布期间将要度量的预期,而不是已经验证的结果。

Maven 文章将于 8 月 4 日发布,包含完整负载审计、切换计划、R2 发布保障、留存策略,以及我们在双发布期间测试的失败模式。## 设备端 AI 与 MCP:覆盖每个平台

PR #5467 将视觉、语言和 LiteRT 推理引入核心。该 API 覆盖 OCR、条码识别、人脸检测、图像标签、姿态检测、自拍分割、文档矫正、语言识别、翻译、智能回复,以及应用自有的 .tflite 模型。

公开 API 保持可移植,但实际处理仍在设备端完成。Android 在较高层级操作中使用 ML Kit。Apple 移植版在适用处使用 Vision、Core Image 和 Natural Language。不支持的平台会报告“不支持该能力”,而不是悄悄把输入上传到云端后备方案。

OCR 特意采用异步设计,因为原生转换和识别不能阻塞 Codename One EDT:

$ java
TextRecognizer recognizer = new TextRecognizer();
recognizer.process(VisionImage.encoded(jpegBytes))
        .ready(result -> textArea.setText(result.getText()))
        .except(error -> Log.e(error));

推理会话会在多次运行之间保持应用自有的模型处于加载状态。运行时模型下载可以使用 ModelCache;它要求 HTTPS,并在激活文件前校验 SHA-256 摘要。模型会改变应用行为,因此接受未经验证的模型字节,等同于制造软件供应链漏洞。

PR #5472 将现有的语义 MCP(Model Context Protocol)服务端扩展到 JavaSE 之外。在支持回环连接的平台上,大语言模型(LLM)可以读取组件树、按语义标识找到按钮、设置文本、触发操作,并在实际应用中检查结果状态。

[LOADING...]

这用应用语义取代了坐标猜测。同时,它也创建了一条控制通道。绑定到 127.0.0.1 可以防止意外暴露到局域网,但不会对设备或工作站上的其他进程进行身份验证。

$ java
if (Display.getInstance().isDebuggableBuild()) {
    MCP.startSocketServer(8642);
}

默认情况下,MCP 拒绝在发布构建中启动。虽然存在显式覆盖开关,可用于受控测试实验室,但不应让它成为消费级应用中的便利开关。

关于 AI 与 MCP 的文章将于 8 月 2 日发布,内容包括能力矩阵、可移植推理模型、语义调试闭环,以及回环仍需要发布构建门禁的原因。

健康数据:拒绝虚假的确定性

PR #5475 为 HealthKit、Health Connect、已记录锻炼、稀疏营养数据、确定性模拟,以及八种已采用的蓝牙健康传感器配置文件,新增了一流的 API 支持。

公共 API 按应用需要理解的边界进行划分:

职责
com.codename1.health存储、权限、样本、查询、聚合、数据源和变更游标
com.codename1.health.workout已记录的锻炼会话、事件、配置和采集的样本
com.codename1.health.sensors实时标准蓝牙健康设备,无需手机健康存储
com.codename1.health.nutrition稀疏营养记录,缺失值仍然保持缺失

模拟器、桌面和 JavaScript 构建会返回 LOCAL_ONLY。这是一个支持读写的受支持存储,而不是缺少提供程序。只有 Android 提供程序失败时,才应引导用户进入提供程序设置:

$ java
Health health = Health.getInstance();
HealthAvailability availability = health.getAvailability();
if (availability == HealthAvailability.PROVIDER_NOT_INSTALLED
        || availability == HealthAvailability.PROVIDER_UPDATE_REQUIRED) {
    health.openProviderSetup();
    return;
}
if (availability == HealthAvailability.NOT_SUPPORTED) {
    return;
}

HealthStore store = health.getStore();

[LOADING...]

有些 API 看起来比较保守,是因为平台契约本身就保守。HealthKit 不会透露用户是否拒绝了读取权限。授权页完成只表示系统询问过用户,并不代表应用可以读取该类别。因此,公共 API 没有 hasReadPermission() 方法,因为在 iOS 上它会给出不实信息。

该 API 在整个模型中保留了这些区别。空聚合返回 null,而不是 0。按日历日分桶需要时区。手机和手表数据源保持可区分,因为静默添加重叠样本可能会让一次步行被重复计数。不支持的手机映射会以 TYPE_NOT_SUPPORTED 失败,而不是返回一个看似成功的空集合。

模拟器包含一种模式:在不报错的情况下允许写入但拒绝读取。这让你可以测试宽松假实现会漏掉的 UI 错误:当唯一准确的说法是“没有可用数据”时,却指责用户拒绝了访问权限。

这个 API 不会让应用自动符合 HIPAA 合规要求。它从不上传健康数据,可以强制校验特定用途字符串,并拒绝不支持的写入。访问控制、加密、审计记录、留存、用户同意、泄露处理、后端契约和应用商店披露,仍然需要应用自行负责。

关于健康数据的文章将于 8 月 1 日发布,内容包括平台矩阵、授权陷阱、样本模型、变更游标、锻炼、蓝牙传感器、模拟器故障模式、构建配置和 HIPAA 边界。

折线不是路线

地图折线只是连接已经存在的坐标。它无法发现坐标之间的道路、行驶时间、转向动作或替代路径。

PR #5480 新增了 com.codename1.maps.routing。最小可用路径是两个坐标:

$ java
MapView map = new MapView();
Routing.showRoute(
        map,
        new LatLng(38.8977, -77.0365),
        new LatLng(38.8894, -77.0352)
);

该调用会立即返回。活动的 RouteService 找到路线后,API 会绘制路线几何形状,并在 Codename One EDT 上调整地图取景范围。

[LOADING...]

默认服务是 OSRM,因此首次请求驾车路线不需要注册提供方。它指向公共 OSRM 演示服务器,该服务器没有生产级服务等级协议(SLA),并且使用汽车配置。WALKING 请求并不会把这张路网图变成步行路线。

生产应用可以将 OsrmRouteService 指向自己的服务器,或安装其他提供方:

$ java
Routing.setService(new OsrmRouteService(
        "https://routing.example.com"
));

应用使用可移植的 Route 对象,而不是提供方返回的 JSON。这些对象包含备选路线、距离、时长、边界、几何形状、路段、分步指引、转向位置和提供方元数据。

这是路线规划,而不是逐向导航。该版本没有声称支持重新规划路线、交通预测、离线地图包或语音引导。这些功能需要位置更新、生命周期状态和提供方特定规则。

关于路线规划的文章将于 8 月 3 日发布,内容包括自定义路线样式、预计到达时间(ETA)处理、编码折线支持、OSRM 限制、出行方式、提供方替换,以及路线结果与导航之间的边界。

明天,我会从新的 Health API 开始,逐一讲解这些改动。今天,请大家通过 cloud.codenameone.com 发送一条真实通知。这一周,你可以并排比较旧的和新的推送实现。如果任何行为有差异,请在我们下周切换流量之前提交 issue

本文《Push V3:从你的服务器到每个平台的一条消息》首发于 foojay