Skip to content

Watch apps: one entry point, a phone-watch channel, and a simulator that can run the pair - #5487

Open
shai-almog wants to merge 239 commits into
masterfrom
watch-apps-product
Open

Watch apps: one entry point, a phone-watch channel, and a simulator that can run the pair#5487
shai-almog wants to merge 239 commits into
masterfrom
watch-apps-product

Conversation

@shai-almog

@shai-almog shai-almog commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

isWatch() existed without a product on top of it. This turns the watch render
slice into a watch app: one setting builds it on both platforms, the two apps can
talk, and you can develop the pair on your desktop.

Bugs this fixes

Wearables are new and nothing depends on them, so these are fixed rather than
preserved:

  • A cloud build never produced a watch app. codename1.watchMain became a
    build argument only on the local path; the server lifts only codename1.arg.*
    keys out of the uploaded settings file, so the daemon asked for watchMain and
    got nothing.
  • The documented companion default never embedded the watch app.
    watchNative.embedCompanion defaulted to false, so the "Embed Watch Content"
    phase was actively removed even in companion mode.
  • watchMain reached only iOS. Wear OS was enabled by an unrelated
    android.wear hint, so a project had to declare the same intent twice.
  • The simulator had no watch form factor. JavaSEPort never overrode
    isWatch(), so the guide's advice to iterate on a watch layout locally was
    untrue.
  • A Wear app could not scroll. onGenericMotionEvent read only the mouse
    axes; rotary input arrives on SOURCE_ROTARY_ENCODER / AXIS_SCROLL.
  • A round Wear face clipped its own corners. No display cutout is reported,
    so the safe area came back zero.

What is new

One setting. codename1.watchMain is the entire opt-in on both platforms.
Nine build hints are deleted; bundle id, deployment target, team id and display
name are derived. codename1.watchStandalone is the only other setting — the one
thing not inferable from the project. Net new hints: zero.

com.codename1.wearable — the phone↔watch channel, same API on Apple Watch
and Wear OS, modelled on com.codename1.car (portable API, SPI bridge, inert
when there is nothing on the other end). It exposes the three transports the
platforms actually give us, because picking the wrong one is the usual reason a
watch app "never gets the update": sendMessage for a live answer, putData for
state that survives sleep and relaunch, transferFile for bulk. Callbacks arrive
on the EDT and are queued across a cold start — the platform starts an app purely
to hand it a payload. Backed by WCSession on Apple and the Wearable Data Layer
on Android, both gated by API scan so apps that never talk to a watch link
nothing.

A simulator that runs the pair. Four generated watch skins (Apple Watch 41/45,
Wear round, Wear square), isWatch() and the "watch" override layer, and a
Watch menu that launches watchMain in a second process wired to the first — so
sendMessage and putData genuinely round-trip on the desktop. Two processes,
not two windows: Display is a singleton and sharing it would hide the bugs that
only appear once the pair is real.

Complications as surfaces families. A complication is a WidgetKit widget in
an accessory family, so WATCH_CIRCULAR/RECTANGULAR/INLINE/CORNER join
WidgetSize rather than getting an API of their own.

The guide, rewritten around the two-app model, with the data-sharing decision
table as its centre.

Not yet done, and stated as such

  • The watchOS widget extension target and the Wear complication/tile services
    that render the watch families are not generated yet. The guide says so.
  • The watch target compiles its OWN ParparVM translation, rooted at
    codename1.watchMain, and boots a watch stub. Verified end to end: the watch
    target compiles, links and renders on the watchOS simulator (166 pass / 3 fail
    / 2 skip). Tree-shaking is real -- the phone tree carries 24 generated SVG
    classes, the watch tree none -- and that is also the one known regression:
    those classes are reached reflectively, so SVGStatic,
    SVGAnimatedScreenshotTest and LottieAnimatedScreenshotTest fall back to a
    placeholder render. The watch pass needs the same reflective roots the phone
    pass keeps. A project whose watch entry point IS the phone main keeps a single
    translation and is unaffected.
  • Companion mode on Android does not yet emit a second wear APK; standalone works.

Verification

CI is the gate. Everything below is what was additionally checked locally before
pushing, and the numbers move as the branch does.

  • codenameone-maven-plugin: 466 pass, 1 skipped
  • core-unittests: 4754 pass, with SpotBugs, PMD and Checkstyle at zero
  • core, JavaSE, iOS and Android ports build; the injected Android bridge sources
    type-check against a stub harness (nothing in CI compiles them) and the
    WatchConnectivity native passes clang -fsyntax-only for iphoneos/arm64
  • docs gates green: snippet validation (659 blocks), Asciidoctor
    --failure-level WARN, Vale, capitalization
  • build-ios-watch does run in CI -- it is a job in Test iOS UI build
    scripts
    , on macos-15 with DEVELOPER_DIR pointed at Xcode 26, and this PR
    touches paths that trigger it. It is the real gate for anything that alters
    the generated Xcode project. It is not run on the authoring machine, and
    during the rapid review cycle several of its runs were cancelled by
    supersession, so check it against the CURRENT head rather than an older run.

Server-side half: codenameone/BuildDaemon#watch-apps-product

🤖 Generated with Claude Code

shai-almog and others added 6 commits July 29, 2026 08:11
…the cloud

Declaring codename1.watchMain is now the entire opt-in for a watch app on both
Apple Watch and Wear OS. Nine build hints are deleted; the bundle id, deployment
target, signing team and display name are derived from settings the project
already has. The only other recognized setting is codename1.watchStandalone,
which says the watch app ships on its own rather than inside the phone app --
the one thing that cannot be inferred.

Three shipped bugs fall out of this:

- A cloud build never produced a watch app. codename1.watchMain was lifted into
  a build argument only on the local path; the server reads only codename1.arg.*
  keys out of the uploaded settings file, so the daemon's WatchNativeBuilder
  asked for "watchMain" and got nothing. createAntProject now mirrors the
  secondary entry points into that namespace.

- The documented companion default never embedded the watch app.
  watchNative.embedCompanion defaulted to false, so the "Embed Watch Content"
  phase was actively removed even in companion mode. Embedding is what declaring
  a watchMain next to a phone main means, so it is no longer opt-in.

- watchMain reached only the iOS build. Wear OS was enabled by an unrelated
  android.wear hint, so a project had to say the same thing twice. Both
  platforms now read the same declaration.

The five byte-identical watchMain/tvMain blocks in CN1BuildMojo collapse into
one table, and WatchNativeBuilder gains the unit tests it never had (10 cases
pinning enablement, distribution, the Info.plist and the generated entry point)
plus 4 covering the cloud mirroring.

Mirrored to the BuildDaemon (WatchNativeBuilder, AndroidGradleBuilder,
IPhoneBuilder), which is the code cloud builds actually run.

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

A watch app and a phone app are two apps on two devices with two sandboxes, and
until now Codename One gave them no way to talk. com.codename1.wearable is that
channel, and it is the same API on Apple Watch and Wear OS.

The API exposes the three transports the platforms actually provide, because
choosing the wrong one is the usual reason a watch app "doesn't get the update":
sendMessage for a live request/response while both apps are awake, putData for
state that must survive sleep and relaunch, and transferFile for bulk. Payloads
carry the primitive types both platforms can move natively. Callbacks arrive on
the EDT and are queued across a cold start -- the platform starts an app purely
to hand it a message, so dropping what arrives before init() finishes would lose
exactly the payload that mattered. With nothing on the other end the whole API is
inert, so app code needs no platform conditionals. Modelled on
com.codename1.car: portable API, spi/WearableBridge from Display, no-op default.

The simulator could not do watch development at all: JavaSEPort never overrode
isWatch(), so it was always false and the guide's advice to iterate on a watch
layout locally was untrue. It now reads watch=true from the skin the same way it
reads tablet, prepends "watch" to the platform overrides so the existing theme
and CSS layers apply, and ships four generated skins -- Apple Watch 41mm and
45mm, Wear round and Wear square. The round one matters: it is where a layout
that assumes a rectangle falls apart, and its safe area is inset accordingly.

A Watch menu launches the project's watchMain in a second simulator process, and
JavaSEWearableBridge connects the pair so sendMessage and putData genuinely round
-trip on the desktop. Two processes rather than two windows in one JVM: Display
is a singleton, and sharing it would hide precisely the bugs that appear once the
pair is real. Replicated data is files in the shared app home, so a value
published while the peer was not running is simply there when it starts; live
messages need a loopback socket, so isReachable() is false with no peer open,
matching the device instead of papering over it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CN1WatchConnectivity is the WCSession delegate behind the phone-to-watch API. The
same file compiles into both the phone target and the watch target: WCSession is
symmetric, so the two halves of a pair run identical code and the Java API behaves
identically at both ends. The three transports land where they belong --
sendMessage on sendMessage:replyHandler:, replicated data on the session's
application context (which survives both apps being killed and is handed to the
peer whenever it next runs), and transferFile on transferFile:metadata:.

Payloads cross as opaque bytes, so the native layer never has to understand the
value model. Reply blocks for inbound messages are parked until the Java side has
hopped to the EDT and answered, which is what lets a listener do real work rather
than having to respond inside the delegate callback.

Gated by API scan like CarPlay and surfaces before it: the builder defines
CN1_USE_WATCHCONNECTIVITY and links WatchConnectivity.framework only when the app
references com.codename1.wearable, so apps that never talk to a watch carry no
WCSession symbols. Unlike the CarPlay and widgets defines this one deliberately
survives on the watch slice -- that is the half that needs it most. It is undone
on tvOS and Mac Catalyst, where WatchConnectivity does not exist.

Mirrored to the BuildDaemon.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three things a Wear OS app needs that the port did not provide.

The Data Layer bridge is the Android half of com.codename1.wearable. It is
injected into the generated project rather than living in the port, because the
port cannot reference play-services-wearable -- the same reason the Android Auto
glue is injected. The three transports land where they belong: a live message on
MessageClient (nearby nodes only), replicated data on a DataItem marked urgent so
the system does not sit on it for minutes, and a file on a background-synced
DataItem. MessageClient is one-way, so a request carries its reply token in the
path and the answer comes back on a matching reply path, which is what makes the
reply handler behave identically to WCSession's. Unlike Apple, Wear allows several
watches on one phone, so sends fan out to every connected node. The listener
service is what Android starts to deliver a message when the app is not running --
exactly the case the API's cold-start queue exists for.

Rotary input: the rotating side button and bezel report on SOURCE_ROTARY_ENCODER /
AXIS_SCROLL, which onGenericMotionEvent did not read -- it handled only the mouse
axes, so a Wear app could not scroll at all. It now feeds the same wheel path the
Digital Crown uses, scaled by the device's own scroll factor.

Round-screen safe area: a circular face reports no display cutout, so the safe
area came back zero and a layout drawn to the full rectangle had its corners eaten
by the bezel. The largest rectangle inside a circle loses about 14.6% a side, and
that is now reserved on top of whatever the system asks for.

Mirrored to the BuildDaemon.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A watch complication is a WidgetKit widget in an accessory family, and a Wear
complication is the same shape again: content-driven, rendered while the app is
not running, fed by a timeline. That is exactly what com.codename1.surfaces
already models, so complications are four new WidgetSize families rather than a
second API with its own serialization, image handling and state model.

WATCH_CIRCULAR / WATCH_RECTANGULAR / WATCH_INLINE / WATCH_CORNER map onto the
WidgetKit accessory families, and the Swift renderer resolves the most specific
published layout: accessoryRectangular prefers "watchRectangular" and falls back
to "lockscreen", so an app that only published a lock-screen layout still gets a
complication, and one that designed for both gets what it designed.
accessoryCorner is emitted behind an os(watchOS) guard -- the symbol does not
exist on iOS, so naming it unguarded would fail to compile the phone extension
over code that could never run.

WidgetTimeline kept one field per family and a switch in three accessors, which
did not survive four more families; it is now a map keyed by family, and the
serializer's content check iterates the enum instead of naming members. Both
changes mean the next family costs one enum constant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The chapter documented build hints. It now documents the product: how one project
produces two apps, what they do and do not share, how to run the pair while you
develop, how they exchange information, and how a complication is published.

The section that matters most is the data one, because the mistake it prevents is
the common one. A watch app and a phone app are two apps in two sandboxes, so
Storage, Preferences and the SQLite database are per device -- a value written on
the phone is simply not on the watch. The three transports exist because they
answer three different questions, and choosing the wrong one is the usual reason
a watch app "never gets the update", so the chapter leads with a decision table
and says plainly which to reach for by default.

Also corrected: the old chapter told developers to iterate on a watch layout in
the simulator, which was untrue until this branch made isWatch() work there.

The complications section states honestly that the families and descriptor
pipeline are in place but the platform targets that render them on a watch face
are not generated yet, rather than implying a working feature.

Snippets are extracted into docs/demos as the guide requires; Vale, LanguageTool,
the capitalization check, snippet validation and the warning-free Asciidoctor
build all pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings July 29, 2026 11:21
chatgpt-codex-connector[bot]

This comment was marked as resolved.

@github-actions

github-actions Bot commented Jul 29, 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)

This comment was marked as resolved.

"wakeups" and the British "honouring" both trip the gate; the guide is US English.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 29, 2026 11:36
@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

chatgpt-codex-connector[bot]

This comment was marked as resolved.

The copyright gate checks added and modified sources, so editing a file that
never had a header brings it into scope. Five files needed one:
GenerateWatchSkins (new), the settings tool's main class, the wearables guide
snippet, the surfaces Swift renderer resource, and BuildHintSchemaDefaults --
which carried a truncated hybrid header naming Codename One in the copyright line
but Oracle in the grant, and matched neither accepted form.

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

This comment was marked as resolved.

Copilot AI review requested due to automatic review settings July 29, 2026 11:44
The Codename One runtime has no java.util.EnumMap, so the Ant build (which
compiles core against CLDC11) failed where the Maven build had not. Lookups here
are by key, so the ordering an EnumMap would give buys nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chatgpt-codex-connector[bot]

This comment was marked as resolved.

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@shai-almog

shai-almog commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

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

SpotBugs treats DM_NUMBER_CTOR and DM_FP_NUMBER_CTOR as build-breaking, and
valueOf caches small values rather than allocating. Five sites across the
wearable API plus the simulator bridge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 29, 2026 12:07
chatgpt-codex-connector[bot]

This comment was marked as resolved.

This comment was marked as resolved.

…d cache

getData() and getDataPaths() answered an EDT caller from a cache rather than
blocking it, which is the right instinct and the wrong contract. Both document
absence -- null, and an empty list -- and after a cold launch the cache is empty
for durable state that still exists, because the Data Layer has no reason to
re-announce an item it delivered before the restart. A UI action reading either
one concluded there was no state and discarded it, and priming in the background
only makes the NEXT call right when there may not be a next call.

invokeAndBlock is what this needed all along: the query runs off the EDT while
the EDT keeps painting and handling input, so the caller gets the real answer
without a five-second freeze. Android's main thread keeps the cache-and-prime
path -- it has no invokeAndBlock and blocking it is an ANR -- and only internal
Play services callbacks arrive there.

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: f4f37f3ded

ℹ️ 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 9, 2026 08:12
NativeLookup.register(X.class, XStub.class) is a hard reference and XStub holds
one to the generated implementation, so a stub installing the app-wide list
roots every native implementation in its own translation. With a distinct
watchMain that meant a phone-only native interface had its Objective-C staged
and compiled for watchOS, where a UIKit import or an iOS-only symbol fails the
build in code the watch never calls. The watch stub's registrations are now
filtered by the watch root's reachable set, like its health bindings.

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

Two watch build failures, one of them mine.

Revert of "the watch stub registers only the natives the watch reaches". The
safety argument for that filter had a hole: the reachability walk only follows
references it can resolve INSIDE classesDir, and stops at the first class that
lives in the core jar or a cn1lib -- so app-lifecycle -> core class -> app
native interface reads as unreachable and the registration was dropped. That is
the under-approximating direction I claimed it could not take, and the watch
screenshot suite hung at its first capture with the app running and streaming
nothing. Filtering these needs a walk over the whole classpath, not over
classesDir, which is not a change to make under a red build.

The GoogleSignIn break is older and is a real one. The builder switches
GOOGLE_SIGNIN and INCLUDE_GOOGLE_CONNECT on by EDITING
CodenameOne_GLViewController.h, so both arrive on every slice at once -- and the
watch translation stages that same header. Neither Google SDK ships a watchOS
slice, so the device build failed with "'GoogleSignIn/GoogleSignIn.h' file not
found" from a native source the watch never calls into. Undefining both under
TARGET_OS_WATCH also empties the implementation blocks in the .m files, since
every one of them is gated on the same two macros -- which is exactly the state
an app without Google login already builds in.

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: 42fc550449

ℹ️ 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 docs/developer-guide/Wearables.asciidoc Outdated
…e health walk

Four review findings plus the native-ios timeout.

A one-shot message and a data removal were the two deliveries that could be lost
outright. Both were handed to WearableConnection's in-memory queue from a
service process, on the assumption that the background startActivity() above
would bring a lifecycle up to drain it -- and Android 10+ refuses that start. The
Data Layer retains neither: a message is not an item at all, and a removal's item
is by definition gone, so startup replay reports nothing. Both now go through a
durable spool that is written with commit() before anything else, drained oldest
first when the app comes up, and cleared before replay so a throwing listener
cannot turn a one-shot into a permanent one. A replicated value and a
reply-bearing request keep the in-memory path: the first is still published and
replayed, and an answer produced after the peer timed out is not an answer.

A transfer acknowledgement no longer outlives a failed claim write. commit()
returning false was ignored, so a full or unwritable store let the sender retire
an item this device has no durable claim on -- and a restart then lost the
transfer. The acknowledgement is withheld instead, which costs the sender an item
until the hard cap and buys a redelivery this device will accept.

The per-root health walk now starts at the generated registries as well as the
lifecycle class. Both stubs install the route dispatcher and the annotation
bootstraps, and each names every target it can dispatch to, so ParparVM retains a
HealthKit screen reached only by route string while a lifecycle-rooted walk
cannot see it -- that target shipped unentitled and was refused at runtime. The
health binding factory is deliberately not a root: it names every listener, which
is the circularity this attribution exists to break.

The guide's summary table no longer advertises complications as a feature: the
row says where those surfaces will come from, and the targets are not generated.

native-ios timed out because the sample declares a watchMain now, so xcodebuild
builds an embedded watch app with a ParparVM translation of its own before it can
run a test -- 16 of its 25 minutes went on that.

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: b61f329ad3

ℹ️ 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/CN1WatchRuntime.m Outdated
shai-almog and others added 2 commits August 9, 2026 16:23
…nly native shaking

The watch readiness predicate proved nothing. It asked
`[CodenameOne_GLViewController instance] != nil` on the reasoning that the
implementation creates the view controller during Display.init -- but that
accessor LAZY-ALLOCATES, so asking made it non-nil and the answer was yes from
the moment the runtime flag was set. A phase forwarded in that window reached a
half-built VM, or delivered stop() before init() and start() had ever run. The
generated bootstrap now publishes readiness itself, after the watch stub's main
returns: Display.init has completed by then, the implementation exists and the
EDT is running.

Spool sequence allocation is serialized. The message worker and the data worker
are different threads, and two of them reading the same seq before either
committed wrote the same key -- the later commit then overwrote the earlier
record and lost a delivery that has no other copy. Allocation, trim, commit and
take are one critical section now; replay stays outside it, because a listener
runs app code and must not block the next write.

Native registrations are filtered per translation root again, and this time only
for APP natives. My earlier revert said the walk could not see through the core
jar -- that was wrong: the app zip, iOSPort.jar and the ParparVM Java API are all
unzipped into classesDir before any of this runs, so the walk has the whole
translation classpath. What is true is that com.codename1 plumbing gets reached
in ways a constant-pool walk cannot always see, and an absent registration there
does not fail the build, it silently disables a service. Framework natives are
therefore kept unconditionally and only the developer's own natives are shaken
out -- which is the entire case, since it is their Objective-C that imports an
iOS-only SDK.

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

Copying the phone's whole dependency set onto the watch target made Xcode
resolve and build every one of them for watchOS, so an iOS-only package used
solely by the phone broke the watch build for code the watch never references.
No SDK check is possible for a package -- Package.swift is not resolved until
long after the project is generated -- but the staged watch tree is the complete
set of sources the target compiles, so a product no file in it imports cannot be
needed. A skip is logged with the product name, because a module named
differently from its product would otherwise fail to link with nothing pointing
at the cause.

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: 9f4e301526

ℹ️ 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".

…s on module boundaries

The readiness call could never execute. The stub's main reaches Display.init ->
postInit -> IOSNative.initVM, whose watch branch blocks its thread forever --
exactly as UIApplicationMain does on the phone -- so a call placed after that
main was dead code. The flag stayed false and every background and foreground
transition queued for ever, which is the stop()/start() the watch app was missing
in the first place. It is published from inside that branch now, immediately
after the IOSImplementation callback that installs the lifecycle: safe that early
because both forwarders serial-dispatch onto the EDT, so a released phase queues
behind the app start the callback just scheduled.

The Swift package import test matched substrings. A product Foo looked used by a
source importing FooBar, mirroring an unrelated iOS-only package onto the watch
-- the exact failure the gate exists to prevent. It matches module boundaries
now, and it also reads Swift's declaration-scoped form: `import struct Foo.Bar`
names the module Foo just as `import Foo` does, and missing it dropped a product
the source genuinely needs. Sixteen cases checked against the generated regex,
including both reviewers' examples.

And `durable` was declared in claimTransfer instead of confirmTransferDelivered,
so the generated bridge did not compile for any Android app that references
com.codename1.wearable. My syntax check missed it because javac stops at 100
errors by default and the real one was past the cap, behind the expected noise
of unresolved Android and Play services types.

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: 37559394a2

ℹ️ 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 tools/watch-skins/GenerateWatchSkins.java Outdated
…used clock floor

The non-circular skins advertised a safe area with no horizontal inset, on the
reasoning that a rounded rectangle keeps its full width. It keeps it along the
middle and nowhere near the corners: the display is drawn with a 28px radius, so
the advertised rectangle's own corners fell outside it and a component correctly
honouring getDisplaySafeArea() could still be clipped by the bezel -- the one
failure this metadata exists to prevent, and the one that only shows on hardware.
The inset floor is now derived from the radius, r * (1 - 1/sqrt(2)), and the
radius is a named constant so the artwork and the metadata cannot drift. A new
test walks every skin's safe corners against the shape actually drawn, circle or
rounded rectangle.

persistClock advanced its in-memory record BEFORE the write, so a refused
commit() was remembered as a success: every later call saw the floor as already
persisted, returned early, and nothing ever reached disk. The next process then
restored the older floor and could publish below a peer item that is still there,
which deliverIfOutranks ranks stale and drops silently. The record now moves only
on a successful write, which makes the next observation or publication a retry.

And the Swift import matcher accepted only @_exported in front of an import.
@preconcurrency, @_implementationOnly and @_spi(Name) are all valid there, as are
access-level imports, and each of those lines was read as "unused" -- dropping a
module the source cannot compile without. Any attribute list is accepted now;
fifteen cases checked against the generated regex.

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: 079bece2c2

ℹ️ 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 9, 2026 21:52
…hrough to its caller

The spool removed every record before replaying any of them, so a process
reclaimed in that window took the batch with it -- the crash the spool exists to
survive. Each record is now claimed rather than deleted: the attempt count is
written back first, the callback runs, and only then is the record forgotten. The
count is what keeps that bounded, so a listener that throws or a payload that
kills the process is retried three times and then abandoned by name, rather than
replaying on every launch for ever.

Sensor write-through is attributed to the class that asked for it. The flag was
app-wide, so a watch lifecycle calling setWriteToStore(true) made the PHONE reach
health the moment its own code touched any sensors class -- and entitling the
phone against a profile without HealthKit fails release signing. The scanner now
reports which class it is reading, and a root reaches health through the sensors
package only when it reaches one of the callers.

The clock floor is re-derived from what is actually published, in the background
at startup. A refused commit leaves nothing on disk and the retry only happens on
the next observation, so the first putData of a run could stamp a sequence below
a peer item that is still there and have its own update ranked stale. The
published items are durable whatever the disk did, and one pass over them puts
the clock above all of it -- without refusing to publish, which would trade a
possible loss for a certain one.

A static archive is judged on its architectures instead of excluded outright.
arm64_32 and armv7k exist on watchOS and nowhere else, so either is unambiguous
proof of a watch slice -- unlike arm64, which is an iPhone and an Apple silicon
watch simulator at once. Skipping every .a left the watch target compiling a
caller and failing on its symbols; libPods-*.a stays excluded, since it is
generated for the iOS target and never has one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
replaySpooled only QUEUES onto WearableConnection -- the listeners run later on
the EDT, and not at all while the delivery is parked for want of one. Forgetting
the record at queue time therefore lost exactly what the spool exists to survive:
a process that dies between the queueing and the callback. WearableConnection
gained a delivery-confirmation callback for the two paths that need it, run once
every listener has been offered the payload and never while it is parked, and the
bridge releases its record from there.

Attempts are charged one record at a time. Incrementing the whole batch up front
meant the oldest record crashing the process three times threw away every later
message and removal with it -- none of which had been tried even once.

The phone stub's native registrations are filtered by the phone root, as the
watch stub's already were by its own. With two translations the app-wide list
roots every native implementation in both, so a watch-only NativeInterface whose
Objective-C imports WatchKit broke the phone target: the mirror image of the
failure the watch filter fixes.

And an injected plist key is matched on its resolved CONTENT rather than as
serialized markup. An XML parser reads
<key><![CDATA[CFBundleShortVersionString]]></key> as that key, which is what the
phone's plist does -- so the phone suppressed its default while the watch failed
to find it and fell back, and the pair shipped with different marketing versions,
which archive validation rejects.

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: d0c8bb6b0d

ℹ️ 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 drain runs from deliverableNow, which every incoming message and removal
calls, so a second event re-entered it while the first record was still queued on
the EDT. Nothing marked the record as being delivered, so it was claimed a second
time: the same callback queued twice, and three such events inside one process
burned the whole attempt budget and deleted the durable copy before any listener
had run -- turning the protection into the loss it was meant to prevent.

Claimed keys are tracked in memory under the same lock that guards the store. A
record parked for want of a listener stays claimed for the life of the process,
which is right: the parked runnable still holds it, and re-claiming would
duplicate the delivery rather than rescue it. A release whose write fails also
stays claimed -- the listeners have already had it, so a retry here could only
duplicate it, and the next launch is bounded by the attempt count.

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: 2b26070f81

ℹ️ 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".

…hen it cannot decide

Two Data Layer workers could both walk the spool: the first claims the oldest key
and is descheduled, the second skips that in-flight key and queues the NEXT
record, and the oldest then arrives after it. Sorted keys buy nothing if two
threads walk the list at once. A single drain owner now holds the walk, and a
second thread returns rather than blocking -- it has a live event to deliver, and
the drain already running loops until the store is empty, so a record written
meanwhile is still picked up.

A Swift package product name is not always its module name: package FooKit can
export module Foo, and staged code saying `import Foo` matched no product, so the
gate concluded FooKit was unused and the watch failed to compile against a module
it does import. The mapping lives in Package.swift, which xcodebuild does not
resolve until long after this runs -- but whether the assumption HOLDS can be
established here: every module the watch sources import is either a product name
or a watchOS SDK framework. When that is true the gate is exact and stays on.
When some import is attributable to neither, product and module names
demonstrably differ, the gate cannot decide, and it steps aside and says so. A
package that then fails on watchOS is named by Xcode; a module silently withheld
is not.

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: 2d35ae605b

ℹ️ 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 CodenameOne/src/com/codename1/wearable/WearableConnection.java Outdated
shai-almog and others added 2 commits August 10, 2026 10:17
…not overtake the drain

resetForReload cleared pendingMessages outright while the data queue beside it
carefully hands its undelivered paths back. A parked message is one nothing has
received yet and there is nothing to receive it from twice: the port consumed the
socket frame to park it, and no later enumeration reconstructs a live message the
way a replicated value is re-read. Clearing it lost the payload, and a
reply-bearing request left the peer waiting out its whole timeout. The queue
survives now -- safe because a parked delivery captures the payload and resolves
its listeners when it runs, which is the reloaded app's set, and addMessageListener
drains it.

The single drain owner still let a live event overtake it. A second worker
returned from drainSpool immediately, deliverableNow answered true, and it handed
the app a new event before the owner had queued an older spooled one -- the
reversal the owner was meant to end, arriving through the other door. A live event
is spooled instead while a drain is running or any claimed record is unconfirmed,
so it takes a later key than everything outstanding and the owner replays it in
order.

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

A reply-bearing request arriving at a stopped app went into the process-local
queue and nowhere else. On Android 10+ the background activity start is refused,
so nothing ever drained that queue and the app never learned it had been asked --
while the ordinary message branch beside it spools for exactly this case. The
answer genuinely cannot be saved (the peer waits out a timeout measured in
seconds), but the request is still information the app subscribed to: it is
spooled as an ordinary one-shot message now, and the listener sees it on the next
launch with expectsReply false, which is precisely true -- nobody is waiting any
more. The local token is allocated only on the live path, so no origin is
recorded for a reply that will never be sent.

And the plist scan skips comments and CDATA when looking for the tags themselves,
not only inside them. A fragment carrying an example --
<!-- <key>CFBundleVersion</key><string>9.9</string> --> -- is invisible to the
phone's XML parser, but the raw search treated it as live: the watch took the
commented version while the app it is embedded in kept its real one, and archive
validation rejects that mismatch.

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

.append(" .gesture(SpatialTapGesture().onEnded { e in\n")
.append(" CN1WatchHost.shared().tapAt(x: Int32(e.location.x), y: Int32(e.location.y))\n")
.append(" })\n")

P2 Badge Forward drag gestures into the watch pointer pipeline

When a watch UI contains a drag-dependent control such as a Slider, swipe gesture, or touch-scrolled container, this root view recognizes only a completed SpatialTapGesture and sends a press/release at the final point. No start or movement events reach the existing CN1WatchHost.pointerPressedAtX, pointerDraggedToX, and pointerReleasedAtX methods, so those controls cannot be operated on watchOS; add a drag gesture that forwards all phases while preserving tap behavior.

ℹ️ 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 10, 2026 11:15
The watch root view recognised only a completed SpatialTapGesture, so the host
saw a press and a release at one point and nothing in between. Every drag-driven
control was inert while appearing to have been touched: a Slider could not move,
a scrolled container could not be dragged, a swipe never fired. A
DragGesture(minimumDistance: 0) forwards all three phases now -- the first
onChanged is the press, the rest are drags, onEnded releases -- and covers a plain
tap as the one gesture it replaces did, including the case where a gesture ends
with no onChanged at all.

Drain ownership had a gap at its very end. A worker seeing a drain in progress
spools its event and does not start one, and if the owner cleared the flag before
that write landed, nobody was left looking and the record waited for unrelated
traffic or a restart. Writers raise a dirty flag under the same lock the owner
checks before it stops, so the owner either loops or has not finished; and every
writer now attempts a drain of its own, which is a no-op when an owner exists.

An import the Swift compiler never sees is no longer read as a watch dependency.
`#if os(iOS) import PhoneSDK #endif` attached an intentionally iOS-only package to
the watch target and broke watchOS resolution over excluded code. Only a
condition demonstrably not watchOS is dropped -- an os() test naming another
platform, or one negating watchOS -- because dropping an import the watch does
need is the worse failure and the one with no explanation attached.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`#if os(watchOS) ... #else ... #endif` kept BOTH arms. A single "am I suppressed"
flag only ever switched arms when the FIRST one was suppressed, so a watch-first
branch never suppressed its else -- and an iOS-only package imported only by that
else was mirrored onto the watch target, breaking watchOS resolution over code the
compiler excludes there.

The state is per nesting level now, and it records two different facts: whether
this arm is suppressed, and whether an arm that POSITIVELY applies to watchOS has
already been taken. The second is what closes the else. A condition that cannot be
evaluated -- a custom flag, an ObjC macro, a compiler-version test -- still keeps
every arm, because guessing either way risks dropping an import the watch needs.

Ten shapes checked against the generated stripper: watch-first with and without an
else, iOS-first, negated watchOS, elseif chains selecting either platform, a
nested conditional inside a watch arm, an unevaluatable condition keeping both
arms, and a file with no conditionals at all.

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

pointerWheelMovedCallback(cx, cy, 0, (int)(-crownDelta));

P2 Badge Scale Digital Crown deltas to usable scroll distances

When the generated Swift view uses digitalCrownRotation(... by: 1), ordinary crown changes arrive in roughly one-unit steps, but this forwards that unit directly as a pixel delta (and truncates any fractional movement). The shared wheel pipeline therefore moves a container by only about one logical point per crown step—unlike the Android rotary path, which applies the platform scroll factor—making scrolling through watch forms impractically slow and potentially dropping sub-unit changes entirely. Convert the crown movement using an appropriate point-per-step factor before invoking the wheel callback.

ℹ️ 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".

…all survive

The Digital Crown moved a form by ONE POINT per detent. The Swift view asks for
crown values `by: 1`, and that unit went straight to the wheel callback as a pixel
delta -- the Android rotary path multiplies by the platform's scroll factor, and
watchOS has no equivalent to consult. A line-height-sized step per unit replaces
it, and the fractional remainder is carried between events: truncating each one
independently threw away every rotation smaller than a point, so a slow deliberate
turn produced a stream of zeroes and scrolled nothing.

An unevaluable `#elseif` no longer closes its branch. `#if FEATURE_A / #elseif
FEATURE_B / #else import FallbackSDK` suppressed the else, and when both flags are
off that else is the arm the watch actually compiles -- so its package was dropped
and the target failed on a missing module. Only an arm that demonstrably selects
watchOS decides a branch now.

And an import inside a comment is not an import. A documentation example or a
commented-out line still contained the words the regexes look for, so an iOS-only
product named in one was attached to the watch target over code that does not
exist.

Seventeen shapes checked against the generated stripper, including the ten from
the previous round to confirm none of them regressed.

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: 0aebc3d59c

ℹ️ 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".

// deregisters on pause -- and replying anyway handed the sender an empty SUCCESS,
// so replyReceived fired for a request no application code ever saw. Staying
// silent lets the sender's own timeout report the failure it actually had.
if (replyToken != 0 && copy.length > 0) {

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 Repark messages when the listener snapshot is empty

When a message listener is removed after deliver() observes a nonempty list but before this EDT runnable executes, copy is empty and this condition merely suppresses the reply; execution still reaches runDeliveredCallback(delivered). For Android cold-start records, that callback invokes releaseSpooled() and deletes the durable copy even though no listener received the one-shot message, so it cannot be replayed after the listener returns or the process restarts. Repark the message without invoking delivered, as the adjacent tracked-data dispatch does for the same listener-removal race.

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.

2 participants