diff --git a/docs/developer-guide/Bastion-Users-Guide.asciidoc b/docs/developer-guide/Bastion-Users-Guide.asciidoc index b6d634971cf..4f6bf4f2969 100644 --- a/docs/developer-guide/Bastion-Users-Guide.asciidoc +++ b/docs/developer-guide/Bastion-Users-Guide.asciidoc @@ -128,8 +128,6 @@ image::img/bastion-cards.png["The cards screen with a card, its masked number, i *Exchange* lists the markets, BTC-USD and ETH-USD, with their last price and the index price the bank values coins at, and the customer's recent trades. -image::img/bastion-exchange.png["The exchange screen listing the BTC/USD and ETH/USD markets with their prices",scaledwidth=40%] - image::img/bastion-order-book.png["The BTC/USD market: the last price, the bid and ask, hourly candles with the volume under them, and the order book",scaledwidth=40%] A market opens the last day in hourly candles, a rising hour in green and a falling one in red over its high-low range, the volume traded each hour, its order book and a ticket: diff --git a/docs/developer-guide/Desktop-Interop.asciidoc b/docs/developer-guide/Desktop-Interop.asciidoc index 47680411619..707185cb534 100644 --- a/docs/developer-guide/Desktop-Interop.asciidoc +++ b/docs/developer-guide/Desktop-Interop.asciidoc @@ -436,6 +436,24 @@ when it paints, so it stays sharp at any display density. === On a phone and on the desktop +SwingSet2, the JDK's own Swing demo, and the open source JavaFX 2048 game build without source changes beyond what the importer asks for (SwingSet2's classes moved out of the default package): + +[cols="1,1",frame=none,grid=none] +|=== +a|image::img/desktop-compat-swingset2.png[SwingSet2 running as a Codename One app on an iPad,scaledwidth=95%] +a|image::img/desktop-compat-fx2048.png[The JavaFX 2048 game running as a Codename One app on an Android phone,scaledwidth=70%] +|=== + +The screens below are the Swing and JavaFX scenes from the screenshot test suite, running unmodified on an iPhone. Each is ordinary desktop code: a `JTable` with a custom renderer, JavaFX controls, and an FXML document styled by its own style sheet. + +[cols="1,1",frame=none,grid=none] +|=== +a|image::img/desktop-compat-ios-swing-controls.png["Swing text fields, check boxes, radio buttons, a combo box, a slider and a progress bar on an iPhone",scaledwidth=90%] +a|image::img/desktop-compat-ios-swing-tables.png[A JTable with a custom renderer and a striped JXTable on an iPhone,scaledwidth=90%] +a|image::img/desktop-compat-ios-javafx-controls.png["JavaFX text fields, a choice box, a slider, a progress bar and check boxes on an iPhone",scaledwidth=90%] +a|image::img/desktop-compat-ios-javafx-fxml.png[An FXML form with its controller and style sheet on an iPhone,scaledwidth=90%] +|=== + ==== Logical pixels Desktop code sizes things in pixels and assumes about 96 of them to the diff --git a/docs/developer-guide/MCP-Headless-API.asciidoc b/docs/developer-guide/MCP-Headless-API.asciidoc index c3e6a946a3a..fbc77a800b6 100644 --- a/docs/developer-guide/MCP-Headless-API.asciidoc +++ b/docs/developer-guide/MCP-Headless-API.asciidoc @@ -118,6 +118,10 @@ The simulator adds tools of its own, prefixed `sim_`, that exist only on the des * Android in resize mode: the application gets `sizeChanged` with the height above the keyboard. * Android in pan mode: the size stays the same and the whole screen shifts up until the field clears the keyboard. +The recording below was made by an agent through these tools: it turned the iOS keyboard on, started a recording, tapped the email field, typed into both fields, moved between them with *Next* and *Done*, and stopped the recording. + +image::img/simulator-virtual-keyboard.gif[The simulated iOS keyboard sliding up while a sign up form scrolls its email and password fields into view,scaledwidth=40%] + By default Android chooses between resize and pan for each field, the same way the Android port does. The *Android: Resize* and *Android: Pan* items, or `androidSoftInput`, force one mode. Virtual keyboard listeners fire on show and hide in both cases. *Touches.* `sim_tap` and `sim_drag` press the screen through the same path as a mouse click, so they reach the simulated keyboard and appear as touch indicators, which `ui_activate` doesn't. `sim_touch_indicators` sets the style (`none`, `dot` or `ripple`), color, size and drag trails. *Simulate > Show Touches* also draws them in the simulator window. diff --git a/docs/developer-guide/img/bastion-exchange.png b/docs/developer-guide/img/bastion-exchange.png deleted file mode 100644 index db86e01c5a1..00000000000 Binary files a/docs/developer-guide/img/bastion-exchange.png and /dev/null differ diff --git a/docs/developer-guide/img/desktop-compat-fx2048.png b/docs/developer-guide/img/desktop-compat-fx2048.png new file mode 100644 index 00000000000..d21adeb54ae Binary files /dev/null and b/docs/developer-guide/img/desktop-compat-fx2048.png differ diff --git a/docs/developer-guide/img/desktop-compat-ios-javafx-controls.png b/docs/developer-guide/img/desktop-compat-ios-javafx-controls.png new file mode 100644 index 00000000000..d5a1ff11fe6 Binary files /dev/null and b/docs/developer-guide/img/desktop-compat-ios-javafx-controls.png differ diff --git a/docs/developer-guide/img/desktop-compat-ios-javafx-fxml.png b/docs/developer-guide/img/desktop-compat-ios-javafx-fxml.png new file mode 100644 index 00000000000..c897727a9cc Binary files /dev/null and b/docs/developer-guide/img/desktop-compat-ios-javafx-fxml.png differ diff --git a/docs/developer-guide/img/desktop-compat-ios-swing-controls.png b/docs/developer-guide/img/desktop-compat-ios-swing-controls.png new file mode 100644 index 00000000000..ab875ba60ba Binary files /dev/null and b/docs/developer-guide/img/desktop-compat-ios-swing-controls.png differ diff --git a/docs/developer-guide/img/desktop-compat-ios-swing-tables.png b/docs/developer-guide/img/desktop-compat-ios-swing-tables.png new file mode 100644 index 00000000000..e53785cc221 Binary files /dev/null and b/docs/developer-guide/img/desktop-compat-ios-swing-tables.png differ diff --git a/docs/developer-guide/img/desktop-compat-swingset2.png b/docs/developer-guide/img/desktop-compat-swingset2.png new file mode 100644 index 00000000000..6d1b79e0e5b Binary files /dev/null and b/docs/developer-guide/img/desktop-compat-swingset2.png differ diff --git a/docs/developer-guide/img/simulator-virtual-keyboard.gif b/docs/developer-guide/img/simulator-virtual-keyboard.gif new file mode 100644 index 00000000000..896500eaa29 Binary files /dev/null and b/docs/developer-guide/img/simulator-virtual-keyboard.gif differ diff --git a/docs/website/content/blog/android-apps-everywhere-even-without-google.md b/docs/website/content/blog/android-apps-everywhere-even-without-google.md new file mode 100644 index 00000000000..8725cfdd887 --- /dev/null +++ b/docs/website/content/blog/android-apps-everywhere-even-without-google.md @@ -0,0 +1,117 @@ +--- +title: "GrapheneOS, LineageOS, /e/OS and Others Are Now Officially Supported" +slug: android-apps-everywhere-even-without-google +url: /blog/android-apps-everywhere-even-without-google/ +date: '2026-11-04' +author: Shai Almog +description: "Codename One now officially supports GrapheneOS, LineageOS, CalyxOS, /e/OS and other Android builds without Google, with a boundary CI verifies and push through UnifiedPush. Also: the Android compatibility layer now runs Room, WorkManager, location and document pickers on iOS and desktop." +feed_html: "\"An GrapheneOS, LineageOS, CalyxOS and /e/OS are now officially supported, and the Android layer runs Room, WorkManager and document pickers on iOS." +series: ["release-2026-10-30"] +--- + +![An Android app running on Android and on Codename One side by side](/blog/android-apps-everywhere-even-without-google.jpg) + +This post is mostly about phones that don't have Google at all: GrapheneOS, LineageOS, CalyxOS, /e/OS and other Android builds. Codename One now supports them officially. After that, a shorter update on the Android compatibility layer, which runs existing Android apps on iPhone, desktop and the web. + +## GrapheneOS, LineageOS, CalyxOS and /e/OS + +I was a member of Team OS/2. I know what it's like to use an operating system the big vendors forgot, and to find that half the apps you need just don't start. People choose GrapheneOS, LineageOS and other AOSP builds for good reasons, usually privacy, and they deserve apps that work. There's a business case too: when you support a niche operating system its users have learned to live without, you get the most loyal advocates your product will ever have. They notice, they remember, and they tell each other. + +Codename One apps mostly worked on these phones before. But mostly isn't support: call the wrong API and you could hit a failure, or behavior nobody could predict. Now it's official, with a clear boundary. One APK serves both kinds of phone: at run time it checks whether Google Play Services is there. No build hint, no flavor, no second build. The Android port never calls Play services, Play Core or Firebase without checking first, and CI enforces that, so a future change can't quietly break it. + +| Feature | With Google Play | Without Google Play | +| --- | --- | --- | +| UI, storage, networking, SQLite, media | Works | Works | +| On device ML Kit models | Works | Works | +| Location and geofences | Fused location | Framework `LocationManager` | +| Maps | Google Maps | Vector `MapView` | +| Google sign in | Native | Browser OAuth flow | +| Push | Firebase | UnifiedPush (ntfy, NextPush, Sunup) | +| In-app purchase | Play Billing | Not available | +| Integrity attestation, in-app review | Works | Not available | + +The location fallback fixed something on Google phones too: builds without Play location now get proximity alerts and background location through the framework API. + +### Push, with one line + +Push needs one line of configuration, because the phone has no Firebase to deliver it. UnifiedPush lets the user pick the app that holds the push connection, called a distributor. A UnifiedPush endpoint is a standard Web Push subscription, so the Codename One push service delivers to it with the Web Push credential your app already has. You copy its public key into a build hint: + +```properties +codename1.arg.android.unifiedPush.vapidKey= +``` + +Your app code doesn't change, and neither does your server. The registration order is automatic: + +{{< mermaid >}} +flowchart TD + S[PushClient registers] --> G{Google Play Services?} + G -->|yes| F[Firebase Cloud Messaging] + G -->|no| H{Huawei HMS?} + H -->|yes| K[Huawei Push Kit] + H -->|no| U{UnifiedPush distributor installed?} + U -->|yes| P[UnifiedPush, delivered as Web Push] + U -->|no| E[pushRegistrationError: a push service is needed] +{{< /mermaid >}} + +The one real gap is in-app purchase. Play Billing needs the Play Store. If selling on de-Googled phones matters to you, tell us; it's something we can add. + +To test, use the Android emulator's AOSP system images, the `default` target, which have no Google apps. The [Android without Google Play chapter](/developer-guide/android-without-google-play/) has the details. + +## Also new: classic Android apps, on iPhone + +[Three weeks ago](/blog/android-apps-beyond-android/) we showed that an Android Studio module's `src/main` can build unmodified as a Codename One app. There's no Android runtime inside it. Views are Codename One components, layouts follow Android's measure and layout rules, and resources compile to a binary table at build time. Nothing parses XML or uses reflection on the device. You import a project with one command: + +```bash +mvn cn1:import-android-project -Dcn1.android.import=/path/to/MyAndroidApp +mvn cn1:build -Dcodename1.platform=ios +``` + +{{< guide-block >}} +
+
A Material 3 screen on Android and on Codename One
Material 3.
+
A RecyclerView on Android and on Codename One
RecyclerView.
+
A Kotlin activity on Android and on Codename One
Kotlin activities.
+
+{{< /guide-block >}} + +Each pair shows Android on the left and Codename One on the right, from the same source. + +### What's new: the parts real apps lean on + +Toy apps are mostly UI. Real apps store data, run work in the background and talk to the hardware. The layer now covers those: + +- **Room.** The build generates the `_Impl` classes, the way Android's annotation processor does. Features we don't support yet (`@Relation`, `suspend` DAOs, Flow, Rx, Paging, `@AutoMigration` and FTS) fail the build and name the member. +- **WorkManager.** Chains, unique work, retries and LiveData status, persisted across app restarts. +- **Location and sensors.** `LocationManager` and `SensorManager`. +- **Services.** Manifest services, including `startForeground()` mapped to Codename One's `ForegroundService`. Notifications from `NotificationManager` and `NotificationCompat` show as local notifications. +- **Keystore.** AES keys from `AndroidKeyStore` with GCM and CBC. +- **Document pickers.** `ACTION_OPEN_DOCUMENT`, `ACTION_CREATE_DOCUMENT` and `ACTION_GET_CONTENT`, plus the ActivityResult contracts, open the platform's native open and save dialogs. + +So this is ordinary Android code, and it now runs on an iPhone: + +```java {class="cn1-no-try"} +@Dao +public interface NoteDao { + @Query("SELECT * FROM Note ORDER BY created DESC") + List all(); + + @Insert + long insert(Note note); +} + +Intent save = new Intent(Intent.ACTION_CREATE_DOCUMENT); +save.addCategory(Intent.CATEGORY_OPENABLE); +save.setType("text/plain"); +save.putExtra(Intent.EXTRA_TITLE, "notes.txt"); +startActivityForResult(save, SAVE_NOTES); +``` + +### Where iOS says no + +Some Android behavior has no iOS equivalent, and the [Android chapter](/developer-guide/android-interop/) has a table for it. Location in a foreground service keeps running in the background when the manifest asks for background location. Other foreground service work gets only the short window iOS allows. Constrained WorkManager jobs go to iOS's background task scheduler, and iOS decides when they run. On iOS, `ACTION_CREATE_DOCUMENT` returns `RESULT_OK` right away and shows the save dialog when your app closes the output stream, so a cancel isn't reported back. + +Other limits: Keystore keys aren't hardware backed there, `Parcel` is an in process container, SQLite uses a single connection, there are no system content providers or broadcasts, and reflection based libraries won't run on iOS. + +Tomorrow, the last post of the week: {{< post-link path="/blog/backend-caches-certificates-and-a-simulator-that-types" text="Caches, Certificates, Streaming Uploads and a Simulator That Types" >}}. + +{{< giscus >}} diff --git a/docs/website/content/blog/backend-caches-certificates-and-a-simulator-that-types.md b/docs/website/content/blog/backend-caches-certificates-and-a-simulator-that-types.md new file mode 100644 index 00000000000..6706b14b728 --- /dev/null +++ b/docs/website/content/blog/backend-caches-certificates-and-a-simulator-that-types.md @@ -0,0 +1,206 @@ +--- +title: "Caches, Certificates, Streaming Uploads and a Simulator That Types" +slug: backend-caches-certificates-and-a-simulator-that-types +url: /blog/backend-caches-certificates-and-a-simulator-that-types/ +date: '2026-11-05' +author: Shai Almog +description: "Eight backend upgrades driven by this month's templates: a bounded cache, outbound OAuth tokens, certificates and CSRs, JSON codecs, error advice, streaming uploads, java.time in the ORM and focused test slices. Plus a simulator that slides up a virtual keyboard and records video." +feed_html: "\"Backend Eight backend upgrades, from a bounded cache to certificate validation and test slices, plus a simulator that slides up a virtual keyboard and records video." +series: ["release-2026-10-30"] +--- + +![Backend code and the simulator with a virtual keyboard](/blog/backend-caches-certificates-and-a-simulator-that-types.jpg) + +The templates this month (a bank, a streaming service, a messenger, a delivery service and a game) all have servers, and every server ran into something a modern web app takes for granted. This post collects what we added to the Codename One backend to fill those gaps. Several of them apply to the client too. Then there's the simulator, which can now show a virtual keyboard and record a video of your app. + +If you write Spring Boot during the day, most of this will look familiar on purpose. When there's no reason to differ, the backend behaves the way Spring does. + +## A bounded cache + +Bastion looks up prices on every quote. Calling the price feed on every quote is slow and burns the feed's quota. Caching forever shows stale prices. The new [cache](/developer-guide/backend-observability/#_application_caches) has a size bound, expiry, a removal listener and metrics: + +```java {class="cn1-no-try"} +private static final Cache PRICES = Cache.builder("prices") + .maximumSize(10000) + .expireAfterWriteMillis(60000) + .removalListener((sku, price, cause) -> { + if (cause == Cache.SIZE) { + System.out.println("evicted " + sku); + } + }) + .build(); +``` + +## Tokens for calling other services + +Your server calls other servers: a Google API with a service account, a billing service with client credentials. [`AccessTokenProvider`](/developer-guide/backend-security/#backend-security-outbound) fetches the token, caches it and refreshes it before it expires: + +```java {class="cn1-no-try"} +AccessTokenProvider billing(Config config) throws IOException { + return new AccessTokenProvider(ClientCredentialsGrant + .builder("https://auth.example.com/oauth/token", "orders-service") + .clientSecret(config.get("billing.client-secret")) + .scopes("invoices:write") + .build()); +} +``` + +The same change added [importing users from an existing identity store](/developer-guide/backend-security/#backend-security-migration), so moving an app's accounts over doesn't mean asking everyone to reset a password. + +## Certificates without OpenSSL on the command line + +Banks and partners love mutual TLS and private certificate authorities. The [PKI API](/developer-guide/backend-security/#backend-security-pki) parses PEM, validates a chain against your own anchors, creates certificate signing requests and reads and writes PKCS#12: + +```java {class="cn1-no-try"} +List anchors = Certificate.listFromPem(KeyFiles.read("/etc/app/partner-ca.pem")); +List path = new CertificateValidator(anchors) + .forHost("api.partner.example") + .validate(Certificate.listFromPem(presentedPem)); +``` + +## Errors in one place + +[`@RestControllerAdvice` and `@ExceptionHandler`](/developer-guide/backend-web/#backend-web-errors) turn exceptions into responses once, instead of a try and catch in every handler. [Interceptors](/developer-guide/backend-web/#backend-web-interceptors) and [validation](/developer-guide/backend-web/#backend-web-validation) came with the same change. + +```java {class="cn1-no-try"} +@RestControllerAdvice +public class ApiErrors { + @ExceptionHandler(IllegalArgumentException.class) + public HttpServer.Response badInput(IllegalArgumentException e, HttpServer.Request request) { + return HttpServer.Response.text(400, e.getMessage()).header("X-Error", "input"); + } +} +``` + +## Streaming bodies and a proxy + +Reel's uploads are video files. Reading them into memory isn't an option. [`request.getBodyStream()`](/developer-guide/backend-web/#_large_uploads_and_downloads) hands you the body as it arrives, and large uploads, multipart ones included, spool to disk instead of the heap: + +```java {class="cn1-no-try"} +@PostMapping("/archive") +public Map archive(HttpServer.Request request) throws IOException { + long total = 0; + byte[] buffer = new byte[64 * 1024]; + InputStream in = request.getBodyStream(); + try { + for (int n = in.read(buffer); n > 0; n = in.read(buffer)) { + total += n; // store, hash or forward each block + } + } finally { + in.close(); + } + return Collections.singletonMap("bytes", total); +} +``` + +[`HttpProxy`](/developer-guide/backend-web/#_forwarding_to_another_service) forwards a request to an internal service. It reaches only its target and the hosts you allow, so a crafted path can't turn your server into an open proxy: + +```java {class="cn1-no-try"} +images = new HttpProxy("https://images.internal:8443") + .allowHost("cdn.internal"); + +@RequestMapping("/images/{name}") +public HttpServer.Response image(HttpServer.Request request) { + return images.forward(request); // the same path and query, upstream +} +``` + +## JSON codecs and java.time in the ORM + +These two apply to the client as well as the server. [JSON mapping](/developer-guide/backend-web/#backend-json-contract) gained custom codecs through `JsonTypeAdapter`, constructor based creators, generics, arrays and explicit null handling. A codec turns a type into JSON and back, here a money amount kept as a decimal string so it never passes through a `double`: + +```java {class="cn1-no-try"} +public final class MoneyAdapter implements JsonTypeAdapter { + public Object toJson(Money m) { + return m == null ? null : m.toDecimalString(); + } + public Money fromJson(Object json) { + if (json == null) { + return null; + } + if (!(json instanceof String)) { + throw new IllegalArgumentException("expected an amount"); + } + return Money.parse((String) json); + } +} +``` + +Put `@JsonAdapter(MoneyAdapter.class)` on the field or the class and the mapper uses it. + +The [ORM now maps `java.time`](/developer-guide/annotation-sqlite-orm/#orm-native-dates) without an annotation: `Instant` and `LocalDateTime` as epoch milliseconds and `LocalDate` as an epoch day, so a date column is a number you can index and compare: + +```java {class="cn1-no-try"} +@Entity +public class Booking { + @Id + private long id; + private Instant created; // epoch milliseconds + private LocalDate checkIn; // epoch day + private LocalDateTime pickup; // epoch milliseconds +} +``` + +## Tests that start only what they need + +[`@WebMvcTest`](/developer-guide/backend-testing/#_testing_one_slice_of_the_application) starts one controller and the beans you list, not the whole application, and `MockMvc` calls it without a socket: + +```java {class="cn1-no-try"} +@WebMvcTest(value = GreetingApi.class, includes = Greetings.class) +class GreetingSliceTest { + @Autowired + private MockMvc mvc; + + @Test + void greetsWithOnlyTheControllerWired() throws Exception { + mvc.perform(get("/greet/{name}", "Ada")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.greeting").value("Hello, Ada")); + } +} +``` + +The same change defined a [metric schema](/developer-guide/backend-observability/#_the_metric_schema) and [probes](/developer-guide/backend-observability/#_probes_and_draining) for observability, so a dashboard means the same thing for every Codename One service. + +The backend is still a tech preview. The [backend chapters](/developer-guide/backend/) of the developer guide cover each of these, with the code that the snippets above come from. + +## A simulator that types + +Here's a bug you've probably shipped. The login form looks great in the simulator. On a phone, the keyboard slides up and covers the password field, or the layout jumps, and nobody noticed because the simulator never had a keyboard. + +Now it [does](/developer-guide/mcp-headless-api/#_simulator_keyboard_touches_and_recording). **Simulate > Virtual Keyboard** slides up a keyboard styled after the skin's platform, and it behaves the way that platform does: + +- **iOS:** the screen keeps its size, `Display.getInvisibleAreaUnderVKB()` reports the keyboard height, and the focused field scrolls into view. +- **Android, resize mode:** the form gets a `sizeChanged` and lays out again. +- **Android, pan mode:** the whole screen shifts up. + +{{< guide-block >}} +
Recorded by the simulator itself, driven over MCP by the script below.
+{{< /guide-block >}} + +By default Android picks a mode per field, the way the real platform does. The **Android: Resize** and **Android: Pan** menu items, or the `androidSoftInput` setting, force one so you can test both. + +The simulator also records. It writes MP4, MOV, WebM or an animated GIF, of the screen alone or inside the device skin, on a fixed frame clock so the video doesn't stutter when your machine is busy. Taps show touch indicators and can play a click, and you can add captions. That's a store preview, a bug report or a tweet, without a screen recorder. + +Both are also tools in the simulator's built in MCP server, so a coding agent can drive a whole flow and hand you the video: + +``` +sim_keyboard_set {"mode": "ios"} +sim_record_start {"path": "/tmp/signup.mp4", "frame": "screen"} +sim_record_caption {"text": "The keyboard slides up, the field stays visible"} +ui_snapshot {} +sim_tap {"nodeId": 41} +sim_keyboard_type {"text": "ada@example.com"} +sim_keyboard_key {"key": "next"} +sim_keyboard_type {"text": "correct horse"} +sim_keyboard_key {"key": "done"} +sim_record_stop +``` + +It's an emulation of each platform's keyboard behavior, not the native keyboard itself, so do a final pass on a device. But the covered password field shows up on your desk now, not in a one-star review. The [simulator keyboard and recording section](/developer-guide/mcp-headless-api/#_simulator_keyboard_touches_and_recording) of the MCP chapter lists every tool. + +## That's the week + +Flutter, Compose, Swing, JavaFX and Android code, a bank, a streaming service, phones without Google and a better backend. Next week, we keep going. The [Flutter post](/blog/flutter-better-in-java/) links everything from this week in one place. + +{{< giscus >}} diff --git a/docs/website/content/blog/bastion-hardened-bank-template.md b/docs/website/content/blog/bastion-hardened-bank-template.md new file mode 100644 index 00000000000..4912e71b659 --- /dev/null +++ b/docs/website/content/blog/bastion-hardened-bank-template.md @@ -0,0 +1,207 @@ +--- +title: "Bastion: A Hardened Bank Template, and Crypto Wallets in Core" +slug: bastion-hardened-bank-template +url: /blog/bastion-hardened-bank-template/ +date: '2026-11-02' +author: Shai Almog +description: "Bastion is a virtual bank and small exchange, app and server in one Java project, built as a reference for how to harden a financial app: passkeys, TOTP, signed requests, device keys, pinning, a hash chained audit log and a double entry ledger. Codename One also gained wallet, exchange and custody APIs for Ethereum, Bitcoin and Solana, benchmarked against libsecp256k1 and OpenSSL." +feed_html: "\"The Bastion is a virtual bank and exchange built as a reference for hardening a financial app, and Codename One now has wallet APIs for Ethereum, Bitcoin and Solana." +series: ["release-2026-10-30"] +--- + +![The Bastion banking app and its Security Center](/blog/bastion-hardened-bank-template.jpg) + +A large share of the teams building on Codename One are banks and financial institutions. They don't ask me for a prettier list component. They ask how to pass a penetration test, what the auditor will want to see, and which protections actually work on which device. + +**Bastion** is my answer, written as code. It's a virtual bank and a small exchange, the app and its server in one Java project. Customers hold dollars, euros, bitcoin and ether, move money, issue and freeze cards, trade, and withdraw coins to addresses they've allowed in advance. Staff verify the ledger and the audit trail. It's a template you can brand, but mostly it's a reference: every hardening technique in it is there so you can read how it's done. + +{{< guide-block >}} +
+
The Bastion dashboard
Accounts in four currencies.
+
The BTC/USD market with candles and the order book
The BTC/USD market.
+
Withdrawing coins to an allow-listed address
Withdrawals to allowed addresses.
+
The Bastion Security Center
What is on, on this device.
+
+{{< /guide-block >}} + +## Defense in layers + +No single protection makes an app secure. Bastion stacks them, so an attacker who gets past one meets the next. + +**Signing in.** OAuth authorization code with PKCE, five minute access tokens and rotating twelve hour refresh tokens. A TOTP second factor with ten recovery codes, plus WebAuthn passkeys. Once an account has a passkey, Bastion requires the strongest factor it has. Lockouts and per address rate limits slow down guessing, and changing the password ends every other session. + +**The device.** On first sign in, the app generates a P-256 key pair, registers the public key with the server, and seals the private key with AES-256-GCM under a key kept in the Keychain or Keystore. The app locks after 30 seconds in the background or two minutes idle, and unlocks with biometrics, then a PIN, then a full sign in. + +**Every request that moves money is signed.** Not just authenticated with a token. Signed with the device key, over a canonical form that includes a timestamp and a single use nonce: + +```java {class="cn1-no-try"} +String text = SignedRequest.canonical(method, target, Hash.sha256(body), + idempotencyKey, nonce, ts); +byte[] der = Signature.sign(Signature.SHA256_WITH_ECDSA, privateKey, utf8(text)); +return SignedRequest.header(deviceId, ts, nonce, Base64.encodeUrlSafe(jose(der))); +``` + +A stolen access token can't move money without the device key. A captured request can't be replayed, because the nonce is spent. + +{{< mermaid >}} +sequenceDiagram + participant App + participant Server + App->>App: canonical form: method, path, body hash, nonce, time + App->>App: sign with the device key (P-256) + App->>Server: request + token + Bastion-Signature + Server->>Server: token valid? device registered? + Server->>Server: signature valid? nonce unused? clock in range? + Server->>Server: limits, payee cooling-off, step-up + Server-->>App: posted to the ledger, appended to the audit chain +{{< /mermaid >}} + +**The network.** The app refuses cleartext, talks only to an allow list of hosts, and pins their certificates. That's `AppShield`, a Codename One API, configured in a few lines: + +```java {class="cn1-no-try"} +ShieldConfig shield = new ShieldConfig().collectSignals(true); +for (String host : AppConfig.ALLOWED_HOSTS) { + shield.protect(host, HostPolicy.ENFORCED); +} +AppShield.init(shield); +``` + +**The screen.** Secure windows that keep sensitive screens out of screenshots and the app switcher, tapjacking protection, and a clipboard that clears itself after you copy an account number. + +**Limits.** Per transfer and daily limits, a cooling-off period for new payees, step up authentication above a threshold, and an allow list for crypto withdrawals. + +## The Security Center tells the truth + +Most security settings screens show a column of green checks. Bastion's Security Center shows what's actually on for this device and this build. Some protections need a cloud build: code obfuscation with string encryption and control flow flattening (`cn1.hardenLevel=aggressive`), Play Integrity, jailbreak detection, and blocking screenshots when the app goes to the background. When those aren't active, the Security Center labels them "Cloud build" rather than showing a green check. A test keeps that screen in sync with the build hints, so it can't drift into a lie. + +## The back office auditors want + +Money is a double entry ledger, not a balance column. The admin console has the journal, T-accounts, a trial balance, a balance sheet, an income statement, reconciliation, period close and a maker checker approvals queue. Every event lands in a hash chained audit log, and the console can verify the chain and export evidence as CSV. A read only auditor role sees all of it, on a desktop or on a phone. + +{{< guide-block >}} +
The Bastion admin console overview on the desktop
The back office console.
+{{< /guide-block >}} + +## Crypto wallets, exchanges and custody, in core + +I'm not a fan of crypto currency. I know plenty of people who are, and a few of them are customers. We're here to serve, and if we serve, we should be the best at it. So this isn't a wallet library bolted on the side. It's a big feature: everything a wallet, an exchange or a custodian needs, in core, with no third party library, and the same classes run in the app and in a Codename One backend. + +| Package | What's in it | +| --- | --- | +| `crypto.hd` | BIP-39 recovery phrases, BIP-32 and SLIP-10 derivation, `DerivationPath` | +| `crypto.evm` | EIP-55 addresses; legacy, EIP-2930 and EIP-1559 transactions; EIP-191 and EIP-712 signing; the Solidity ABI and ERC-20 | +| `crypto.bitcoin` | P2PKH, P2SH, P2WPKH, P2WSH and Taproot addresses, scripts, transactions, PSBT and WIF | +| `crypto.solana` | Transactions, the system and SPL token programs, priority fees | +| `crypto.wallet` | `Wallet`, `Signer`, `SeedSigner` and `VaultSigner` | +| `crypto.rpc` | EVM JSON-RPC, Esplora for Bitcoin, Solana JSON-RPC | +| `crypto.market` | Price feeds: CoinGecko, any JSON price API, and a caching decorator | + +### A wallet + +One recovery phrase gives you addresses on every chain, and they match MetaMask, Electrum, Sparrow and Phantom on the standard paths, so a user can restore the same phrase in another wallet and find the same money: + +```java {class="cn1-no-try"} +String words = Mnemonic.generate(256); // 24 words +Wallet wallet = Wallet.fromMnemonic(words, ""); +EvmAddress eth = wallet.getEvmAddress(0, 0); // m/44'/60'/0'/0/0 +BitcoinAddress btc = wallet.getBitcoinAddress(BitcoinNetwork.MAINNET, + Wallet.BIP84, 0, 0, 0); // bc1q... +SolanaAddress sol = wallet.getSolanaAddress(0); // m/44'/501'/0'/0' +``` + +Bitcoin covers legacy, nested SegWit, native SegWit and Taproot, on the main network and on every test network. Sending ether is an EIP-1559 transaction with the fees the node suggests: + +```java {class="cn1-no-try"} +EvmRpc rpc = new EvmRpc(new NetworkTransport(), "https://ethereum-sepolia-rpc.publicnode.com"); +DerivationPath path = Wallet.evmPath(0, 0); +EvmAddress from = wallet.getEvmAddress(path); +long nonce = rpc.getTransactionCount(from, "pending"); +UInt256[] fees = rpc.suggestFees(); // {tip, fee cap} +long gas = rpc.estimateGas(from, to, value, null); + +EvmTransaction tx = EvmTransaction.eip1559(rpc.chainId(), nonce, fees[0], fees[1], + gas, to, value, null); +String hash = rpc.sendRawTransaction(wallet.signEvmTransaction(tx, path)); +``` + +Bitcoin goes through a PSBT, so the same code works with a hardware signer that only sees the partially signed transaction. Taproot inputs are signed with BIP-340 Schnorr automatically: + +```java {class="cn1-no-try"} +EsploraClient.Utxo coin = esplora.getUtxos(from).get(0); +BitcoinTransaction tx = new BitcoinTransaction(); +tx.addInput(coin.getTxid(), coin.getVout()); +tx.addOutput(coin.getValue() - feeSats, BitcoinAddress.parse(recipient, net)); + +Psbt psbt = Psbt.fromTransaction(tx); +psbt.setWitnessUtxo(0, coin.toOutput()); +psbt.signAll(wallet.getBitcoinKey(path)); +psbt.finalizeAll(); +String txid = esplora.broadcast(psbt.extract()); +``` + +Keys are handled the way a security review expects. `VaultSigner` keeps the phrase sealed behind biometrics or a password and refuses to sign while it's locked. `revealMnemonic` returns a `char[]` you can wipe, not a `String` that lingers on the heap, and `SeedSigner.destroy()` wipes the key. + +### An exchange + +An exchange is mostly the server side, and that's what Bastion's backend shows. It derives a deposit address per customer (`m/44'/60'/0'/0/n`), scans new blocks, credits deposits once they're deep enough, and sweeps them to a hot wallet on a separate account. It stops scanning and writes an audit event on a reorganization deeper than its confirmation depth. Before every withdrawal it checks that reserves cover what customers hold, and withdrawals go only to addresses the customer allowed in advance. Order prices are collared against the index price, so a fat finger can't sell bitcoin for a dollar. + +{{< mermaid >}} +flowchart LR + D[Customer deposit address] --> S[Block scanner] + S -->|confirmed| C[Credit the ledger] + C --> H[Sweep to the hot wallet] + W[Withdrawal request] --> L[Limits, allow list, step up] + L --> R{Reserves cover it?} + R -->|yes| T[Sign, wipe the key, broadcast] + R -->|no| A[Audit event, refused] +{{< /mermaid >}} + +Every one of those steps lands in the double entry ledger and the hash chained audit log from the previous section, which is what lets the auditor trace a coin from deposit to withdrawal. + +### Performance + +The expensive parts (secp256k1, Schnorr, Ed25519, the hashes and PBKDF2) run as native C on iOS, macOS, Android, Windows and Linux, with a portable Java engine as the fallback. CI benchmarks every kernel against libsecp256k1, OpenSSL and BouncyCastle on the same machine, and a pull request that makes one slower fails. + +{{< guide-block >}} +
Codename One throughput as a multiple of the reference library's, on the JVM, native and Android
Throughput as a multiple of the reference library. Above 1 is faster.
+{{< /guide-block >}} + +Natively, ECDSA signing matches libsecp256k1 (60,770 against 59,453 signatures a second), and Ed25519 signs 2.44 times as fast as OpenSSL. Even the portable Java engine, which the simulator uses, beats BouncyCastle on the JVM: 1.76 times on public key derivation, 1.49 on ECDSA signing, and 20 times the JDK's own Ed25519 provider. On Android the native engine verifies ECDSA signatures six times as fast as BouncyCastle on ART: + +{{< guide-block >}} +
Both Android engines as a multiple of BouncyCastle on ART
Android: the native and Java engines against BouncyCastle on ART.
+{{< /guide-block >}} + +A phone signs one transaction at a time. An exchange or an indexer checks thousands of signatures at once, so `CryptoBatch` takes a whole batch: + +```java {class="cn1-no-try"} +boolean[] valid = CryptoBatch.verifySecp256k1(hashes, signatures, publicKeys); +``` + +It spreads the batch across every core, and for large batches the GPU too: Metal on Apple, Vulkan elsewhere and WebGPU in the browser, split by measured speed. The GPU never sees a private key. On every core, a batch of 1,024 verifies ECDSA 7.46 times as fast as libsecp256k1 called once per signature, and Ed25519 15.6 times as fast. In the browser, WebGPU verifies ECDSA 8.5 times as fast as the portable engine. + +{{< guide-block >}} +
A native batch on every core as a multiple of the reference library called once per entry
A batch on every core, against the reference library one call at a time.
+
Signatures verified per second by the CPU, the GPU and both together
CPU, GPU and both together on an M4 Max.
+{{< /guide-block >}} + +The GPU's gain on a machine with 12 fast cores is modest, up to 1.63 times for Ed25519, and the chapter says so. Where it pays is a smaller CPU and the browser. Every table and chart is in the [crypto wallets chapter](/developer-guide/crypto-wallets/), with the machine, the library versions and how to rerun them. + +## What's simulated + +Bastion is a reference, not a licensed bank. Deposits, card spending and the market (a random walk) are simulated, and Android integrity checks trust every device in the dev profile. There's no KYC or sanctions screening, no payment rail, no HSM and no regulatory reporting. The [implementor's guide](/developer-guide/bastion-implementors-guide/) has a section for each of those that says what a production deployment needs. On the crypto side, ether can settle on a real Ethereum node, while bitcoin stays a ledger balance in the sample. Only the three chain families above are supported, and the node clients are blocking calls, so keep them off the UI thread. + +## Try it + +Generate Bastion from the [Initializr](/initializr/), or run it from the repository: + +```bash +CN1_PROFILE=dev backend/server.sh run +mvn verify -Psimulator -DskipTests -Dcodename1.platform=javase +``` + +Sign in as `customer@bastion.example` with the password `bastion-demo-password`, then open the Security Center and see what your build actually protects. + +Tomorrow: {{< post-link path="/blog/reel-streaming-and-courses-template" text="Reel: Your Own Streaming Service or Course Platform" >}}. + +{{< giscus >}} diff --git a/docs/website/content/blog/compose-on-more-screens.md b/docs/website/content/blog/compose-on-more-screens.md new file mode 100644 index 00000000000..ec1ab036ef3 --- /dev/null +++ b/docs/website/content/blog/compose-on-more-screens.md @@ -0,0 +1,171 @@ +--- +title: "Your Compose Code, on More Screens and Less CPU" +slug: compose-on-more-screens +url: /blog/compose-on-more-screens/ +date: '2026-10-31' +author: Shai Almog +description: "Kotlin Multiplatform code with Compose now builds as a Codename One app. The Compose runtime ships as it is, the UI layer is ours, and on our benchmark screen it did about a quarter of the CPU work per frame that Compose Multiplatform does (frame rates were equal)." +feed_html: "\"A Kotlin Multiplatform code with Compose now builds as a Codename One app, with about a quarter of the CPU work per frame on our benchmark screen." +series: ["release-2026-10-30"] +--- + +![A Compose schedule screen running on Compose Multiplatform and on Codename One](/blog/compose-on-more-screens.jpg) + +If Codename One didn't exist, Kotlin Multiplatform is probably the platform I'd choose. I prefer Java, but Kotlin is a good language, and KMP was one of the strongest platforms we measured against in the [Flutter post](/blog/flutter-better-in-java/) this week. Its first take on UI leaned on heavyweight native widgets, the way React Native does. The team learned from that, and Compose Multiplatform today is a good product built by people who care about performance. + +So this post isn't about KMP being bad. KMP is good. This is also good, and in a few ways better. Your Compose code now builds as a Codename One app, so it reaches more targets and did less work per frame on our benchmark. You don't have to pick one forever: the same `commonMain` builds both ways. + +## What you keep + +There's no transpiler here. kotlinc compiles your Kotlin for the JVM with the Compose compiler plugin, exactly as it does today. The Compose runtime and `kotlinx.coroutines` are the upstream jars. The UI layer (`androidx.compose.ui`, `foundation`, `material3` and `animation`) is our reimplementation, written over Codename One components and graphics. Then ParparVM compiles everything ahead of time to a native binary. + +{{< mermaid >}} +flowchart LR + C[commonMain Kotlin + Compose] --> K[kotlinc + Compose plugin] + K --> R[Compose runtime, unmodified] + K --> U[Compose UI layer, Codename One] + R --> P[ParparVM, ahead of time] + U --> P + P --> I[iOS] + P --> A[Android] + P --> D[macOS / Windows / Linux, no JVM] + P --> W[Web] + P --> T[watchOS / tvOS] +{{< /mermaid >}} + +`Dispatchers.Main` is the Codename One event dispatch thread, so your coroutines behave the way you expect. Measuring follows Compose's rules: the modifier chain is walked outermost first, under Compose constraints. `LazyColumn` and `LazyRow` compose only what's on screen. + +Bringing a KMP module over is one command: + +```bash +mvn cn1:import-kmp-project -Dcn1.kmp.import=/path/to/MyKmpApp +``` + +It copies `commonMain` and your JVM source set into `common/src/main/kmp`, leaving out the file that opens a desktop window. A whole app is a `ComposeForm`: + +```kotlin +class MyApp : Lifecycle() { + override fun runApp() { + ComposeForm { LayoutScreen() }.show() + } +} +``` + +## The numbers, and what they don't mean + +The benchmark is Keynote, a Compose conference schedule app, built three ways: Compose on Codename One, Compose Multiplatform, and the same app written in plain Java on Codename One. All three were release builds on an iPhone 17 Pro simulator with Xcode 27, five runs each. + +{{< guide-block >}} +
The Keynote schedule rendered by Compose Multiplatform on the left and by Codename One on the right
Keynote: Compose Multiplatform on the left, Codename One on the right.
+{{< /guide-block >}} + +| | Compose on Codename One | Java on Codename One | Compose Multiplatform | +| --- | --- | --- | --- | +| Executable, stripped | 16.8 MB | 8.9 MB | 22.9 MB | +| App bundle | 18.3 MB | 10.2 MB | 22.9 MB | +| Cold start | 961 ms | 805 ms | 1008 ms | +| Warm start, median | 580 ms | 572 ms | 604 ms | +| Recomposition on a day change, median | 4.58 ms | 1.49 ms | 19.24 ms | +| Scroll work per frame, median | 4.04 ms | 1.76 ms | 17.03 ms | +| Frame interval, median | 35.45 ms | 34.79 ms | 33.35 ms | +| Memory footprint | 47 MB | 47 MB | 48 MB | + +{{< guide-block >}} +
Bar charts of recomposition time, scroll work per frame, executable size and cold start for the three builds
Lower is better in every panel.
+{{< /guide-block >}} + +All three apps hit the same frame rate, and Compose Multiplatform is slightly ahead there. On a fast machine, a simple screen keeps up either way. What differs is how much CPU each one burns to get there: about 4 ms a frame for Compose on Codename One against about 17 ms for Compose Multiplatform. On a phone that's battery life, heat, and headroom for the frame where your screen isn't simple. + +A few caveats, because they matter: + +- This is the simulator, not a phone. Scrolling is programmatic, not a finger fling. +- Compose Multiplatform's first frame marker fires before Skia draws, so the start-up figures lean in its favor. +- An empty UIKit app takes 640 to 750 ms to start in this setup, so most of every start-up figure is iOS itself. + +## Why imperative Java is faster and smaller still + +Look at the middle column. The same screen, written imperatively in Java, handles a day change in 1.49 ms, a third of the Compose version on the same engine, and its executable is about half the size. + +The benchmark is tilted toward Compose, too. The Java version was written to mirror the Compose app screen for screen, so it rebinds cards the way a recomposition would, and carries some of that model's overhead. A Java screen written from scratch the Codename One way would do less. + +Declarative UI pays for its model by recomposing, diffing and remembering, which is work an imperative screen never does, because it changes the one label that changed. The usual argument is that the model buys you something back. Here is the same session card both ways. Compose: + +```kotlin {class="cn1-no-try"} +@Composable +fun SessionCard(s: Session, favorite: Boolean, onFavorite: () -> Unit, onOpen: () -> Unit) { + Card(onOpen, Modifier.fillMaxWidth()) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box(Modifier.width(6.dp).height(84.dp).background(Color(s.track.color))) + Column(Modifier.weight(1f).padding(12.dp)) { + Text(s.timeLabel + " - " + s.room, style = MaterialTheme.typography.labelMedium) + Text(s.title, style = MaterialTheme.typography.titleMedium, maxLines = 2) + Text(s.speaker.name + ", " + s.speaker.company, + style = MaterialTheme.typography.bodySmall) + } + FavoriteButton(favorite, onFavorite) + } + } +} +``` + +Codename One, with the styling in CSS where it belongs: + +```java {class="cn1-no-try"} +class SessionCard extends Container { + private final Label time = new Label("", "SessionTime"); + private final Label title = new Label("", "SessionTitle"); + private final Label speaker = new Label("", "SessionSpeaker"); + private final Button heart = new Button("", "Favorite"); + private Session session; + + SessionCard(Schedule app) { + super(new BorderLayout(), "SessionCard"); + add(BorderLayout.CENTER, BoxLayout.encloseY(time, title, speaker)); + add(BorderLayout.EAST, heart); + heart.addActionListener(e -> app.toggleFavorite(session)); + } + + void bind(Session s) { + session = s; + time.setText(s.timeLabel() + " - " + s.room); + title.setText(s.title); + speaker.setText(s.speaker + ", " + s.company); + } +} +``` + +Put them side by side and the claimed advantages don't hold up: + +- **Readability.** Both are a box with three lines of text and a button. Neither is shorter in any way that matters. The Java version names its parts, so `title` is a field you can find, not the second `Text` call in a nested lambda. +- **Testability.** Compose tests find nodes by test tags you add for the purpose. A Codename One test holds the component, calls `bind`, and asserts on `title.getText()`. The [test framework](/developer-guide/testing-with-junit/) and the simulator's MCP tools drive both the same way. +- **Debugging.** This is where imperative code wins outright. Set a breakpoint in `bind` and you see the one call that changed the text, with a stack that leads back to your code. In Compose, the same change runs through the snapshot system, the runtime that schedules recomposition, and generated code from the compiler plugin, and "why did this recompose?" is a question you need a dedicated tool to answer. + +Compose is a fine way to write UI, and if your team already has it, bring it. But it isn't simpler than the imperative code it replaced, and on this screen it costs two to three times the CPU and twice the binary. Codename One lets you choose per screen. Keep Compose where you have it, and write the hot screen in Java: + +```kotlin +val form = Form("Mixed", BorderLayout()) +form.add(BorderLayout.NORTH, Label("Codename One")) +form.add(BorderLayout.CENTER, ComposeView { ListScreen() }) +form.show() +``` + +`ComposeView` is an ordinary `Container`, so a Compose screen and a Codename One component share a form, a theme and an event thread. + +## What's different on our side + +- **More targets.** iOS, Android, the web, native macOS, Windows and Linux with no JVM, plus watchOS and tvOS. The Keynote benchmark itself hasn't been built for watchOS, tvOS, Windows or Linux yet. +- **Java too.** Your team can write Java and Kotlin in one project, and share code with a Java backend. +- **Smaller.** 16.8 MB of executable against 22.9 MB, or 8.9 MB if the screen is in Java. +- **Build errors instead of surprises.** A composable or modifier we haven't implemented fails the build with its original name and source line. It doesn't render wrong on a device. + +## What doesn't work yet + +`LazyColumn(reverseLayout = true)` throws; it isn't implemented. `Box`, `Row` and `Column` aren't inline functions here, as they are upstream, because a public inline function can't call our internal layout code. The Compose runtime's inline functions are JVM 11 bytecode, so the build targets 11. On Gradle, the plugin refuses to build until `org.jetbrains.kotlin.plugin.compose` is applied, and it tells you the line to add. + +## Try it + +The [Kotlin Multiplatform chapter](/developer-guide/kotlin-multiplatform-interop/) covers the import, mixing Compose with Codename One components, and the benchmark. The Keynote source and its harness are in `scripts/kmp-bench` in the [repository](https://github.com/codenameone/CodenameOne). If you run KMP in production, I'd love to hear which composable you hit first that we don't have. + +Tomorrow: {{< post-link path="/blog/swing-and-javafx-on-phones" text="Your Swing and JavaFX Code on iPhone, Android and the Web" >}}. + +{{< giscus >}} diff --git a/docs/website/content/blog/flutter-better-in-java.md b/docs/website/content/blog/flutter-better-in-java.md new file mode 100644 index 00000000000..ada434e1be6 --- /dev/null +++ b/docs/website/content/blog/flutter-better-in-java.md @@ -0,0 +1,310 @@ +--- +title: "Flutter Better in Java" +slug: flutter-better-in-java +url: /blog/flutter-better-in-java/ +date: '2026-10-30' +author: Shai Almog +description: "Codename One now builds Flutter apps from their Dart source, and the result starts faster and uses less memory than Flutter's own release build on Android, macOS, Windows and Linux, and ships less code on all six platforms we measured. This week also covers Kotlin Multiplatform, Swing and JavaFX, classic Android apps, a hardened bank template, a streaming template and phones without Google Play." +feed_html: "\"The Codename One now builds Flutter apps from their Dart source, and the result starts faster, uses less memory and ships less code than Flutter's own release build. This week also covers KMP, Swing and JavaFX, Android apps and more." +series: ["release-2026-10-30"] +--- + +![The Flutter Gallery running in Flutter and in Codename One side by side](/blog/flutter-better-in-java.jpg) + +I usually blog about a feature the week it lands. This one landed a few weeks ago and was in development for months before that: Codename One can now take a Flutter app's Dart source and build it as a Codename One app. The results are amazing. I held this post back because I know how explosive these claims are. I wanted the numbers to come from a CI job that runs on every commit instead of a benchmark I tuned once on my laptop. + +In the meantime we also added support for Kotlin Multiplatform with Compose, for a big part of Swing and JavaFX, and for classic Android apps on iOS. More is coming. The part that still surprises me is how well Codename One does against the originals where we measured them. The same Flutter app, built from the same Dart files, starts faster, uses less memory and ships less code when Codename One builds it. That holds on Android, macOS, Windows and Linux, and the sizes hold on iOS and the web too. + +## The same Dart, two builds + +This is the Flutter Gallery, the Flutter team's own showcase app. On the left it runs as Flutter built it. On the right, Codename One built it from the same Dart source, with no edits. + +{{< guide-block >}} + +{{< /guide-block >}} + +Look closely and you'll see it isn't pixel identical. In the Reply inbox the avatars are circles on the Flutter side and rounded squares on ours. Text wraps a few characters apart in places. The translation isn't perfect, and the [developer guide](/developer-guide/flutter-interop/) says so up front: Dart support is experimental, some callbacks and parameters are accepted and then ignored, and so far we have verified it against one large app. If you bring a Flutter app over, test every interaction your screens depend on. + +But the hard part works. Layout, navigation, theming, text input, lists and the Material and Cupertino widgets all render from the original source, and what you get is a real Codename One app, not a picture of one. + +Bringing a project over is one command in a Codename One project: + +```bash +mvn cn1:import-flutter-project -Dcn1.flutter.import=/path/to/flutter_app +``` + +It copies `lib`, the assets and the fonts, adds the Flutter runtime to the build, writes the main class and tells you which of your pub dependencies are covered. You don't need the Dart or Flutter SDK to build the result. The transpiler is Java and runs inside the normal Maven build. + +## Coming This Week + +Today is the Flutter story and the numbers behind it. Then one post a day: + +- **Saturday, October 31:** {{< post-link path="/blog/compose-on-more-screens" text="Your Compose Code, on More Screens and Less CPU" >}}. Kotlin Multiplatform and Compose, running on Codename One, and why the Java version of the same screen is faster still. +- **Sunday, November 1:** {{< post-link path="/blog/swing-and-javafx-on-phones" text="Your Swing and JavaFX Code on iPhone, Android and the Web" >}}. What carries over, what doesn't, and why you might not miss the parts that don't. +- **Monday, November 2:** {{< post-link path="/blog/bastion-hardened-bank-template" text="Bastion: A Hardened Bank Template, and Crypto Wallets in Core" >}}. A virtual bank and exchange with passkeys, signed requests and a double entry ledger, plus wallets for Ethereum, Bitcoin, and Solana. +- **Tuesday, November 3:** {{< post-link path="/blog/reel-streaming-and-courses-template" text="Reel: Your Own Streaming Service or Course Platform" >}}. Community uploads, courses, DRM and moderation in one Java project. +- **Wednesday, November 4:** {{< post-link path="/blog/android-apps-everywhere-even-without-google" text="GrapheneOS, LineageOS, /e/OS and Others Are Now Officially Supported" >}}. Room, WorkManager and document pickers in the Android layer, and apps that run on GrapheneOS as they are. +- **Thursday, November 5:** {{< post-link path="/blog/backend-caches-certificates-and-a-simulator-that-types" text="Caches, Certificates, Streaming Uploads and a Simulator That Types" >}}. Eight backend upgrades and a simulator with a virtual keyboard and video recording. + +{{< guide-block >}} +
+
A Compose schedule app rendered by Compose Multiplatform and by Codename One
Compose, on Codename One.
+
The Bastion Security Center screen
Bastion's Security Center.
+
The Reel home screen
Reel's catalog.
+
An Android Material screen rendered by Android and by Codename One
Android code, on every target.
+
+{{< /guide-block >}} + +## The numbers + +Every commit to Codename One builds the Flutter Gallery twice: once with Flutter's own release build and once with Codename One. CI measures both on the same machine in interleaved rounds, so a runner that slows down halfway through penalizes both builds equally. The figures below are from the run on October 29. + +{{< guide-block >}} +Cold start to first frame. Android: 395 ms for Codename One against 1247 ms for Flutter. macOS: 609 against 722 ms. Windows: 126 against 205 ms. Linux: 123 against 198 ms. +Memory at rest. Android: 46 MB for Codename One against 79 MB for Flutter. macOS: 37 against 56 MB. Windows: 58 against 92 MB. Linux: 82 against 224 MB. +Executable code. Android: 4.7 MB for Codename One against 20.6 MB for Flutter. iOS: 17.3 against 19.1 MB. macOS: 33.2 against 47.9 MB. Windows: 11.4 against 14.1 MB. Linux: 14.2 against 16.1 MB. Web: 12.9 against 25.2 MB. +{{< /guide-block >}} + +CI collects 26 metrics: installed size, executable code and download size on six platforms, plus cold start and memory at rest on the four where we can measure both sides fairly. Codename One is ahead on all 26. The Android timings come from an emulator, and iOS start-up and memory aren't measured; both are explained below. The biggest gaps are on Android, where the app starts in a third of the time and carries a quarter of the code, and on Linux, where it settles at 82 MB against 224 MB. + +The comparison is tilted toward Flutter, not away from it. The app is Flutter's own showcase, written by the Flutter team in Flutter's idioms, and Codename One runs it as it is. To do that, Codename One carries a runtime that reimplements Flutter's widget, element and layout model on top of Codename One components. That layer is slower than Codename One code written directly; the [Compose post](/blog/compose-on-more-screens/) shows the same effect, where the imperative Java version of a screen does a third of the work of the declarative one. A Codename One app never pays for that layer. We win while paying for it. + +CI also runs eleven compute workloads (hash maps, recursion, string building, sorting, arithmetic and more) in Java and in Dart. On every native platform we measured, Codename One is faster overall, by 1.3x on Android up to 1.9x on Windows and Linux: + +{{< guide-block >}} +Compute speedups for eleven workloads on Android, macOS, Windows and Linux. Codename One is faster or tied in 38 of the 43 measured cells. Flutter wins object allocation on Android (2.3x) and macOS (3.1x), value escape on Android (1.2x), and quicksort on macOS and long arithmetic on Windows by 2% and 6%. Geometric means: Android 1.3x, macOS 1.5x, Windows 1.9x, Linux 1.9x for Codename One. +{{< /guide-block >}} + +Flutter takes 5 of the 43 cells. Two are within 6%. The real one is object allocation on Android and macOS, where Dart's allocator is quick at short lived objects; that's on our list. + +The web is the exception, and it's the one place Flutter is faster. We leave it out of the comparison because Flutter's web build gets some of the workloads wrong: integer arithmetic, random array access and quicksort return different checksums than the same code on every other platform, because Dart on the web runs numbers as JavaScript doubles. A benchmark that gets the answer wrong can't be timed against one that gets it right. On the workloads where both sides agree, Flutter is ahead. The browser is asynchronous by nature, and that's the one environment where Flutter's model helps it rather than costing it. The sizes still favor us there. + +Some things aren't measured, and the charts leave them out rather than guess. Flutter can't compile Dart ahead of time for the iOS simulator, so iOS start-up and memory would compare Flutter's debug engine against our release build. We report iOS sizes only. Web start-up needs a browser driver the benchmark doesn't have yet. Android timings come from an emulator, so they compare two builds on one machine rather than predicting a phone. The method, the harness and the raw numbers are in the [Flutter chapter of the developer guide](/developer-guide/flutter-interop/), and the benchmark lives in `scripts/flutter-bench` in the [repository](https://github.com/codenameone/CodenameOne). + +## How a compatibility layer can be faster than the original + +Each of these compatibility layers is a tool that runs at build time. None of them is a VM inside your app. + +{{< mermaid >}} +flowchart LR + D[Flutter / Dart] -->|transpiled to Java| J + K[Kotlin Multiplatform / Compose] -->|kotlinc + Compose UI layer| J + S[Swing / JavaFX] -->|bytecode remapped| J + A[Android app] -->|resources compiled, bytecode remapped| J + J[Java bytecode + Codename One core] --> P[ParparVM / native ports] + P --> I[iOS] + P --> N[Android] + P --> M[macOS / Windows / Linux] + P --> W[Web] + P --> O[watchOS / tvOS] +{{< /mermaid >}} + +The Dart transpiler reads your widget source during `generate-sources` and writes Java. A runtime library then implements Flutter's layout and composition model (constraints down, sizes up, elements reconciled against widgets) on top of Codename One containers. It does not reimplement Flutter's rasterizer. Painting is ours. Compose works the same way: kotlinc compiles your Kotlin, the Compose runtime ships unmodified, and only the UI layer is ours. Swing, JavaFX and Android apps compile against their usual APIs, and a remap step points the bytecode at our implementations. + +So the overhead is real, and it's added on top of Codename One. The reason the result still wins is what's underneath. ParparVM compiles Java ahead of time to C and then to a native binary, and its dead code elimination drops whatever your app doesn't reach. There's no Dart VM, no Skia or Impeller engine and no second runtime in the bundle. That's how the Android build ends up with 4.7 MB of executable code against 20.6 MB. + +Here is the Dart that calls into the platform. It's ordinary Flutter code: + +```dart +final batteryChannel = MethodChannel('com.example.app/battery'); + +Future getBatteryLevel() async { + final level = await batteryChannel.invokeMethod('getBatteryLevel'); + return level as int; +} +``` + +And here is the other side of that channel, in plain Java, with no plugin and no per-platform Swift or Kotlin: + +```java {class="cn1-no-try"} +MethodChannel channel = new MethodChannel("com.example.app/battery"); +channel.setMethodCallHandler(new MethodChannel.MethodCallHandler() { + @Override + public void onMethodCall(MethodCall call, MethodChannel.Result result) { + if ("getBatteryLevel".equals(call.method())) { + result.success(87); + } else { + result.notImplemented(); + } + } +}); +``` + +You don't have to move the whole app either. `FlutterUI.wrap()` returns an ordinary `Container`, so a Dart screen can sit inside a Codename One form next to components you wrote in Java: + +```java {class="cn1-no-try"} +Form dashboard = new Form("Dashboard", BoxLayout.y()); +dashboard.add(new Label("Written in Codename One")); +dashboard.add(FlutterUI.wrap(new Text("Rendered from Dart"))); +dashboard.add(new Button("Also Codename One")); +dashboard.show(); +``` + +## Similar in spirit + +Flutter and Codename One made the same bet. Neither wraps the platform's widgets. Both draw every component themselves on a lightweight surface, so you control every pixel and the behavior doesn't drift between platforms. Codename One still ships native themes, so an app can look like iOS on an iPhone and like Material on Android, or look like your brand everywhere. That shared architecture is why a Flutter widget tree maps onto Codename One so naturally. The differences are in what sits around that core: the threading model, how apps are built and how features ship. + +## Can't Flutter do the same? + +No. Flutter could, in theory, run a Codename One app the way we run a Flutter app. It would be slow, because the two models point in opposite directions. + +Flutter is asynchronous to the core. Dart runs in an event loop, platform calls are messages that come back as futures, and the engine draws on its own threads. Codename One is synchronous: your code calls the platform and gets an answer. Emulating a synchronous API on top of an asynchronous one means queuing, waiting and copying on every call. Going the other way is cheap: a future is a callback, and a callback is a method call. + +The same reason explains why Flutter can't catch Codename One on most platforms. Operating systems, graphics APIs and hardware are synchronous. Codename One calls them directly. Flutter goes through a message channel and its own engine to reach them. + +Android makes it obvious. The native platform on Android is Java. Codename One compiles to it and talks to Android the way Android expects. Flutter avoids it, shipping its own VM and engine beside the one the phone already has. That's why Android shows the biggest gap: a third of the start-up time and a quarter of the code. + +## Two differences you feel every day + +The threading model shows up in the benchmarks. These two don't, but you notice them every day you work. + +### Builders + +A Codename One app is a Java project. The build of a native app happens in a builder: our cloud build servers or a local build, which take your compiled code and produce the iOS, Android or desktop binary. That moves the hard part of the developer experience into our hands: + +- **Build iOS apps from Windows or Linux.** The cloud builder holds the Mac, Xcode and the signing steps. You don't need to own a Mac to ship to the App Store. +- **Cheap CI.** Your unit and UI tests run in Java, in the simulator, on any CI runner. No emulator to boot and no macOS runner to pay for on every commit. +- **New features without migration work.** When Apple or Google change a requirement, or we add a capability, we update the builder. Your project picks it up on the next build, with nothing to upgrade or reconfigure. + +### Everything in one place + +Codename One used to aim for a modular core, with features broken out into cn1libs. Flutter is going through that phase now, moving pieces such as its Material and Cupertino themes out of the framework into independent packages. We went the opposite way. The Codename One core now covers what Flutter does, plus most of what people reach for in Flutter's most popular packages, and this week's posts add more. + +A bigger core costs you nothing, because you don't pay for what you don't use, and that works on two levels. ParparVM drops every class your app doesn't reach. The builder goes further: it looks at what your compiled code calls and adds a native library only when a feature needs it. This week's crypto support is a good example. Its native kernels for secp256k1, Ed25519 and the hashes are what make it fast, and an app that never touches the crypto API shouldn't carry them. It doesn't. Call a wallet or a signature API and the builder links them in, and you get native speed that our CI measures against the reference libraries, libsecp256k1 and OpenSSL among them. That's how the builds above stay smaller than Flutter's. What you get instead: + +- **One version.** No matrix of package versions to keep compatible, and no upgrade that breaks because two packages disagree. +- **Nothing abandoned.** No package your app depends on that its author stopped maintaining two years ago. +- **A short supply chain.** This is the big one. Every package in a dependency tree is a person you trust with your users' devices. A maintainer who burns out, sells the package, gets angry or gets hacked can push code into your app on the next update, and attacks on open source packages have done exactly that. With the features in core, your app's dependencies are one project with one review process, not a tree of packages from strangers. That fits the rest of this week: the [Bastion](/blog/bastion-hardened-bank-template/) template is a hardened bank because security is something we take seriously, and banks and other regulated organizations build on Codename One. For them, a short supply chain is a requirement. +- **Everything moves together.** When we fix rendering, add a platform or change the build, every feature comes along, including the niche ones. +- **One source of truth.** The developer guide documents all of it, and when something breaks there is one team responsible for the answer. + + +## We can finally show the code + +For years, every Codename One against Flutter discussion went the same way. I'd explain why ahead of time compiled Java with a lightweight UI should produce a smaller, faster app. Someone would explain why it shouldn't. Neither of us had the same app built both ways, so it came down to which camp you were already in. + +Now there's one app, built both ways, measured on every commit. If you think a number is wrong, the harness is in the repository and you can rerun it. + +The part I didn't expect is how much this made Codename One better. Each compatibility layer exposed problems we had missed for years: + +- **Styles were parsed over and over.** Every component re-parsed its pressed and disabled styles from the theme. Caching them took component creation in the Gallery from 131 ms to 17 ms and resident memory from 247 MB to 144 MB. +- **Every string constant was created at startup.** ParparVM materialized every string literal before `main()` ran. Making the pool lazy cut that step from 119 ms to 46 ms. +- **Rounded images were copied.** Rounding an image's corners built a second bitmap, 32 times during the first frame. Doing it in the fragment shader removed the copies and stopped memory from swinging between runs. +- **Images decoded inside the first frame.** Deferring that decode took the first frame from 302 ms to 213 ms. +- **An idle app burned CPU.** The Compose work found, and we fixed, a bug where on phones with a notch, safe area padding kept queuing layout so an idle app sat at 14% to 26% CPU. + +None of these fixes are specific to Flutter. If you write Codename One apps in plain Java and never touch a Dart file, your app gets these fixes too: faster start-up, lower memory and an idle CPU that stays idle. + +## More This Week + +### Your Compose code, on more screens + +Kotlin Multiplatform is a good platform, and Compose Multiplatform is well built. Codename One now runs the same Compose code on more targets, used about a quarter of the CPU per frame on our benchmark screen, and lets you drop to Java where it counts. + +{{< guide-block >}} +
The same Compose schedule screen rendered by Compose Multiplatform and by Codename One
The same Compose screen: Compose Multiplatform on the left, Codename One on the right.
+{{< /guide-block >}} + +```kotlin +class MyApp : Lifecycle() { + override fun runApp() { + ComposeForm { LayoutScreen() }.show() + } +} +``` + +Saturday's {{< post-link path="/blog/compose-on-more-screens" text="Compose article" >}} has the numbers and their caveats. + +### Swing and JavaFX on phones + +A large share of real Swing and JavaFX code now builds for iOS, Android and the web: the samples we test include SwingSet2 and Notepad, the JDK's own demos. On Linux the native build is 4 to 6 times smaller zipped than the same app on the JVM, and starts faster in all 8 apps, even against a JVM tuned with AppCDS. + +{{< guide-block >}} +
+
SwingSet2, the JDK Swing demo, running as a Codename One app on an iPad
SwingSet2, the JDK demo, on an iPad.
+
The open source JavaFX 2048 game running as a Codename One app on an Android phone
The JavaFX 2048 game on a phone.
+
+{{< /guide-block >}} + +{{< guide-block >}} +
+
Swing controls running on an iPhone
Swing controls.
+
A JTable running on an iPhone
JTable.
+
JavaFX controls running on an iPhone
JavaFX.
+
An FXML form running on an iPhone
FXML and CSS.
+
+{{< /guide-block >}} + +Sunday's {{< post-link path="/blog/swing-and-javafx-on-phones" text="Swing and JavaFX article" >}} goes through what carries over and why you won't miss what doesn't. + +### A bank you can study, and wallets in core + +A large share of our users are banks and financial institutions. **Bastion** is a reference implementation for them: a virtual bank and small exchange with passkeys, TOTP, signed requests, device keys, a hash chained audit log, a double entry ledger and an auditor console. Codename One also gained wallet APIs for Ethereum, Bitcoin, and Solana. + +{{< guide-block >}} +
+
The Bastion dashboard
Four currencies.
+
The BTC/USD market with candles and the order book
The BTC/USD market.
+
The Bastion Security Center
The Security Center.
+
+{{< /guide-block >}} + +```java {class="cn1-no-try"} +Wallet wallet = Wallet.fromMnemonic(words, ""); +EvmAddress eth = wallet.getEvmAddress(0, 0); +BitcoinAddress btc = wallet.getBitcoinAddress(BitcoinNetwork.MAINNET, Wallet.BIP84, 0, 0, 0); +SolanaAddress sol = wallet.getSolanaAddress(0); +``` + +Monday's {{< post-link path="/blog/bastion-hardened-bank-template" text="Bastion article" >}} covers the hardening, one layer at a time. + +### Streaming and courses + +**Reel** is a streaming service and its server in one Java project. Switch a preset and it becomes a subscription service, a creator platform in the style of YouTube, or an academy with courses, lessons, and quizzes. {{< guide-block >}} +
+
The Reel catalog
The catalog.
+
The Reel player
The player.
+
A Reel course
A course.
+
+{{< /guide-block >}} + +```java {class="cn1-no-try"} +StreamRequest req = new StreamRequest(manifestUrl) + .setDrm(new DrmConfig(DrmScheme.WIDEVINE, licenseUrl)); +``` + +Tuesday's {{< post-link path="/blog/reel-streaming-and-courses-template" text="Reel article" >}} walks through uploads, moderation, and DRM. + +### Android code everywhere, including GrapheneOS + +The Android compatibility layer now covers Room, WorkManager, location, sensors, foreground services and the document picker intents, all running on iOS too. And GrapheneOS, LineageOS, CalyxOS and /e/OS are now officially supported, with a boundary CI verifies: one APK checks for Play Services at run time, and push works through UnifiedPush. + +{{< guide-block >}} +
+
A Material 3 screen on Android and on Codename One
Material 3, Android left, Codename One right.
+
A RecyclerView on Android and on Codename One
RecyclerView, same source.
+
+{{< /guide-block >}} + +Wednesday's {{< post-link path="/blog/android-apps-everywhere-even-without-google" text="Android article" >}} has the details. + +### A modern backend, and a simulator that types + +{{< guide-block >}} + +{{< /guide-block >}} + +Thursday closes the week with eight backend upgrades (a bounded cache, outbound OAuth tokens, PKI, JSON codecs, streaming uploads, error advice, `java.time` in the ORM and test slices) and a simulator that slides up a virtual keyboard and records video. See the {{< post-link path="/blog/backend-caches-certificates-and-a-simulator-that-types" text="backend and simulator article" >}}. + +## The universal platform + +Last week I made the case that [a Java word processor beats its Rust twin](/blog/rewrite-everything-in-java-not-rust/). This week the swing is bigger. Flutter, Compose, Swing, JavaFX and Android code all build into the same kind of app and run on iOS, Android, desktop, the web and the watch. + +Instead of coding to an operating system, you code to Codename One, and Codename One becomes the operating system. The usual price for that kind of abstraction is performance. We don't charge it. You get more performance, more features, more compatibility and more platforms, and it's free and open source. + +If you have a Flutter app, run the import on a copy and build it for the simulator. Tell us which screen breaks first; that's the most useful bug report we can get right now. If you're on Kotlin, Swing, JavaFX or Android, the rest of this week is for you. And if you think a number above is wrong, the benchmark is in the repository. Rerun it and open an issue. + +{{< giscus >}} diff --git a/docs/website/content/blog/reel-streaming-and-courses-template.md b/docs/website/content/blog/reel-streaming-and-courses-template.md new file mode 100644 index 00000000000..5d6b29eb8c6 --- /dev/null +++ b/docs/website/content/blog/reel-streaming-and-courses-template.md @@ -0,0 +1,153 @@ +--- +title: "Reel: Your Own Streaming Service or Course Platform" +slug: reel-streaming-and-courses-template +url: /blog/reel-streaming-and-courses-template/ +date: '2026-11-03' +author: Shai Almog +description: "Reel is a streaming service and its server in one Java project. Flip a preset and it's a subscription service, a creator platform with community uploads, or an academy with courses and quizzes. Codename One also gained a streaming API with DRM: AES-128 HLS, ClearKey, Widevine and FairPlay." +feed_html: "\"The Reel is a streaming service and its server in one Java project: a subscription service, a creator platform or a course academy, with a new DRM streaming API." +series: ["release-2026-10-30"] +--- + +![The Reel streaming app catalog and player](/blog/reel-streaming-and-courses-template.jpg) + +Video is how a lot of businesses now talk to their customers. A gym sells classes, a university publishes lectures, a creator community wants its own home instead of renting space on someone else's platform. They all need the same pieces: a catalog, a player that remembers where you stopped, uploads, moderation, payments and some protection for paid content. + +**Reel** is those pieces, the app and its server in one Java project. Like last week's [Skirmish, Parley and Savor](/blog/rewrite-everything-in-java-not-rust/), it's a template you generate and brand, not a demo you admire. + +{{< guide-block >}} +
+
The Reel catalog
The catalog.
+
The Reel player with audio and subtitle tracks
Audio and subtitle tracks.
+
A Reel course with its lessons
A course and its lessons.
+
The Reel moderation queue
The moderation queue.
+
+{{< /guide-block >}} + +## One template, three businesses + +Reel's behavior is driven by presets you switch at run time, with no rebuild: + +- **Subscription service.** Plans or rentals, profiles, downloads, resume across devices. +- **Creator platform.** Anyone with the creator role uploads video, the server packages it, and moderators approve it before it's public. Think of a community YouTube for your niche. +- **Academy.** Courses, lessons and quizzes, for training and tutorials. + +On top of the preset, toggles turn ads, rentals, downloads, uploads, courses, quizzes and polls on or off. The server assigns roles (viewer, creator, moderator and admin) and the app shows each person the screens their role allows. + +## Uploads and moderation + +Creators upload from the app and the server packages each upload for streaming and checks it against blocklists the moderators maintain. A title only goes live after a moderator approves it, and a moderator can take it down later. Takedowns take effect mid-stream: the player's heartbeat checks with the server, so a removed title stops playing instead of running to the end. + +{{< mermaid >}} +flowchart LR + C[Creator uploads] --> P[Server packages HLS] + P --> B[Blocklist checks] + B --> Q[Moderation queue] + Q -->|approve| L[Live in the catalog] + Q -->|reject| R[Back to the creator] + L -->|takedown| X[Stops on the next heartbeat] +{{< /mermaid >}} + +## A streaming API with DRM, in core + +Reel needed playback for protected video, and that belongs in the framework, not in one template. So Codename One now has a streaming API in `com.codename1.media`. It's small on purpose: + +| Class | What it does | +| --- | --- | +| `StreamRequest` | An HLS manifest or a progressive file URL, plus a start position, request headers, a completion callback and an optional DRM configuration. `create()` or `createAsync()` returns an ordinary `Media`. | +| `DrmConfig` | The scheme and license server URL, headers sent with every license request, a FairPlay certificate URL and a content id. | +| `DrmScheme` | `AES_128` (encrypted HLS), `CLEARKEY`, `WIDEVINE` and `FAIRPLAY`. | +| `MediaManager.isDrmSupported(scheme)` | Whether this device can play a scheme, asked before you open the stream. | + +Opening a protected stream that resumes where the viewer stopped: + +```java {class="cn1-no-try"} +StreamRequest req = new StreamRequest(manifestUrl) + .setStartPosition(resumeSeconds * 1000) + .setDrm(new DrmConfig(DrmScheme.CLEARKEY, licenseUrl) + .addLicenseHeader("X-Reel-License", licenseToken)); +req.createAsync().ready(media -> { + player.add(BorderLayout.CENTER, media.getVideoComponent()); + player.revalidate(); +}); +``` + +Each port maps the scheme to what the device has: AVPlayer decrypts AES-128 HLS and plays FairPlay on Apple, and Android's player and `MediaDrm` handle AES-128 HLS, ClearKey, and Widevine where the device has it. Devices differ, so Reel asks first and sends the server the schemes this device can play, strongest first. The server picks one and returns the matching manifest and license URL: + +```java {class="cn1-no-try"} +List out = new ArrayList(); +DrmScheme[] order = {DrmScheme.WIDEVINE, DrmScheme.FAIRPLAY, DrmScheme.CLEARKEY, DrmScheme.AES_128}; +for (int iter = 0; iter < order.length; iter++) { + if (MediaManager.isDrmSupported(order[iter])) { + out.add(order[iter].name()); + } +} +``` + +{{< mermaid >}} +sequenceDiagram + participant App + participant Server + participant License as License server + App->>App: isDrmSupported for each scheme + App->>Server: play title, supported schemes + Server-->>App: manifest URL, scheme, license URL, media token + App->>License: license request + X-Reel-License + License-->>App: key (ClearKey) or license (Widevine, FairPlay) + loop every few seconds + App->>Server: heartbeat, position + Server-->>App: keep playing, or stop (takedown, stream limit) + end +{{< /mermaid >}} + +Out of the box, Reel packages each video once, as AES-128 HLS and as CENC encrypted MP4 for ClearKey, under one key. Keys are issued only against a license token tied to the session, and every playlist, segment and key request carries a media token that expires and is scoped to one video and a maximum quality. Concurrent stream limits are enforced by the heartbeat. Protected video also blocks screenshots and screen recording, and can add a watermark with the account name. + +I'll be direct about the limit: AES-128 and ClearKey stop casual copying, not a determined attacker, because the key reaches the device. For studio grade protection you plug in a Widevine or FairPlay license vendor through `reel.drm.widevine.url` and `reel.drm.fairplay.url`. The server forwards the license challenge, and the app code doesn't change. + +## Audio tracks and subtitles + +{{< guide-block >}} +
+
A Reel lesson playing with Spanish subtitles
Subtitles over a lesson.
+
Reel subtitle language and size settings
Language and size settings.
+
Adding a WebVTT subtitle track in the Reel studio
A creator adds a track per language.
+
+{{< /guide-block >}} + +Creators add a subtitle track per language in the studio, as WebVTT. The server checks that it really is WebVTT and stores it beside the video, not inside the stream. + +That last part is a deliberate choice. A platform player can render text tracks, but each one finds them differently, styles them differently, and some can't see a track inside an encrypted stream at all. So Reel draws subtitles itself, over the picture, and they look the same on every device and under every DRM scheme. The parser is about a hundred lines of plain Java in the template: it reads each cue's start and end time and its text, and drops WebVTT's positioning and inline tags such as `` and ``: + +```java {class="cn1-no-try"} +Captions.load(track.url(), loaded -> { + if (wanted.equals(subtitleLanguage)) { + captions = loaded; + updateCaption(); + } +}); + +private void updateCaption() { + String text = captions == null ? "" : captions.at(media.getTime()); + if (!text.equals(caption.getText())) { + caption.setText(text); + } +} +``` + +`captions.at(millis)` returns the cues that cover that moment, one per line, and the player calls it as the position moves. Switching subtitles in the player's **Audio & subtitles** sheet takes effect at once, because nothing about the stream changes. Switching the audio language or the quality reopens the stream at the same position, and the server marks the viewer's preferred audio track as the playlist default, so it starts in the right language. + +## Payments and the rest + +Payments are simulated in the dev profile; the test card 4242 4242 4242 4242 goes through. Set `reel.payments.provider=stripe` for Stripe, and store purchases are verified with Apple and Google. Push goes through the Codename One push service. Reel also runs in a browser with nothing installed, which is how many viewers will first meet your service. + +{{< guide-block >}} +
The Reel catalog on a desktop screen
The same app on a desktop.
+{{< /guide-block >}} + +## Try it + +Generate Reel from the [Initializr](/initializr/). The [user's guide](/developer-guide/reel-users-guide/) walks through every screen, and the [implementor's guide](/developer-guide/reel-implementors-guide/) covers the packaging pipeline, DRM vendors and what to change before you launch. The template source is free under MIT-0, with no royalties and no revenue share. + +Tomorrow: {{< post-link path="/blog/android-apps-everywhere-even-without-google" text="GrapheneOS, LineageOS, /e/OS and Others Are Now Officially Supported" >}}. + +{{< giscus >}} diff --git a/docs/website/content/blog/swing-and-javafx-on-phones.md b/docs/website/content/blog/swing-and-javafx-on-phones.md new file mode 100644 index 00000000000..1cbe1af7411 --- /dev/null +++ b/docs/website/content/blog/swing-and-javafx-on-phones.md @@ -0,0 +1,141 @@ +--- +title: "Your Swing and JavaFX Code on iPhone, Android and the Web" +slug: swing-and-javafx-on-phones +url: /blog/swing-and-javafx-on-phones/ +date: '2026-11-01' +author: Shai Almog +description: "Codename One now builds a large share of Swing, SwingX and JavaFX application code for iOS, Android, the web and native desktop. It can't run everything, and this post says exactly what it can't. What you get in return is a native build 4 to 6 times smaller and APIs desktop Java never had." +feed_html: "\"Swing Codename One now builds a large share of Swing and JavaFX application code for phones, the web and native desktop. What works, what doesn't, and what you get in return." +series: ["release-2026-10-30"] +--- + +![Swing and JavaFX code running as a native app](/blog/swing-and-javafx-on-phones.jpg) + +There's a lot of Swing and JavaFX code in the world. Much of it is internal tooling and line of business software that works fine and that nobody wants to rewrite. Then someone asks for it on an iPad, or in a browser, and the answer has always been "rewrite it." + +Codename One can now build a large share of that code for iOS, Android, the web and native desktop, as a native app with no JVM inside. SwingSet2 and Notepad, the JDK's own demos, both build and run. This post shows what carries over, what it costs in size and start-up (less than the JVM), and why the parts that don't carry over have better replacements in a client app. + +{{< guide-block >}} +
+
SwingSet2, the JDK Swing demo, running as a Codename One app on an iPad
SwingSet2, the JDK demo, on an iPad.
+
The open source JavaFX 2048 game running as a Codename One app on an Android phone
The JavaFX 2048 game on a phone.
+
+{{< /guide-block >}} + +## What it does + +You drop the project's `src/main` into a Codename One project, or import it: + +```bash +mvn cn1:import-desktop-project -Dcn1.desktop.import=/path/to/MyDesktopApp +``` + +Your code keeps compiling against the JDK's own Swing, or against our JavaFX API under the real `javafx.*` names. A remap step after javac points the bytecode at our implementations of `java.awt`, `javax.swing`, `java.beans`, `javax.imageio`, a SwingX subset and a JavaFX subset, all written on top of Codename One. FXML files compile to Java classes at build time, and JavaFX CSS compiles to a table. Nothing parses XML on the device. + +{{< mermaid >}} +flowchart LR + S[Your Swing / JavaFX source] --> J[javac against the usual APIs] + J --> R[remap: java.awt, javax.swing, javafx to Codename One] + F[.fxml and .css] --> X[compiled at build time] + R --> C[compliance check: unsupported API is a build error] + X --> C + C --> P[ParparVM native builds] +{{< /mermaid >}} + +{{< guide-block >}} +
+
Swing controls running on an iPhone
Swing controls.
+
A JTable and a JXTable running on an iPhone
JTable and JXTable.
+
JavaFX controls running on an iPhone
JavaFX controls.
+
An FXML form styled with CSS running on an iPhone
FXML with its CSS.
+
+{{< /guide-block >}} + +Unmodified desktop code on an iPhone, captured by our screenshot tests. + +Our test set includes two JDK demos, SwingSet2 and Notepad, along with a Swing gallery, a falling blocks game, a JavaFX gallery, an open source 2048 game, an address book and a Mandelbrot explorer. + +## The numbers + +On Linux, the same app as a `jlink` image against a Codename One native build: + +| Application | JVM image, zipped | Native, zipped | Smaller by | +| --- | --- | --- | --- | +| SwingSet2, the JDK demo | 34.1 MB | 8.2 MB | 4.2x | +| Notepad, the JDK demo | 32.8 MB | 6.5 MB | 5.0x | +| JavaFX gallery | 39.1 MB | 7.1 MB | 5.5x | +| Address book (JavaFX) | 39.1 MB | 6.5 MB | 6.0x | + +{{< guide-block >}} +
Zipped download size on Linux: a jlink JVM image against the Codename One native build, per sample
Download size, zipped.
+
Cold start on Linux: each sample on the default JVM, a tuned JVM and the Codename One native build
Cold start on Linux.
+{{< /guide-block >}} + +From process start to the first painted frame, the native build starts faster than the default JVM in all 8 apps, by 1.5x to 5.5x. Against a JVM tuned with the serial GC, C1 only and an AppCDS archive, it's still faster in all 8, by 1.1x to 3.6x. The closest is the falling blocks game, at 86 ms native against 93 ms tuned. The JavaFX gallery starts in 116 ms native against 637 ms on the default JVM. The same apps build for iOS at 25 to 29 MB and as 2.8 to 4.4 MB Android APKs. + +Memory is closer than you might expect. At idle the native Swing gallery holds 64.5 MB resident against 84.2 MB on a tuned JVM. After a scripted session of clicks and drags it's 90.8 MB against 86.0 MB. + +## What isn't supported + +This is the important part, so here it is without softening: + +- **No binary drop in.** A third party jar that was compiled against `java.awt`, `javax.swing` or `javafx` fails the build. We don't relocate other people's jars. If the library ships source, you can bring the source. +- **No class loaders, `ServiceLoader`, modules or OSGi.** +- **No runtime reflection.** `java.lang.reflect` and `Class.getMethod` are build errors. `Class.forName` by string doesn't work on Android or the web. +- **No Java serialization, `HttpURLConnection`, `java.net.http` or raw sockets** through these layers. +- **`ProcessBuilder` doesn't exist on a phone.** Check `ProcessBuilder.isSupported()`. +- **Only a subset of the API.** Swing is big. When you call something we don't have, the build tells you exactly what and where: + +``` +com/example/OrdersPanel#restyleTree()V -> javax.swing.JTree.getUI() is not +supported by the Codename One Swing compatibility layer +(com/example/OrdersPanel.class) at OrdersPanel.java:48 +``` + +That error is the point. You find out at build time, on your machine, with a file and line number. You don't find out from a user on an iPad. + +## Why pay for what you don't need? + +Look at that list again and ask where those features show up in a typical desktop app. Reflection usually powers a binding framework, a serialization library or a dependency injector. Class loaders power plugins. Serialization powers a cache or a save file. + +Codename One does those jobs at build time instead. Binding a form to a model is generated code, with no reflection and no listener bookkeeping: + +```java {class="cn1-no-try"} +@Bindable +public class LoginModel { + @Bind(name = "userField", attr = BindAttr.TEXT) + @Required + private String user; + public String getUser() { return user; } + public void setUser(String u) { this.user = u; } + + @Bind(name = "rememberMe", attr = BindAttr.SELECTED) + public boolean remember; +} +``` + +The same goes for JSON and XML mapping, REST and GraphQL clients and the SQLite ORM. They're all generated by the build, so they're type checked, they survive dead code elimination, and they cost nothing at startup. Reflection is what makes a JVM app big and slow to start. Removing it is a large part of why the native build is 4 to 6 times smaller. + +## What desktop Java never gave you + +You don't have to wait for a phone to get something out of this. On the desktop, today, the same app picks up things Swing developers have fought for years to get, and that JavaFX offers only as its own aging WebKit fork: + +- **A real browser.** [`BrowserComponent`](/developer-guide/working-with-javascript/) embeds the system's engine: WebKit on the Mac, WebView2 on Windows and WebKitGTK on Linux, with two way calls between Java and JavaScript. No bundled Chromium, no `JEditorPane` rendering HTML 3.2. +- **The camera.** [`Capture`](/developer-guide/miscellaneous-features/#_capture_photos_video_audio) takes photos and video from the webcam on macOS, Windows and Linux. In Swing that has meant a native library and a prayer. +- **Bluetooth LE.** The [Bluetooth API](/developer-guide/bluetooth/) talks to real devices on all three desktops, so the tool that configures your hardware no longer needs a vendor SDK or a serial port bridge. +- **Maps.** A [vector `MapView`](/developer-guide/maps/) drawn by Codename One itself, with markers and shapes, the same on the desktop as on a phone. +- **The web.** The same app builds for the browser, so the internal tool people install today can also be a link, with nothing installed. +- **Generated network clients.** Point [`cn1:generate-openapi`](/developer-guide/maven-appendix-goals/#_generate_openapi_client_generate_openapi) at an OpenAPI 3 spec and you get a typed client. GraphQL and gRPC have goals of their own, and [OAuth and sign in with Google or Apple](/developer-guide/authentication-and-identity/) are built in. +- **SQLite with an ORM** whose mapping mistakes fail the build, **CSS styling** with light and dark themes and live reload, and **a Java backend** in the same project, sharing your types. + +And when the app does reach a phone, [push](/developer-guide/push-notifications/), [biometrics](/developer-guide/biometric-authentication/), [NFC](/developer-guide/near-field-communication/) and [location](/developer-guide/miscellaneous-features/#_location_gps) are there too. + +`SwingInterop.asComponent()` and `FxInterop.asComponent()` wrap a Swing component or a JavaFX node as a Codename One component, so you can migrate one panel at a time and put a Codename One map or camera view next to your existing table. + +## Try it + +The [desktop compatibility chapter](/developer-guide/desktop-interop/) has the full list of what's covered, the import steps and the benchmark method. Start with the screen your users ask for on mobile, run the import and read the build errors. They'll tell you how far you are. + +Tomorrow: {{< post-link path="/blog/bastion-hardened-bank-template" text="Bastion: A Hardened Bank Template, and Crypto Wallets in Core" >}}. + +{{< giscus >}} diff --git a/docs/website/static/blog/android-apps-everywhere-even-without-google.jpg b/docs/website/static/blog/android-apps-everywhere-even-without-google.jpg new file mode 100644 index 00000000000..503dedbfafa Binary files /dev/null and b/docs/website/static/blog/android-apps-everywhere-even-without-google.jpg differ diff --git a/docs/website/static/blog/backend-caches-certificates-and-a-simulator-that-types.jpg b/docs/website/static/blog/backend-caches-certificates-and-a-simulator-that-types.jpg new file mode 100644 index 00000000000..be690de1c37 Binary files /dev/null and b/docs/website/static/blog/backend-caches-certificates-and-a-simulator-that-types.jpg differ diff --git a/docs/website/static/blog/bastion-hardened-bank-template.jpg b/docs/website/static/blog/bastion-hardened-bank-template.jpg new file mode 100644 index 00000000000..e4e7e34cb2c Binary files /dev/null and b/docs/website/static/blog/bastion-hardened-bank-template.jpg differ diff --git a/docs/website/static/blog/compose-on-more-screens.jpg b/docs/website/static/blog/compose-on-more-screens.jpg new file mode 100644 index 00000000000..56785d74627 Binary files /dev/null and b/docs/website/static/blog/compose-on-more-screens.jpg differ diff --git a/docs/website/static/blog/flutter-better-in-java.jpg b/docs/website/static/blog/flutter-better-in-java.jpg new file mode 100644 index 00000000000..758c7f73609 Binary files /dev/null and b/docs/website/static/blog/flutter-better-in-java.jpg differ diff --git a/docs/website/static/blog/reel-streaming-and-courses-template.jpg b/docs/website/static/blog/reel-streaming-and-courses-template.jpg new file mode 100644 index 00000000000..b197e7916a8 Binary files /dev/null and b/docs/website/static/blog/reel-streaming-and-courses-template.jpg differ diff --git a/docs/website/static/blog/release-2026-10-30/flutter-code-size.svg b/docs/website/static/blog/release-2026-10-30/flutter-code-size.svg new file mode 100644 index 00000000000..fc120b3d764 --- /dev/null +++ b/docs/website/static/blog/release-2026-10-30/flutter-code-size.svg @@ -0,0 +1,43 @@ + + +Executable code +The Flutter Gallery, same Dart source, both release builds. Lower is better. +Codename One +Flutter +Android + +4.7 MB + +20.6 MB +4.4x +iOS + +17.3 MB + +19.1 MB +1.1x +macOS + +33.2 MB + +47.9 MB +1.4x +Windows + +11.4 MB + +14.1 MB +1.2x +Linux + +14.2 MB + +16.1 MB +1.1x +Web + +12.9 MB + +25.2 MB +2.0x + \ No newline at end of file diff --git a/docs/website/static/blog/release-2026-10-30/flutter-cold-start.svg b/docs/website/static/blog/release-2026-10-30/flutter-cold-start.svg new file mode 100644 index 00000000000..785e8cb2724 --- /dev/null +++ b/docs/website/static/blog/release-2026-10-30/flutter-cold-start.svg @@ -0,0 +1,31 @@ + + +Cold start to first frame +The Flutter Gallery, same Dart source, both release builds. Lower is better. +Codename One +Flutter +Android + +395 ms + +1247 ms +3.2x +macOS + +609 ms + +722 ms +1.2x +Windows + +126 ms + +205 ms +1.6x +Linux + +123 ms + +198 ms +1.6x + \ No newline at end of file diff --git a/docs/website/static/blog/release-2026-10-30/flutter-compute.svg b/docs/website/static/blog/release-2026-10-30/flutter-compute.svg new file mode 100644 index 00000000000..dec8f46cc86 --- /dev/null +++ b/docs/website/static/blog/release-2026-10-30/flutter-compute.svg @@ -0,0 +1,118 @@ + + +Compute: how many times faster Codename One is +Same workload in Java and Dart, both release builds. Blue: Codename One wins. Orange: Flutter wins. +Android +macOS +Windows +Linux +Hash map churn + +2.0x + +4.2x + +7.4x + +7.6x +Recursion + +4.4x + +1.7x + +1.6x + +3.6x +String building + +1.3x + +2.7x + +3.3x + +3.3x +Sequential arrays + +1.2x + +3.8x + +3.8x + +2.3x +Int arithmetic + +2.1x + +1.4x + +2.3x + +2.5x +Math functions + +1.2x + +1.01x + +1.2x + +1.10x +Random array access + +1.10x + +1.1x + +1.05x + +1.10x +Quicksort + +1.1x + +Flutter 1.02x + +1.05x + +1.07x +Long arithmetic + +1.00x + +1.1x + +Flutter 1.06x + +1.02x +Value escape + +Flutter 1.2x + +2.0x + +n/a + +1.5x +Object allocation + +Flutter 2.3x + +Flutter 3.1x + +1.6x + +1.4x +Geometric mean + +1.3x + +1.5x + +1.9x + +1.9x +n/a: the Windows value escape run did not produce a comparable result. Web is left out: see the text. + \ No newline at end of file diff --git a/docs/website/static/blog/release-2026-10-30/flutter-download-size.svg b/docs/website/static/blog/release-2026-10-30/flutter-download-size.svg new file mode 100644 index 00000000000..4bb40f2d7f7 --- /dev/null +++ b/docs/website/static/blog/release-2026-10-30/flutter-download-size.svg @@ -0,0 +1,43 @@ + + +Download size (zipped) +The Flutter Gallery, same Dart source, both release builds. Lower is better. +Codename One +Flutter +Android + +78.2 MB + +95.8 MB +1.2x +iOS + +80.0 MB + +81.9 MB +1.0x +macOS + +85.6 MB + +93.3 MB +1.1x +Windows + +77.9 MB + +86.4 MB +1.1x +Linux + +80.9 MB + +92.9 MB +1.1x +Web + +76.3 MB + +84.1 MB +1.1x + \ No newline at end of file diff --git a/docs/website/static/blog/release-2026-10-30/flutter-memory.svg b/docs/website/static/blog/release-2026-10-30/flutter-memory.svg new file mode 100644 index 00000000000..c60acb8b623 --- /dev/null +++ b/docs/website/static/blog/release-2026-10-30/flutter-memory.svg @@ -0,0 +1,31 @@ + + +Memory at rest +The Flutter Gallery, same Dart source, both release builds. Lower is better. +Codename One +Flutter +Android + +46 MB + +79 MB +1.7x +macOS + +37 MB + +56 MB +1.5x +Windows + +58 MB + +92 MB +1.6x +Linux + +82 MB + +224 MB +2.7x + \ No newline at end of file diff --git a/docs/website/static/blog/release-2026-10-30/flutter-vs-codenameone-poster.jpg b/docs/website/static/blog/release-2026-10-30/flutter-vs-codenameone-poster.jpg new file mode 100644 index 00000000000..96b9f34ed75 Binary files /dev/null and b/docs/website/static/blog/release-2026-10-30/flutter-vs-codenameone-poster.jpg differ diff --git a/docs/website/static/blog/release-2026-10-30/flutter-vs-codenameone.mp4 b/docs/website/static/blog/release-2026-10-30/flutter-vs-codenameone.mp4 new file mode 100644 index 00000000000..ee8718f176b Binary files /dev/null and b/docs/website/static/blog/release-2026-10-30/flutter-vs-codenameone.mp4 differ diff --git a/docs/website/static/blog/release-2026-10-30/kmp-keynote.svg b/docs/website/static/blog/release-2026-10-30/kmp-keynote.svg new file mode 100644 index 00000000000..a6addde1fe0 --- /dev/null +++ b/docs/website/static/blog/release-2026-10-30/kmp-keynote.svg @@ -0,0 +1,44 @@ + + +Keynote on an iPhone 17 Pro simulator, lower is better +Recompose on a day change (ms) +Compose / CN1 + +4.58 ms +Java / CN1 + +1.49 ms +Compose Multiplatform + +19.24 ms +Scroll work per frame (ms) +Compose / CN1 + +4.04 ms +Java / CN1 + +1.76 ms +Compose Multiplatform + +17.03 ms +Executable, stripped (MB) +Compose / CN1 + +16.8 MB +Java / CN1 + +8.9 MB +Compose Multiplatform + +22.9 MB +Cold start (ms) +Compose / CN1 + +961 ms +Java / CN1 + +805 ms +Compose Multiplatform + +1008 ms + \ No newline at end of file diff --git a/docs/website/static/blog/release-2026-10-30/simulator-virtual-keyboard-poster.jpg b/docs/website/static/blog/release-2026-10-30/simulator-virtual-keyboard-poster.jpg new file mode 100644 index 00000000000..c6bf4f30084 Binary files /dev/null and b/docs/website/static/blog/release-2026-10-30/simulator-virtual-keyboard-poster.jpg differ diff --git a/docs/website/static/blog/release-2026-10-30/simulator-virtual-keyboard.mp4 b/docs/website/static/blog/release-2026-10-30/simulator-virtual-keyboard.mp4 new file mode 100644 index 00000000000..ca82b351048 Binary files /dev/null and b/docs/website/static/blog/release-2026-10-30/simulator-virtual-keyboard.mp4 differ diff --git a/docs/website/static/blog/swing-and-javafx-on-phones.jpg b/docs/website/static/blog/swing-and-javafx-on-phones.jpg new file mode 100644 index 00000000000..84e002637ae Binary files /dev/null and b/docs/website/static/blog/swing-and-javafx-on-phones.jpg differ diff --git a/scripts/developer-guide/find_unused_images.py b/scripts/developer-guide/find_unused_images.py index 19c304fa046..c597b98705d 100755 --- a/scripts/developer-guide/find_unused_images.py +++ b/scripts/developer-guide/find_unused_images.py @@ -30,7 +30,8 @@ def iter_text_files(root: Path) -> Iterable[Path]: yield path -IMAGE_MACRO_PATTERN = re.compile(r"(?:^|[\s(])image::?([^\[\s]+)") +# A table cell spells an image as `a|image::...`, so `|` may precede the macro too. +IMAGE_MACRO_PATTERN = re.compile(r"(?:^|[\s(|])image::?([^\[\s]+)") def image_references(doc_root: Path, image_dir: Path) -> Set[str]: