Skip to content

Complications on the watch, and a Wear artifact beside the phone APK - #5583

Open
shai-almog wants to merge 35 commits into
masterfrom
feat-watch-complications
Open

Complications on the watch, and a Wear artifact beside the phone APK#5583
shai-almog wants to merge 35 commits into
masterfrom
feat-watch-complications

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

Closes the gap the wearables chapter has been documenting since watch support shipped: a developer could declare WidgetSize.WATCH_CIRCULAR, the value serialized onto the wire, and nothing on any device ever showed it.

Five places in the code said so out loud. All five are now deleted rather than reworded.

Apple Watch

A CN1WatchWidgets WidgetKit extension, embedded in the watch app rather than the phone app. That single choice is what makes companion and standalone need no separate handling at all: the companion case already copies the finished watch app into the phone app with the .appex inside it, and the platform filter that keeps the watch tree out of the Catalyst slice covers the extension for free.

Three premises turned out to be wrong, and each changed the design:

  • CN1DescriptorWidget.swift did not compile for watchOS. systemSmall/Medium/Large/ExtraLarge are @available(watchOS, unavailable) — unnameable, not merely absent — and the file named all four unconditionally. UIColor.systemBackground, UIColor(dynamicProvider:) and UIGraphicsImageRenderer are all API_UNAVAILABLE(watchos) too.
  • CN1_USE_WIDGETS was explicitly switched off on the watch slice, so Surfaces.publish() from a watch app was a hard no-op that reported success.
  • The deployment floor is watchOS 10, not WidgetKit's 9. containerBackground(for:) is watchOS 10 and every generated widget applies it, so 9.0 fails the build rather than losing the background.

The substitutes are the right answers rather than degradations: a watch face composites over black and has no light appearance, so the background role is black and a light/dark pair resolves to its dark half.

Also here: complication taps reach the action handler through the SwiftUI scene (there is no UIApplicationDelegate on watchOS), and the phone→watch mirror.

Wear OS

Complications and Tiles, generated per watch-bearing kind, plus the companion Wear artifact that has never been produced.

A complication is not a small widget, and the code says so. A watch face asks for one typed value and composes it into its own design, so the node tree is flattened and mined for content rather than rendered — at most two text nodes and one image. Everything dropped is logged once per render, so a developer whose careful layout arrives as one number learns that is by design.

A Tile does render the tree, and two things come out better than on a phone widget: circular progress renders natively where RemoteViews degrades to a linear bar, and per-node taps work where a small iOS widget honors only the root. The honest limitation is time — a countdown freezes and refreshes from the timeline, because ProtoLayout's dynamic expressions are version-sensitive and a frozen value that's always right beats a ticking one that works on some watches.

The companion module shares the app module's source tree rather than copying it. Both modules declare the same namespace — required, not merely convenient, because the shared sources refer to R unqualified from the app's package. That AGP permits this was verified with a throwaway two-module Gradle project before the phase was written.

The mirror

A watch app has its own storage; nothing the phone writes is visible there. On Apple the App Group identifier is the same string but resolves to a watch-local container, and on Wear OS the two apps are separate installs. So a phone-side publish reaches a complication only because the framework carries it.

It lives in the port on both platforms, for opposite reasons that point the same way. On Apple a core reference to com.codename1.wearable would flip usesWearable for every surfaces app and link WatchConnectivity into apps that never asked. On Android Executor.scanClassesForPermissions reads the app's own classes and not the core, so the same reference would fail to flip it and the mirror would silently do nothing.

Apple uses the one WatchConnectivity API that wakes the watch app in the background, which is budgeted at about fifty transfers a day; spending one when no complication is placed wastes what the app will want later, so that and an exhausted budget fall back to a queued transfer. Over the size cap the imagery is shed first — a complication rendering its numbers with a missing glyph beats one that never updates.

Applied on the watch headlessly: a file write and a re-render request, touching no framework state. The wake exists to refresh a complication, not to bring a UI forward nobody asked for.

Two fixes that stand alone

  • CN1BuildMojo collapsed same-extension artifacts onto one path. Two .apk entries in result.zip overwrote each other, corrupting the primary artifact, silently. Role suffixes now survive into the copied name.
  • publishRemote discarded its image side-map unconditionally, so a server-pushed descriptor referencing art has never rendered it.

Verification

  • Generated watch extension typechecks against the real watchOS 26.2 SDK; the iOS one still typechecks against the iOS SDK from the same manifest.
  • Surfaces natives compile for both platforms; check-native-signatures reports 0 fatal (down from 4 — the earlier "fatals" were stale-build artifacts).
  • WearGlueCompilesTest compiles the injected Wear services against the real CN1WatchSurface plus a 37-file stub tree, so a service that drifts from the reader's contract fails here rather than in a customer's Gradle build. Confirmed to fail when drift is injected.
  • SurfacesSwiftWatchPortabilityTest is the half that runs on a CI leg with no Xcode; confirmed to fail when a guard is removed.
  • 917 plugin + 5197 core tests green. SpotBugs zero findings on both modules. Copyright, ASCII, markdown-javadoc and cast-semantics gates all pass.
  • Docs: Vale 0, LanguageTool 0, paragraph-capitalization 0, asciidoctor clean.
  • The CI sample now declares watch families, so build-ios-watch compiles the extension on every PR and asserts the .appex is embedded at the right nesting.

What is not verified

  • The cloud signer embedding a watchOS profile into a doubly-nested .appex. Nothing in this repo does that work. Local ios-source with automatic signing is unaffected.
  • A real Wear OS emulator. The generated-project structure is asserted, and the injected services compile, but nobody has placed a complication on a face yet.
  • The build server honouring cn1-artifacts.properties. That contract is written but its other half lives out of repo — until it ships, the companion Wear artifact is verifiable through android-source plus a local Gradle run.
  • Play's Wear multi-APK version-code direction is argued from how feature filtering works, not confirmed against Play Console.

Twin PR in BuildDaemon mirrors the builder half.

🤖 Generated with Claude Code

shai-almog and others added 11 commits August 21, 2026 16:38
Three preliminaries for generating watch complications, each of which stands on
its own.

A result entry's role suffix now survives into the copied artifact name. Every
entry used to land on target/<finalName><extension>, keyed on the extension
alone, so a build returning two artifacts of the same kind -- a phone APK and a
companion Wear APK beside it -- collapsed both onto one path and the last one
written won. That corrupts the primary artifact, not merely the secondary one,
and it does it silently.

The family classification moves into SurfaceKindFamilies, which also reads the
portable "families" key with "iosFamilies" as its legacy spelling. The Android
builder has to tell a home-screen kind from a complication kind and is not going
to parse a key with "ios" in its name. Delegation rather than a second copy,
because the rule is subtle enough that three call sites once implemented it as
startsWith("watch") and all three got accessoryCircular wrong.

The shared surfaces Swift now compiles for watchOS. Four WidgetKit system
families are @available(watchOS, unavailable) -- unnameable, not merely absent --
and UIColor.systemBackground, UIColor(dynamicProvider:) and
UIGraphicsImageRenderer are all API_UNAVAILABLE(watchos); the file named all of
them unconditionally. The substitutes are the right answers rather than
degradations: a watch face composites over black and has no light appearance, so
the background role is black and a light/dark pair resolves to its dark half.
Images downsample through ImageIO, which decodes at the target size so the
full-size bitmap is never resident, at a quarter of the phone's ceiling.

Verified by typechecking the sources against both the watchOS and iOS SDKs.
SurfacesSwiftWatchPortabilityTest is the half that also runs on a CI leg with no
Xcode; it was confirmed to fail when the systemBackground guard is removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
setWatchTarget(true) builds a second WidgetKit extension for the watch app,
where the first one is built for the phone. They share every Swift source that
can be shared and differ in which families they may name.

The dead watchTarget parameter that has been threaded through familiesSwift,
watchOnlyFamiliesSwift and mapFamily since the families were introduced finally
has a caller -- but it was not correct as written. With watchTarget true it
still mapped small/medium/large onto .systemSmall and friends, which are
@available(watchOS, unavailable): unnameable there, not merely absent, so the
watch bundle would have failed to compile rather than showing a widget nobody
wanted. Those four and lockscreen now resolve to no family in a watch target,
and the home-screen fallback for a kind with no usable family is suppressed
there too. In the other direction accessoryCorner needs no os(watchOS) guard
inside a target whose SUPPORTED_PLATFORMS is watchOS alone.

The rest follows the same split: the two ActivityKit sources are never shipped
to the watch and the live activity never joins its bundle, the widget-count
limit counts the kinds this flavour actually hosts, and the build settings
describe a watch target -- WATCHOS_DEPLOYMENT_TARGET, SDKROOT, device family 4,
arm64_32 -- with ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES off, because the watch
app already embeds the runtime for everything nested inside it.

The floor is watchOS 10.0, not WidgetKit's own 9.0: every generated widget
applies containerBackground(for:), which is watchOS 10, so a lower target does
not lose the background -- it fails the build. A lower one is refused with that
reason.

Verified by generating a watch extension from a mixed manifest and typechecking
the whole thing against the watchOS 26.2 SDK, and by generating the iOS one from
the same manifest and typechecking it against the iOS SDK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CN1_USE_WIDGETS was undone for watchOS alongside tvOS, so every surfaces native
compiled to its unsupported stub and Surfaces.publish() from a watch app was a
hard no-op that reported success. tvOS keeps the undef -- it has no WidgetKit at
all -- but the watch does not: a complication is a WidgetKit widget in an
accessory family, hosted by the watch app's own extension and fed from the
watch's own App Group container.

That container is the counter-intuitive part and is now written down where the
guard used to be. The identifier is the same string as the phone's; the
container behind it is a separate directory on the watch. So the watch has to
publish for itself rather than reading what the phone wrote, which is why
restoring these natives is what makes a complication possible at all.

cn1SurfacesMinOSSupported compares the plist floor against the OS actually
running, so its fallback has to be per-platform too. The iOS default of 16.1
compared against a watchOS version is never met, and every watch would have
reported no widget support whatever the plist said.

The four ActivityKit natives now answer for the watch explicitly instead of
relying on the Swift bridge having compiled its bodies out. They keep their
symbols -- the Java methods are reachable from shared code, so removing them
would fail the watch link rather than tree-shake -- and the guard uses #else
rather than an early return so the watch slice compiles no unreachable statement.

Verified by compiling the surfaces native block against both the watchOS and iOS
SDKs, and by typechecking the app-target Swift glue against the watchOS SDK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The watch app now gets a CN1WatchWidgets target embedded in its own PlugIns
folder, and that single choice is what makes both distributions need no separate
handling: the companion case already copies the finished watch app into the
phone app with the .appex inside it, and the platform filter keeping the watch
tree out of the Mac Catalyst slice covers the extension for free; the standalone
case ships the watch app as the product. There is no branch for either.

The target type is :app_extension. :watch2_extension is the legacy paired
WatchKit app extension -- the same trap as :application versus :watch2_app for
the app target -- while a WidgetKit extension is a plain app extension wherever
Apple ships it.

Generating it belongs here rather than beside the iOS extension because the
watch app target does not exist yet when the schemes ruby runs. So it is written
immediately before the watch builder's own xcodeproj script and wired by that.

Two things the watch target could not previously reach. Its own translation
carries no CN1SurfaceBridge -- only the phone's -src does -- so the natives
found no bridge through NSClassFromString and answered unsupported; the bridge
and its config constant are now added to the watch target by name, de-duped so
the shared-translation case is unaffected. And the entitlements file was gated
on HealthKit alone, which was the only capability the watch did not inherit from
the phone until now; publishing complications adds an App Group. Both are opt-in
and neither implies the other, because granting one that is unused is refused by
entitlement validation rather than ignored.

parseSurfacesManifest no longer returns early when nothing reaches iOS. A
manifest whose every kind is a complication produces no iOS extension and no
phone app-group entitlement, and must still produce a watch one -- that is the
case the watch families exist for. The build now says which of the two happened
instead of reporting that watch kinds appear nowhere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The cn1ss sample already declares codename1.watchMain and a surfaces kind, so
adding two watch families to that kind is enough to make the build-ios-watch job
generate and compile CN1WatchWidgets for watchOS. That is the only automated
check that the shared surfaces Swift stays portable to a platform with no
UIGraphicsImageRenderer, no UIColor dynamic provider and no system widget
families -- every one of which was a real break.

Keeping small and medium on the same kind preserves the existing iOS coverage,
and the manifest switches to the portable "families" spelling so that path is
exercised too.

The script then asserts the .appex is actually in the watch app's PlugIns
folder, declares the WidgetKit extension point and carries an app group. The
screenshot comparison cannot see any of that, and simctl cannot exercise a
complication at all -- there is no API to place one on a watch face -- so the
wiring needs checking directly or it is not checked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tapping a complication launches the watch app with the widgetURL. There is no
UIApplicationDelegate on watchOS, so the SwiftUI scene's onOpenURL is the only
place that URL can be caught -- and nothing was catching it, so the tap opened
the app and the action went nowhere.

The cn1surface:// decode moves out of CodenameOne_GLAppDelegate.m, which is
entirely #if !TARGET_OS_WATCH, into IOSNative.m, which compiles on both. The
delegate now calls it rather than carrying its own copy, so the two platforms
cannot drift on what a surface action means.

Surfaces.dispatchAction already queues until the app registers its handler,
which is what makes this work at all: a complication tap is almost always a
cold start.

The C entry point is declared in the generated watch bridging header, because a
plain C function is invisible to Swift otherwise, and only when the app actually
publishes complications -- an app without them keeps the scene and the header it
had.

Verified by compiling the surfaces native block for watchOS and iOS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A developer designing a complication previously had nothing to look at. simctl
cannot place one on a watch face, so short of building to a device and adding it
by hand there was no way to see the layout at all -- while every phone family
had a preview from the start.

The Widgets window now lists the four watch families at the accessory families'
own point sizes, and clips the round ones the way a face does. That clip is the
point rather than decoration: a watch face shows nothing a circular complication
draws into its corners, so previewing it square would make a design look fine
that loses content on the device.

layoutForSize gains the two substitutions the platform renderers already make,
so the preview and the device agree on what gets shown. watchCorner borrows the
circular layout -- a corner complication is round, and Wear OS has no corner slot
at all -- and watchRectangular borrows lockscreen, which is the same WidgetKit
family on Apple. Both are closer to what the developer designed than "default",
which may well be a rectangular phone widget.

What this previews is the node tree at the right size and shape, not the
per-platform lowering: Wear OS reduces a complication to typed ComplicationData,
so a layout that looks right here can still lose detail on a face.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An App Group container is device-local: the watch resolves the same identifier
to a directory of its own, which nothing on the phone can write. So a phone-side
Surfaces.publish() was invisible to a complication however well everything else
was wired, and the developer's only recourse was to hand-roll the transfer.

IOSSurfaceBridge now forwards the descriptor after the local write has already
succeeded, so nothing here can leave the phone's own widget wrong. Which kinds
are worth sending is decided at build time and written into the plist as
CN1SurfacesWatchKinds, so publishing a phone-only kind costs one dictionary
lookup.

The delivery ladder is the interesting part. transferCurrentComplicationUserInfo
is the only WCSession API that wakes the watch app in the background to refresh
a complication, and it is budgeted at roughly fifty a day. Spending one when the
user has placed no complication wastes what the app will want later, so both
that case and an exhausted budget fall back to transferUserInfo -- queued,
unbudgeted, and applied whenever the watch app next runs. That is materially
weaker, which is why it is the fallback rather than the default. Over the 48KB
property-list cap the imagery is shed first, on the grounds that a complication
rendering its numbers with a missing glyph beats one that never updates; over
the cap even then, it gives up and says so.

Imagery travels in the same dictionary rather than through transferFile, which
is a separate unordered queue with no atomicity against the descriptor -- a
complication could render against art that had not landed, which is worse than
a gap.

Applying it on the watch is deliberately headless: a file write and a WidgetKit
poke, touching no Java. The background wake exists to refresh a complication,
and starting the whole application to do a file write would bring a UI forward
nobody asked for. Reserved keys are routed before anything app-visible, the same
way acknowledgement traffic already is, so the app never sees a message it did
not send.

publishRemote grows an images overload, which also fixes a latent gap: it
discarded the side-map unconditionally, so a server-pushed descriptor
referencing art has never rendered it.

Verified by compiling the surfaces natives for watchOS and iOS, and by
check-native-signatures against a rebuilt port -- which reports 0 fatal, the new
byte[][] mangling included.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Android surfaces codegen never looked at families, so a kind declaring only
watch complications quietly became a home-screen widget -- a surface the
manifest never asked for. It now splits: a kind with a phone family still gets
its AppWidgetProvider, and a watch-bearing kind is collected for the Wear
services instead. iOS has always refused the same thing, so this is the two
platforms agreeing rather than a new rule.

That silence was the real problem, and the "companion Wear APK is not produced
yet" log is replaced by diagnostics that name what actually happens: which kinds
become complications, that watchCorner renders as circular because Wear OS has
no corner slot, that watchRectangular earns a Tile as well, and -- when the
build produces no Wear product at all -- that the declaration reaches no device
and what to set to change that.

watchModuleName answers "which module is the watch product" once, because
everything downstream is the same code and differs only in the destination:
"app" for a standalone build where the single APK is the watch app, "wear" for a
companion build, null for a project that never asked for a watch.

complicationTypes is the mapping WidgetSize already documents, made executable.
It decides whether a complication can be placed in a given slot at all -- a
watch face asks for one specific type and gets nothing if the source does not
offer it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Complications and Tiles now exist on Android, generated per watch-bearing kind,
along with the companion Wear artifact that has never been produced.

The split is deliberate: everything that does not touch androidx.wear lives in
the port and is compiled by this repository, and only the two thin
androidx-facing services ship as build-time resources. An app publishing no
complication must not carry those libraries, but keeping the reader in the port
is what lets CI catch a break in it -- and WearGlueCompilesTest compiles the
injected services against the REAL CN1WatchSurface plus a stub tree, so a
service that drifts from the reader's contract fails here rather than in a
customer's Gradle build naming a file they never wrote.

A complication is not a small widget, and the code says so. A watch face asks
for one typed value and composes it into its own design, so the node tree is
flattened and mined for content rather than rendered; padding, alignment and
colour are the face's business. What is dropped is logged once per render, so a
developer whose careful layout arrives as one number learns that is by design.

A Tile really does render the tree, and two things come out better there than on
a phone widget: circular progress renders natively where RemoteViews degrades to
a linear bar, and per-node taps work where a small iOS widget honours only the
root. The honest limitation is time -- a countdown is frozen and refreshed from
the timeline, because ProtoLayout's dynamic expressions are version-sensitive and
a frozen value that is always correct beats a ticking one that works on some
watches.

The companion module shares the app module's source, resource and asset dirs
rather than copying them, which would roughly double disk and dex time on a
cloud builder for a tree identical apart from one class. Both modules declare
the same namespace -- required, not merely convenient, because the shared
sources refer to R unqualified from the app's package. That AGP permits it was
verified with a throwaway two-module project before this was written.

The mirror lives in the port for a reason that is the opposite of the iOS one
and points the same way: Executor.scanClassesForPermissions reads the app's own
classes and not the core, so a core-level reference to com.codename1.wearable
would fail to turn the Data Layer glue on and the mirror would silently do
nothing. Reserved paths are routed before anything app-visible and without
waking the app, matching how acknowledgement traffic is already handled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five statements in the wearables chapter said the opposite of the truth -- that
Wear OS has no companion form, that no complication target is generated on
either platform, that a companion Android build hands back one artifact. They
are deleted rather than reworded, and the summary table with them.

What replaces them is mostly a warning, because the surprising part of this
feature is not that it works but how much a watch face discards. A complication
is not a small widget: the face asks for one typed value and composes it into
its own design, so the node tree is mined for content rather than rendered, and
on Wear OS a kind supplies at most two text nodes and one image. That has a
section of its own, with the per-node mapping in the surfaces chapter, because
someone reading only the "declare a family" paragraph would design something the
face will not show.

Two places the Tile beats the phone widget are written down too -- native
circular progress and per-node taps -- along with the one place it loses, a
frozen countdown, and why a frozen value that is always right beats a ticking one
that works on some watches.

The mirror gets its own section, leading with the fact that makes it necessary:
a watch app has its own storage, so a phone-side publish reaches a complication
only because the framework carries it. Its budgets, caps and degradation are
stated rather than left to be discovered, and so is the cost -- declaring a watch
family on Android puts play-services-wearable in the phone APK.

Both blog posts still documented the retired android.wear hint; they now say what
drives the build, with a note that the old hint keeps working.

Vale, LanguageTool, the paragraph capitalization check and asciidoctor all report
zero across the whole guide.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 822e641e8c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Ports/Android/src/com/codename1/impl/android/surfaces/CN1WatchSurface.java Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Blog prose gate

✅ No net-new prose findings introduced by this PR.

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog

shai-almog commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 149 screenshots: 149 matched.
Native Windows port, REAL shipping pipeline: the hellocodenameone screenshot suite rendered by a binary CROSS-COMPILED on Linux (clang-cl + xwin, WebView2 linked) and RUN on a Windows x64 runner. Compared against the in-repo baseline in scripts/windows/screenshots.

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 92ms / native 4ms = 23.0x speedup
SIMD float-mul (64K x300) java 70ms / native 4ms = 17.5x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 native bridge unavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 198.000 ms
Base64 CN1 decode 145.000 ms
Base64 SIMD encode 100.000 ms
Base64 encode ratio (SIMD/CN1) 0.505x (49.5% faster)
Base64 SIMD decode 99.000 ms
Base64 decode ratio (SIMD/CN1) 0.683x (31.7% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 36.000 ms
Image createMask (SIMD on) 35.000 ms
Image createMask ratio (SIMD on/off) 0.972x (2.8% faster)
Image applyMask (SIMD off) 81.000 ms
Image applyMask (SIMD on) 200.000 ms
Image applyMask ratio (SIMD on/off) 2.469x (146.9% slower)
Image modifyAlpha (SIMD off) 73.000 ms
Image modifyAlpha (SIMD on) 68.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.932x (6.8% faster)
Image modifyAlpha removeColor (SIMD off) 86.000 ms
Image modifyAlpha removeColor (SIMD on) 76.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.884x (11.6% faster)

@shai-almog

shai-almog commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 149 screenshots: 149 matched.
Native Windows port (x64 / Intel-AMD): full hellocodenameone screenshot suite rendered offscreen with Direct2D/DirectWrite, plus the real benchmarks (base64 native/CN1/SIMD, image createMask/applyMask/modifyAlpha/PNG/JPEG, SSE2 SIMD kernels). Compared against the in-repo baseline in scripts/windows/screenshots.

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 63ms / native 5ms = 12.6x speedup
SIMD float-mul (64K x300) java 67ms / native 5ms = 13.4x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 native bridge unavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 193.000 ms
Base64 CN1 decode 132.000 ms
Base64 SIMD encode 110.000 ms
Base64 encode ratio (SIMD/CN1) 0.570x (43.0% faster)
Base64 SIMD decode 98.000 ms
Base64 decode ratio (SIMD/CN1) 0.742x (25.8% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 26.000 ms
Image createMask (SIMD on) 23.000 ms
Image createMask ratio (SIMD on/off) 0.885x (11.5% faster)
Image applyMask (SIMD off) 59.000 ms
Image applyMask (SIMD on) 53.000 ms
Image applyMask ratio (SIMD on/off) 0.898x (10.2% faster)
Image modifyAlpha (SIMD off) 60.000 ms
Image modifyAlpha (SIMD on) 47.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.783x (21.7% faster)
Image modifyAlpha removeColor (SIMD off) 39.000 ms
Image modifyAlpha removeColor (SIMD on) 175.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 4.487x (348.7% slower)

@shai-almog

shai-almog commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 149 screenshots: 149 matched.
Native Linux port (x64), GTK3/Cairo/Pango, ParparVM bytecode-to-C (no JVM): the hellocodenameone screenshot suite rendered by a native ELF built + run on the GitHub x64 runner. Baseline: scripts/linux/screenshots.

@shai-almog

shai-almog commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 149 screenshots: 149 matched.
Native Linux port (arm64), GTK3/Cairo/Pango, ParparVM bytecode-to-C (no JVM): the hellocodenameone screenshot suite rendered by a native ELF built + run on the GitHub arm64 runner. Baseline: scripts/linux/screenshots-arm.

@shai-almog

shai-almog commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 149 screenshots: 149 matched.
Native Windows port (arm64 / Apple Silicon - Arm): full hellocodenameone screenshot suite rendered offscreen with Direct2D/DirectWrite, plus the real benchmarks (base64 native/CN1/SIMD, image createMask/applyMask/modifyAlpha/PNG/JPEG, NEON SIMD kernels). Compared against the in-repo baseline in scripts/windows/screenshots.

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 53ms / native 3ms = 17.6x speedup
SIMD float-mul (64K x300) java 57ms / native 3ms = 19.0x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 native bridge unavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 246.000 ms
Base64 CN1 decode 129.000 ms
Base64 SIMD encode 65.000 ms
Base64 encode ratio (SIMD/CN1) 0.264x (73.6% faster)
Base64 SIMD decode 64.000 ms
Base64 decode ratio (SIMD/CN1) 0.496x (50.4% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 14.000 ms
Image createMask (SIMD on) 9.000 ms
Image createMask ratio (SIMD on/off) 0.643x (35.7% faster)
Image applyMask (SIMD off) 25.000 ms
Image applyMask (SIMD on) 19.000 ms
Image applyMask ratio (SIMD on/off) 0.760x (24.0% faster)
Image modifyAlpha (SIMD off) 17.000 ms
Image modifyAlpha (SIMD on) 12.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.706x (29.4% faster)
Image modifyAlpha removeColor (SIMD off) 20.000 ms
Image modifyAlpha removeColor (SIMD on) 134.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 6.700x (570.0% slower)

Five review findings, four of which were the same shape: code that compiles,
never throws, and renders nothing.

**Containers were read under the wrong key.** `SurfaceContainer` serializes its
children as `ch`; both the complication reader and the Tile renderer looked for
`c`. Every row, column and box therefore looked empty, so a complication mined a
layout with no text, no progress and no imagery in it and a Tile rendered
nothing. That is indistinguishable from an app that published nothing, which is
why it survived a green build.

**Dynamic nodes carry no text to interpolate.** A `dyn` node serializes a style
plus a date or a dateKey, so asking it for `text` resolved to an empty string and
every countdown, clock and relative date vanished. They now go through the core's
own formatter -- made public rather than copied -- so a countdown reads the same
on a watch face as in the simulator preview and on a home screen.

Both are now pinned by SurfaceWatchWireFormatTest, which asserts the field names
against the serializer itself. The readers live in the Android port and cannot be
unit tested from there, but the wire format can be, and that is what makes this
kind of drift visible instead of silent.

**A Tile tap dropped its action.** A Clickable's id is ProtoLayout interaction
metadata and never reaches the started activity, so the trampoline -- which
dispatches only when EXTRA_ACTION_ID is present -- opened the app and discarded
the action id, source and parameters. The extras are attached explicitly now, the
same three a widget tap sends.

**A companion build raised the phone's minSdk.** Declaring a watch family pushed
the shared floor to 26 before the phone module's Gradle file was written, so a
phone APK that had supported API 21-25 became uninstallable on the devices it
already served. The floor now rises only for a standalone build, where the app
module IS the watch product; the wear module sets its own.

**Mirrored artwork never triggered a redraw.** A file transfer is asynchronous
and unordered against the descriptor, so art routinely lands after the timeline
that references it -- and only the descriptor asked for a refresh. The first
render showed a gap and nothing asked again until the next publish.

Also fixes the two SpotBugs findings that failed CI: mkdirs() return values were
ignored. The naive check is wrong here, since mkdirs() answers false both when
the directory could not be created and when it already exists -- which is the
common case -- so existence afterwards is what the callers test.

SpotBugs is now zero across android, ios, codenameone-maven-plugin and
core-unittests; 917 plugin and 5202 core tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ad8aed6721

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@shai-almog

shai-almog commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 12 screenshots: 12 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

Four more findings from the second review round, three of which meant a
generated artifact could not work at all.

**The watch slice lost the surfaces define.** CN1_USE_WIDGETS was still flipped
only for surfacesExtensionEnabled, so a manifest declaring nothing but
complications compiled the watch slice without it -- and since the
WatchConnectivity delegate calls cn1_watch_apply_mirrored_surface, which that
define guards, the watch slice failed to LINK rather than merely doing nothing.

That edit was written once before and lost: a patch script asserted on a later
anchor and never wrote the file. The same failure ate the daemon's
appendWidgetExtension call site. Both are now verified present rather than
assumed.

**The Wear module declared no Data Layer listener.** Its manifest is selected
outright by the module's sourceSets rather than merged with the phone's, so
nothing the phone declares reaches it -- and the watch needs this one more than
the phone does, being the half that RECEIVES a mirrored complication. Play
services had nothing to bind in the watch APK, so every mirrored descriptor was
dropped and complications stayed at whatever the watch had published for itself.

**Tile padding was read as an object.** SurfaceNode serializes it as the array
[top, right, bottom, left], which is what the RemoteViews renderer reads, so
asking for an object returned null for every valid descriptor and all declared
padding was silently discarded.

**A Tile's vector resources were keyed by object identity.** The layout request
and the resources request are separate calls that each re-read and re-parse the
timeline, so the two ids never matched: the layout referenced a resource the
returned map did not contain and every vector rendered as a missing image. The
id now comes from the node's serialized content, which is equal across parses --
and two identical vectors sharing one resource is correct, since they draw the
same thing.

SurfaceWatchWireFormatTest grows the padding and vector cases, so the class of
bug that produced three of these four -- reading a field the serializer does not
write -- is pinned against the serializer rather than found by review.

SpotBugs zero across android, ios, codenameone-maven-plugin and core-unittests;
917 plugin and 5204 core tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: badd9cd1fa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Ports/iOSPort/src/com/codename1/impl/ios/IOSSurfaceBridge.java
The Android CI failure and four more review findings, all of them the same
mistake in different places: the phone and the watch are separate products in a
companion build, and things scoped to one kept reaching the other.

**The androidx.wear libraries reached the phone module.** They went into the
shared gradleDependencies hint, which in a companion build feeds both modules --
and they declare minSdk 26 while the phone keeps its own floor. A phone app on
API 24 therefore stopped building the moment a watch family was declared, failing
its manifest merge against libraries it never uses. They now go into the wear
module's own dependency block, and only a standalone build -- where that single
module IS the watch -- puts them in the shared one.

**The Data Layer glue was decided before the kinds were parsed**, so
watchSurfaceKinds was always empty at that point. An app that publishes
complications and never writes a line of com.codename1.wearable got no glue, no
dependency and an empty listener declaration -- the mirror had no transport at
either end. The block moves after the surfaces parse.

**A watch-only manifest could not mirror at all.** The phone was deliberately
left without the App Group entitlement, so its container did not resolve,
areWidgetsSupported() answered false, and Surfaces.publish() returned before the
bridge -- taking the mirror with it. The one manifest this feature exists for was
the one that could not update its own complications. The group is genuinely part
of the plumbing on both bundles and is now entitled on both.

**The Wear manifest declared no INTERNET permission.** It receives only the
scanned permissions, not the base ones, and is selected outright rather than
merged -- so a watchMain making an ordinary Codename One network request failed
while the same code worked on the phone.

**The watch bundle declared no cn1surface URL scheme.** A complication supplies a
cn1surface:// widgetURL and the generated scene waits for it in onOpenURL, but
the watch is a separate bundle inheriting none of the phone's URL types. watchOS
had nothing to route the tap to, so the whole tap-dispatch path was inert.

Two new tests pin the module boundary: that a companion phone module carries no
androidx.wear dependency, and that the watch bundle declares the scheme when it
hosts a complication and does not otherwise.

SpotBugs zero across android, ios and codenameone-maven-plugin; 920 plugin tests
pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e4952d5c96

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The CI failure and two more review findings, all from the same root: the Wear
module's build.gradle is derived from the phone's by textual substitution, and a
generated Gradle file only fails when Gradle evaluates it -- twenty minutes after
the mistake, in a job that names none of it.

**The androidx.wear dependency landed in the buildscript block.** The anchor was
"dependencies {", which matches the indented buildscript block FIRST -- and
String.replace hits every occurrence -- so an implementation() call went into
buildscript's dependency handler, where the method does not exist. The whole
:wear project failed to evaluate. Anchored on "\ndependencies {" now, which only
the project block matches.

**The Wear module looked for a keystore beside itself.** Gradle resolves
file("keyStore") relative to the project it appears in, and the certificate is
written only to the app module -- so a companion release build failed to
CONFIGURE, taking the phone artifact with it. Not the watch half degrading: the
whole build not starting.

**The generated services were written into the phone's source root.** The wear
module shares that directory, so the phone compiled androidx.wear imports it has
no libraries for -- the exact mirror of the dependency-scoping fix that preceded
it. They now go to the wear module's own root. The kind-list resource stays on
the phone deliberately, because the mirror reads it THERE to decide what to send.

**The Tile service was copied whether or not a Tile was declared**, while its
dependencies were added only for a rectangular family. Gradle compiles every
source in the tree, so a complication-only build failed on unresolved imports.

The derivation is now a static function, and WearModuleGradleTest pins each
substitution against a build.gradle shaped like the real one -- the dependency
landing in the project block and not buildscript's, the keystore reachable, the
libraries shared, the phone's floor untouched. It calls the real builder rather
than reproducing it, and was confirmed to fail on the exact anchor bug that broke
CI. Two more tests cover where the services are generated and which are copied.

SpotBugs zero across android, ios and codenameone-maven-plugin; 928 plugin tests
pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d2c4b52438

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Progress from a date interval. SurfaceProgress.setDateInterval writes start/end
and no value, and the watch reader checked only value/valueKey -- so a ranged
complication reported no data and a Tile drew a zero-length arc. It now calls
the renderer's own resolveFraction rather than carrying a second copy of the
arithmetic; what stays here is the emptiness question, because a widget always
has a bar to draw and treats an unusable node as zero, while a gauge pinned at
the bottom is a claim about the value rather than an absence of one.

Mirrored images were never collected. Blob names are content hashes, so every
changed image left its predecessor in the watch app's storage for ever. The
mirror receiver now runs the same collection the publish path does, after the
replacement timeline is safely written.

reloadWidgets did nothing for a watch. It called broadcastUpdate, which reaches
home-screen providers only, so a reload of a watch-only kind was a no-op and a
mixed kind refreshed half of itself.

Tile spacers were all 4dp. SurfaceSpacer serializes "min", and only when
non-zero; reading w/h turned every declared spacer into the same stub and every
flexible one along with it. The parent axis is threaded down because the same
node is a width in a row and a height in a column, and a spacer with no minimum
is now expand() rather than four dips -- which is what it was for.

Complications never advanced. A timeline can hold entries that take over at
stated times, the service is asked once, and UPDATE_PERIOD_SECONDS is
deliberately 0 because polling a push-driven surface costs battery for nothing.
The data now carries a valid time range ending at the flip, so the system comes
back at that moment and nowhere in between.

Tiles cached stale artwork. The resources version hashed image ids, and a
vector's id covers its own definition but not the entry state its ops read -- so
a flip that only moved a hand advertised the same version and Wear kept the old
bitmap. The version now includes the state, and both sites that compute it share
one implementation instead of two that had already diverged.

The Tile trampoline is exported only where a Tile exists. ProtoLayout's
LaunchAction is started by the tile host from its own process, so a private
activity fails the permission check and the tap does nothing; a complication is
unaffected because its PendingIntent was created by the app. A project with
complications and no Tile keeps the activity private.

A Wear version code that does not outrank the phone is refused, naming the hint:
a watch would otherwise install the phone build, which is not an error anyone
would trace back to a setting.

The Wear module gets the mobile-services config. Its build.gradle is derived
from the phone's, so an FCM app carried the google-services plugin into a module
with no config file and failed the whole multi-module build -- taking the phone
artifact with it. Copied rather than stripped, because the derived dependency
block already carries Firebase.

The injected services and the port reader were typechecked against the real
androidx.wear jars, not only the stub tree; the stubs were corrected to match
where they had been looser than the API.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 25cebc85c1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

shai-almog and others added 3 commits August 22, 2026 11:16
The watch never activated its session. Every route into CN1WatchConnectivity is
a wearable native, so the WCSession comes up lazily the first time an app
touches com.codename1.wearable -- and an app that declares watch surfaces and
never touches that API takes no such route. The delegate was never installed and
didReceiveUserInfo: could not fire, in exactly the surfaces-only configuration
the phone-to-watch mirror exists to serve, with nothing to report it because the
phone half sends successfully into a session that has no listener. The watch's
initVM now brings the session up before the app starts, so a descriptor that
arrives during launch is not dropped either.

Mirrored artwork was never collected on the watch. Blob names are content
hashes, so every changed image left its predecessor in the App Group container
for ever, and a container that only grows is a watch app that eventually cannot
write. The native apply path now collects after the replacement document is in
place -- the same order IOSSurfaceBridge uses locally -- against the document's
own "images" list rather than the blobs that arrived in this message, because a
mirror only ships art the watch has not seen and collecting against the
transferred names would delete what it was keeping.

The extension declared version 1.0/1 whatever the app said. Apple validates an
embedded bundle's versions against its container, and this one is nested two
deep, so a project on any other version was rejected at submission -- after
every build had gone green. Both widget extensions now declare the app's own
resolved values, which are not simply the project version: ios.plistInject
replaces the default injection where it sets either key, and the two keys are
independent, so the build version must not be derived from an injected marketing
one. The Matter extension had already worked this out by hand and now shares the
same two methods.

The new Objective-C was syntax-checked against both the iPhoneOS and watchOS
SDKs; the native signature gate reports 0 fatal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The daemon has carried embeddedExtensionShortVersion and
embeddedExtensionBundleVersion since the Matter extension needed them, and the
companion copy of this file arrived at the same two methods under different
names. Same name, same file, so the twins stay diffable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Exporting CN1SurfaceActionActivity is what makes a Tile tap work at all -- the
tile host starts it from its own process -- and it also lets any app on the
watch start it with extras of its choosing, which the previous commit narrowed
to Tile builds but did not otherwise defend. It dispatched whatever action id it
was handed.

Every surface this app draws now carries a per-install secret with the tap, and
the trampoline dispatches only when the secret matches. The value is generated
on first use, kept in the app's own private preferences, and travels only inside
the layout handed to the tile host, which no other app can read. All three
producers attach it through one method rather than each remembering to.

The check applies exactly where it can matter: while the trampoline is private,
nothing outside the app can start it and the token would only break a
PendingIntent handed to the launcher before an update. Whether it is exported is
read from the merged manifest rather than assumed, so the check follows what was
actually declared.

Two more from the same round, both consequences of generating the Wear manifest
and gradle independently rather than merging them:

The API 26 floor now rises only when the surface libraries that need it are
present. A companion watch app that uses only the lifecycle or the Data Layer
has always run on the Wear OS 2 baseline, and raising it for every watchMain
build took API 23-25 watches away from projects that declared no surface.

android.xapplication_attr and android.xapplication are carried into the Wear
manifest. A project that names a custom Application -- the usual way to
initialise a native SDK -- got the stock one on the watch while the Wear module
compiled the very sources that expect it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 023172eff0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…urce

Codex reviews both repositories, and these came in against the daemon's copies.
They are the same code, so they are fixed here and mirrored.

A hint could override the two values the Wear artifact cannot get wrong.
android.xgradle_default_config lets a project add its own minSdkVersion and
versionCode inside defaultConfig, and those land AFTER the generated ones -- so
rewriting the generated declaration left the project's value effective: the
watch quietly kept the phone's version code, or a floor below the one the Wear
libraries need, while wearVersionCode validated a number nothing used. Both are
now declared in a trailing block, which is evaluated last whatever the file
above it says.

The Wear launcher had no theme. The shared stub extends AppCompatActivity when
android.extendAppCompatActivity is set, and AppCompat refuses to start under a
theme that is not one of its own, so the watch crashed on its first frame. Both
manifests now take the theme from one method -- which also corrects a typo it
inherited: the AppCompat value was written "@@style/...", and a leading @@ in a
resource attribute is the escape for a literal @, so it was a plain string
naming nothing and the theme was never applied. That branch only runs when a
project has asked for AppCompat, so the intent is not in doubt.

Preview data ignored the type it was asked about. Wear takes it as an answer
about that type, and a ShortText handed to a slot advertising LONG_TEXT or
RANGED_VALUE is rejected or drawn empty -- so a kind went missing from exactly
the pickers where its layout is roomiest. MONOCHROMATIC_IMAGE answers null
rather than inventing an icon, since that type is nothing but its icon.

A Tile showing a clock or a countdown froze for ever. Freshness came only from
the next timeline flip, and a timeline with no future entry asked for no refresh
at all, so "in 5 minutes" still said that the next day. Dynamic text now earns a
bounded refresh of its own -- minute-accurate, which is what a Tile's rate limit
allows anyway.

An unpublish left the complication showing withdrawn content. The Data Layer
announces it as a deletion, and a mirror never entered the replication cache
that ordinarily handles one, so the tombstone went down a path that knew nothing
about it and the descriptor stayed on disk. Mirror paths are now handled whether
the item arrived or left.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a1551b3bac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

shai-almog and others added 2 commits August 22, 2026 12:31
setValidTimeRange was the wrong mechanism and the review was right to say so: it
governs when a value may be DISPLAYED and schedules no further request, so on
its own it replaced a stale complication with an empty one rather than with the
next entry. androidx has the mechanism this feature was shaped for --
ComplicationDataTimeline, delivered through onComplicationDataTimeline -- and
the service now hands over every published entry at once. The system swaps them
at the stated moments without waking this process, which is the same bargain
WidgetKit makes on the other platform. CN1WatchSurface grew readTimeline for it,
because the reader had been collapsing the document to whichever entry was
current.

The Wear manifest declares xmlns:tools. Copying the application hints into it
was the previous commit's fix, and tools:replace and tools:node are how a
project resolves a merger conflict -- so a root declaring only xmlns:android
turned the copy into a document that would not parse.

READ_MEDIA_* declarations reach the Wear manifest. It is generated
independently, so a watchMain reading media on Wear OS 4 could never be granted
the runtime permission the phone half asks for.

A late image transfer no longer resurrects a collected blob. Transfers are
asynchronous and unordered, so art belonging to publication N can arrive after
the descriptor for N+1 has already collected it; the write is now conditional on
the descriptor currently on disk still naming it. A kind with no descriptor yet
still accepts artwork, because it legitimately arrives first in that case.

A mirrored kind is remembered. It was never published by the watch process, so
nothing recorded it, and reloadWidgets(null) walks the remembered set -- a
reload-all on a watch whose content only ever came from the phone skipped the
complication entirely.

Proguard is read from the app module, which is the keystore trap one line
further on: the generated proguard.cfg is written only into app/, and Gradle
resolves a bare proguardFiles path against the project it appears in, so the
default release build had the Wear R8 task looking for a file beside itself.

The injected services were typechecked against the real androidx.wear jars
again, and the stub tree corrected where it had been looser than the API.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A signed request may ask for release and debug together, and the phone half
returns both -- so the companion build now returns a Wear artifact per variant,
named the way the phone names its own: MyApp-wear.apk beside MyApp-wear-debug
.apk. Result extraction has to know the longer suffix or the debug artifact
lands on the phone APK's path.

Longest first, because a suffix that is a tail of another would otherwise match
the shorter one. Neither of these two is, but the next one might be.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a270b7b3ac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Ports/Android/src/com/codename1/impl/android/surfaces/CN1SurfaceMirror.java Outdated
Refusing a mirrored image the stored descriptor does not name was wrong, and the
review caught it: onPublished sends the images BEFORE the descriptor that names
them -- deliberately, so a descriptor is never live against art that has not
landed -- so the normal case is an image arriving while the PREVIOUS descriptor
is still on disk. The check rejected exactly the art the next descriptor was
waiting for, the transfer was then acknowledged and gone, and the new descriptor
referenced a blob that would never exist. Trading a gap for a broken image is
not a trade.

The hazard it was meant to close is real but much smaller than it looked, and
that reasoning is now recorded where the next reader will find it: every
descriptor collects what it does not reference, so art orphaned by a later
publication is removed by the next one, and only the art in flight during the
last publish of all can linger. A fixed cost, not unbounded growth.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ccccac3757

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

A mistyped family name is refused instead of silently doing nothing. isWatch
tested for a "watch" prefix, so "watchCircle" counted as a watch family here
while every mapping downstream -- the layout picker, the complication types, the
Tile decision -- recognises only the real four. The kind lost its phone widget,
gained watch codegen, and ended up with no usable surface on any platform, in a
build that went green. isWatch is now exact, and the builder names the offending
family and lists the real ones rather than treating a typo as a phone family and
rendering a home-screen widget nobody asked for.

Two findings I do not think are bugs, argued in the code where the next reader
will meet them rather than in a thread they will not:

The Tile's inline images stay PNG with IMAGE_FORMAT_UNDEFINED. The library
documents that pairing: of the format it says it "may be left unspecified or set
to IMAGE_FORMAT_UNDEFINED in which case the platform will attempt to extract
this from the raw image data", and of widthPx and heightPx that they are "only
required for formats (e.g. IMAGE_FORMAT_RGB_565) where the image data does not
include size". The named formats describe a raw pixel buffer; a PNG carries its
own header.

SHORT_TEXT still asks for the inline family first. The name is the first thing
pickLayout tries, and when the document has no layout under it the picker falls
back through the kind's other watch layouts -- and only declared families are in
the document at all, so the fallback lands on one of the kind's own. Choosing
here would move the decision somewhere that cannot see the document.

And the advertised complication types stay derived from the declared family:
whether a layout will hold a progress node or an image is a property of what the
app publishes at runtime and can change between publishes, so narrowing at build
time would guess about a document that does not exist yet, and guessing low
makes a kind unselectable in a slot it will later fill.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f655db0480

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The token check stopped a forged action from dispatching and then fell through
to launchMainActivity anyway, so an app that could not forge anything could
still start the exported trampoline in a loop and foreground this application
over and over -- a nuisance the user would blame on us. An intent that fails the
check now ends the activity and returns, and the check runs before the action is
read so an intent carrying no action id is treated the same way.

The iOS parser refuses an unknown family name too, and there it prevents a worse
failure than on Android: a name this framework does not know is not a watch
family, so the kind reads as an iPhone surface, and familiesSwift cannot map it
either and falls back to all three home-screen sizes. A typo SHIPS three widgets
the manifest never asked for rather than shipping none.

The Wear module reads AIDL from the same tree as the Java. The generated in-app
billing interface for a pre-v8 port is written next to the Java it serves, which
is why the phone gradle names src/main/java as an AIDL root -- and pointing the
wear module at a src/main/aidl that no build creates left it compiling the
billing sources with no IInAppBillingService to compile against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7333c48174

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The Wear application tag stopped being valid XML. Copying
android.xapplication_attr in verbatim while also emitting android:label and
android:icon produced each attribute twice when the hint set one, and a
duplicate attribute is not a merge conflict but a document that does not parse
-- so a companion project with a custom label or icon failed before packaging,
taking the whole build rather than the watch half. The tag is now built the way
the phone manifest builds its own: a default only where the hint has not already
said it.

A reload never reached the watch. reloadWidgets means "draw what you already
hold again", which on the watch is watch-local -- but in a companion build the
call runs in the phone APK and the complication and Tile services live in the
wear module, so the notifier's reflective lookups found nothing. The phone
cannot reach into that process, so it asks: the stored descriptor is re-sent
over the mirror, the watch applies it as it applies any other, and its notifier
runs where the generated services actually are. The nonce is load-bearing -- the
Data Layer suppresses an unchanged item, which is what a publish wants and what
a reload has to defeat.

Rows and columns space their children. setSpacing serializes as "spacing" and
the Tile added children straight onto the builder, so a descriptor that spaces
correctly in the simulator, in WidgetKit and in an Android widget packed
together here. ProtoLayout containers have no spacing property, so the gap is an
explicit element sized along the container's axis.

Linear progress renders as a bar. Reaching for an Arc unconditionally turned
every default SurfaceProgress into a ring -- a different shape from the one the
same descriptor draws everywhere else. The arc is now reserved for the circular
style it was added for.

WRITE_EXTERNAL_STORAGE reaches the Wear manifest, so a watch lifecycle is not
refused on API 26 to 29 an operation the phone artifact is permitted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 291429ae07

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

A role suffix is a claim about a set, not about a name. An app whose artifact is
legitimately called "fitness-wear" returns one APK, and reading the suffix off
its name alone made the primary artifact a companion: it was copied to
<finalName>-wear.apk, attached under a classifier, and the artifact the build was
actually for was missing. The role is now decided from what else came back --
per extension, because a build can return a primary APK and no primary AAB.

The Wear manifest declares the push service. The wear module compiles the same
generated CN1FirebaseMessagingService, resolves the same Firebase dependencies
and now carries the same google-services.json -- everything except the
declaration that lets Play services bind it, so a push arriving on the watch had
nothing to deliver to.

A Tile progress node honours its own modifiers. Both branches returned the Arc
or the bar directly, skipping modifiers(node) -- the only place a ProtoLayout
Clickable is built -- so a progress node with setAction on it rendered and then
ignored the tap, alone among the actionable node types, and lost its padding and
background with it. Neither Arc nor Row has a setModifiers of its own, so the
element is wrapped in a Box that does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1fcd13f3a2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Ports/iOSPort/src/com/codename1/impl/ios/IOSSurfaceBridge.java
reloadWidgets forwards to the watch on iOS too. surfacesReloadTimelines drives
WidgetCenter, and a complication lives in another bundle on another device with
its own copy of the descriptor -- so a reload redrew the phone's widget and left
the watch showing what it had. The stored descriptor is handed over again, for
one kind or for every kind that has published. No images: their names are
content hashes, so whatever it references is already there.

Tile rows and columns honour child weights. setWeight serializes as "weight" and
adding the rendered child straight onto the container ignored it, so two
children weighted 1 and 2 came out naturally sized while every other renderer
split the row between them. ProtoLayout expresses the share as a dimension
rather than a property of the child, and a built element has no size to set
afterwards, so the child goes inside a Box that carries it.

An app group is matched as a whole token. The same trap the profile check
already documents, one layer up: a project declaring group.com.example.shared
contains the string group.com.example, so a substring test read the surfaces
group as present and left the entitlement out -- and then the container does not
resolve, areWidgetsSupported() answers false, and publish() returns before the
bridge, taking the watch mirror with it in exactly the watch-only configuration
this entitlement was widened for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bcce5be3b2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The role rule was still wrong for the case that motivated it. An app called
fitness-wear WITH a companion returns fitness-wear and fitness-wear-wear, where
nothing is unsuffixed -- so "is there a primary of this kind" answered no and
classified both as primary, putting them on one path with directory order
deciding which survived. What separates the two is the artifact the suffix
points at: strip it, and a companion names something else in the set while a
primary names nothing. Both readings of the name are now pinned, together.

Tile text resolves semantic colours. ACCENT and SECONDARY_LABEL and the rest
serialize as {"role": ...} with no light or dark value, so testing for "d"
discarded every one of them. The renderer's own resolution is reused rather than
copied -- a role meaning one thing on a home screen and another on a watch face
is a bug nobody would look for -- always in its dark appearance, because a watch
face composites over black and has no light one.

Box children are placed where they asked to be. setAlignment serializes on the
CHILD while a ProtoLayout Box carries the alignment of its contents, so adding
children onto one shared Box gave every documented position the same default.
Each child now gets a Box of its own, which is also what lets siblings sit in
different corners.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0696d62825

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Ports/iOSPort/nativeSources/IOSNative.m Outdated
shai-almog and others added 2 commits August 22, 2026 18:25
The injected Data Layer listener no longer names the surfaces port classes at
compile time. It is injected into EVERY build that references
com.codename1.wearable, including a versioned build pinned to a release older
than external surfaces -- and a hard reference to a class that port does not
contain fails javac in a file the developer never wrote, for a feature they
never enabled. Looked up reflectively now, the way CN1WatchSurfaceNotifier
reaches the androidx classes, and safe against R8 because the builder emits its
surfaces keep rule exactly when the class is there to be found.

Five Tile fidelity gaps, all the same shape -- a serialized field nothing read:

- Cross-axis alignment. A column child declaring LEADING and a row child
  declaring TOP were passing only through the weight wrapper, so the alignment
  work from last round reached SurfaceBox alone. The wrapper is now axis-aware,
  expanding only the cross axis, because expanding a column child's height would
  push every sibling out.
- Fixed sizes. setSize serializes w/h on any node and only the image branch read
  them, so text, dynamic text, progress and containers came out naturally sized.
- Semantic backgrounds. A {"role": ...} background has no dark value, so the
  has("d") gate dropped it and the corner radius that only exists inside it.
- Progress colours. Neither branch read the node's colour: the bar hard-coded
  white and the arc took its default. The track is now the fill's own colour at
  a quarter alpha, so a coloured bar reads as one bar.
- Image tints. The port's decoder does not tint -- the widget renderer applies
  it at the ImageView -- so reusing only the decoder left every tinted glyph its
  original colour. ProtoLayout has the same separation and its own filter.

Every one of these was typechecked against the real androidx.wear jars, and the
stub tree corrected where it had been looser than the API.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The review was right and I resolved that thread before fixing it. Putting the
WCSession activation in initVM made it unreachable in the one launch it exists
for: a mirrored complication update wakes a terminated watch app in the
background, where the SwiftUI root view is not guaranteed to appear -- so
CN1WatchHost.startWithWidth() may never run, initVM never with it, and the
delegate that would receive the update is never installed. The phone half sends
successfully into a session that has no listener, which is silent.

It moves to the generated app delegate's applicationDidFinishLaunching, which
runs on every launch whether or not a view appears. The hook is always declared
in the bridging header and always defined in the generated bootstrap, so the
Swift side calls it unconditionally and only its body depends on whether this
build has a complication extension. Typechecked against the real watchOS SDK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d3403a544

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Ports/iOSPort/src/com/codename1/impl/ios/IOSSurfaceBridge.java Outdated
Both remirror paths send the artwork, not just the descriptor. A reload is how a
watch app installed AFTER the publish gets its first copy of anything, and
handing it a descriptor whose content-hash images have never existed on that
device leaves permanent gaps until the app happens to publish again. The blobs
are read back from the store rather than remembered, because a reload can be
restarts away from the publish that produced them.

Tile images honour the declared scale mode. "fill" crops to the bounds, which is
what the rasterizer does with the same value; unset, every image took
ProtoLayout's default and a fill image was fitted instead. FILL_BOUNDS is not
the match -- it stretches, while the other renderers preserve the aspect ratio --
and "center" has no equivalent at all, which the code now says rather than
mapping it silently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7ca2c09940

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return;
}
// Imagery first, so the descriptor is never live against art that has not landed.
sendImages(kindId, images);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mirror blobs referenced from prior publications

When a watch is installed or reinstalled after artwork was originally published, a later normal publication using new SurfaceImage(previouslyRegisteredName) sends an empty images side-map because that constructor only references an existing blob. This call therefore transfers no artwork before publishing the descriptor, even though the phone store still contains the referenced PNG, leaving the newly installed watch with a permanent image gap unless the application separately calls reloadWidgets(). Merge the descriptor's stored referenced blobs into the mirror payload, as the reload path already does.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant