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 202 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 202 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.

@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: 6a59a945a7

ℹ️ 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
… cap

size() and toArray() as two steps is not a snapshot. Codename One's
ArrayList.toArray(T[]) fills a caller-sized array, so a listener removed between
the two leaves the last slot null -- and a null-terminated array is not empty,
so the "no listeners" recovery does not run and the loop dereferences null. For
a tracked transfer that throws before either callback, leaving the port holding
its claim with the payload unrecoverable in that process. All three listener
lists now snapshot under the monitor their registration uses.

The state list had no monitor at all: its add and remove were unsynchronized, so
locking only its snapshot would have guarded nothing. It now guards itself, with
the same test-and-add-in-one-step shape as the other two.

The startup replay retry stopped at the retention window. An unclaimed transfer
is by definition unacknowledged and the sender now keeps those to the seven-day
cap, so giving up at 24 hours abandoned an item still there on day six -- the
same coupling as the unreadable-asset retry, which moved last round.

Verified: SpotBugs 0, PMD 0 forbidden, Checkstyle 0, 4754 core unit tests,
EvictCheck 25/25, injected Android sources type-check.

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

ℹ️ 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/JavaSE/src/com/codename1/impl/javase/JavaSEWearableBridge.java Outdated
Files.move with REPLACE_EXISTING is the normal path, but some filesystems
refuse it -- Windows renameTo over an existing file being the usual one -- and
the fallback unlinked the destination before retrying. The peer can publish in
that gap, and destroying THAT installed this side's older staging file as the
current value with nothing left to correct it.

The fallback now looks before it unlinks. If the value already there is newer
than the one being written, ours is superseded: the staging file is dropped
rather than forced over it, which is the same outcome the ordering rules produce
everywhere else. It is still reported as published, because the path does hold a
value and the caller must not resurrect a tombstone over it. Otherwise the
delete is version-checked, so a write that lands between the check and the
unlink is not destroyed.

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: 2976a4db34

ℹ️ 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
The drain guard made deliverDataRemoved PARK rather than dispatch, which is what
keeps recovery ordered -- but it also meant a captured removal could land behind
a publication that parked earlier in the same window. The next drain then
dispatched the value and the stale removal after it, leaving the listener
removed while getData returned the replacement: the very failure the recovery
exists to prevent, produced by the recovery.

The records are no longer taken out of droppedRemovals up front. They are copied
and left in place, so deliver() -- which already removes an incoming path from
that set -- can still cancel one, and each announcement claims its record first
and stays silent if a publication got there.

Verified: SpotBugs 0, Checkstyle 0, 4754 core unit tests, EvictCheck 25/25.

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

ℹ️ 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
The finally added last round released the drain guard, but the exception still
escaped and aborted the drain loop. The parked deliveries stayed parked, and
because the queue was then non-empty every later publication parked behind them
-- so an app with a registered listener stopped receiving data until some other
listener happened to register.

Each recovery callback is isolated now: the hand-back to the port, the port's
replay, and the removal re-announcement, which reaches the port too because an
eviction hands a payload back. A failing path costs that path and is logged;
the drain continues and the queue empties.

Verified: SpotBugs 0, PMD 0 forbidden, Checkstyle 0, 4754 core unit tests,
EvictCheck 25/25.

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

ℹ️ 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/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
The menu is built from the "skins" preference and only the skin actually loaded
was ever added to it, so launching the companion registered the 45mm face and
the other three shipped skins could never be chosen. The round face is the one
that matters most here -- it is where a layout that assumes a rectangle falls
apart -- and it was unreachable.

The bundled watch skins present on the classpath are merged into the preference
when the menu is built. Merged, not assigned, so anything the user added stays;
skipped when already listed, so it is idempotent. Built at menu time rather than
at launch, so a developer who never starts the companion can still open a watch
skin to check a layout.

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

ℹ️ 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 7, 2026 12:01
mapFamily documents that both the portable names and the WidgetKit spellings are
accepted, and accessoryRectangular honoured that -- accessoryCircular,
accessoryInline and accessoryCorner did not. Worse than being ignored: the
classification tests startsWith("watch"), so those kinds looked like iOS
surfaces, mapFamily skipped the unrecognised names, and familiesSwift fell
through to its systemSmall/Medium/Large default. A complication became three
home-screen widgets rather than being withheld.

Normalised in one place, because four decisions read these names -- the Swift
family list, the watch-only classification, whether a kind has any watch family,
and the corner complication's platform guard -- and three of them tested the
prefix directly.

accessoryRectangular is deliberately not folded into watchRectangular: it is the
spelling of the portable lockscreen family, which the iOS target does host, and
watchRectangular already emits the same Swift family with the renderer picking
the more specific layout.

Plugin suite 466/0 with a case covering both spellings and pinning that
accessoryRectangular keeps its iOS surface; daemon 205/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
My previous commit folded accessoryCircular and accessoryInline into the
portable watch names. That is wrong in the mirror-image way: both are iPhone
LOCK-SCREEN families as well as watch ones, and CN1DescriptorWidget.swift
already renders all three accessory families on iOS -- they sit under
`if #available(iOS 16.0, watchOS 9.0)`, and only .accessoryCorner is inside
`#if os(watchOS)`. Classifying them as watch-only withheld a lock-screen widget
the manifest had asked for, in exchange for fixing the case where one was
emitted as three home-screen widgets.

accessoryRectangular had said so all along: it maps to the portable lockscreen,
not to watchRectangular. So the two namings are NOT interchangeable here -- the
portable watch* names mean "complication only", the WidgetKit spellings mean the
WidgetKit family, which on iOS is the lock screen. Only accessoryCorner
normalises to a watch name, because only it has no iOS surface.

Plugin 466/0, daemon 205/0, with the test rewritten to pin both directions:
accessory spellings keep their iOS surface and emit their own families rather
than the home-screen default, and accessoryCorner stays watch-only.

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

ℹ️ 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/JavaSE/src/com/codename1/impl/javase/JavaSEWearableBridge.java Outdated
shai-almog and others added 2 commits August 7, 2026 12:34
…ansfer claims

removeData deleted the value and then wrote the tombstone, so a writeValue that
failed -- a full disk, a directory gone read-only -- left the value gone and
nothing saying it ever existed. A peer offline for that call came back to no
value and no removal and kept its stale copy for good. The tombstone is written
first now and nothing is destroyed until it is on disk; if the value then
refuses to delete, the tombstone is the newer record and the existing
reconciliation drops the value on this or any later scan. Writing it first was
previously avoided because both records can briefly exist, and that is no longer
a durable state.

The transfer deletion had the same shape one level down: a refused delete kept
the file, and the only thing stopping redelivery was the in-memory seen-marker.
primeSeenData deliberately restores nothing, so after a restart the surviving
one-shot file looked new and was delivered again. A ".consumed" marker is
written beside it instead, honoured by the scan -- which retries the delete and
drops both once it succeeds -- and excluded from delivery, getDataPaths and
gone-detection like the other bookkeeping files.

Verified: SpotBugs 0, Checkstyle 0, 4754 core unit tests, TombCheck 38/38
including a read-only directory proving the removal destroys nothing when it
cannot record itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The startup replay and onDataChanged both inspect the same item on a cold start
-- that race is expected, not exceptional -- and each inspection started its own
retry chain. Every chain then reopens the same asset on the shared transfer
timer for up to the hard cap, each further callback added another, and the work
queued behind them (replay, cleanup) waited longer for it.

The chain is claimed per URI. The claim IS the retryStarts entry, which every
exit already clears -- decode success, our own echo, the item having
disappeared, the hard-cap give-up -- so a chain that ends releases the URI for a
later one with no new bookkeeping to leak. Claiming it at the first attempt also
means the window the chain is bounded by starts when the caller actually first
tried, rather than when it first reached the hard-cap check.

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

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

@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: 369a62e687

ℹ️ 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
shai-almog and others added 3 commits August 7, 2026 13:42
The watch target compiled the PHONE's ParparVM translation and had the phone
stub's main defined away with -Dmain=cn1_watch_phone_main_unused. So watchMain
was never really the watch's entry point, and the watch binary carried
everything the phone reaches rather than what the watch does.

The watch now gets a stub of its own, written into the same source folder so one
javac pass compiles both, and a second translator pass runs over the SAME
classes from that root -- which is what produces the tree-shaking, since the
translator walks out from the entry point it is given. The pass is skipped when
watchMain names the phone's own main class, because then the two roots reach the
same code and it would be the same tree twice.

The staged tree is copied next to the project and the watch target compiles it
instead of the app target's sources; the -Dmain neutraliser now applies only in
the shared case, where it is still needed to stop one binary defining main
twice; and CN1WatchBootstrap.m enters the watch stub, whose symbol is the only
one present in a watch-rooted binary.

applyXcodeSettings generated the xcodeproj script AND ran it, which is why none
of its decisions were tested. Split: buildXcodeScript returns the script and the
tests pin both modes -- own translation compiles the staged sources with no
-Dmain, shared keeps one translation with it -- plus the bootstrap target.

Plugin suite 469/0. NOT yet verified end to end: the generated watch target has
to compile and link, which needs the sample build and the watch golden suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The hot reload builds a new app instance while this class -- static, in a class
loader the reload does not replace -- keeps the old one's listeners. Every later
event then reached both instances: side effects ran twice and callbacks landed
on UI objects belonging to a screen that no longer exists. The dead instance
also stayed strongly reachable through those lists, so it never collected.

resetForReload clears the listeners and the state owed to them -- the parked
deliveries, the dropped-path and dropped-removal records, the replay requests,
the drain guard and the port's recovery handler. Handing those to the
replacement would deliver, as new, what the previous instance had already been
told. Executor's reload hook calls it reflectively so the JavaSE port keeps
building against a core without the wearable API.

Only the simulator needs this; on a device the process dies instead.

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

The summary table said "Companion or standalone" for Android while the section
above it, and AndroidGradleBuilder, say the opposite: leaving
codename1.watchStandalone unset produces only the phone APK. A reader trusting
the summary would pick companion mode and wait for a wearable artifact that is
never built.

This is a correction to the table, NOT a substitute for the feature. Emitting a
second Wear APK in companion mode is still outstanding.

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: 715c28b547

ℹ️ 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
Comment thread docs/developer-guide/Wearables.asciidoc Outdated
shai-almog and others added 2 commits August 7, 2026 15:36
…d the old build

The cast-semantics gate is right and this was a real iOS bug, not a style point:
AndroidWearableSupport cast the reflectively created bridge inside a try whose
handler catches Throwable. ParparVM does not throw ClassCastException, so on iOS
the handler would never run and the wrong object would simply be used. Tested
with instanceof and branched instead.

resetForReload cleared the port's DroppedDeliveryHandler, which is not app
state: the bridge registers it once from its constructor and the simulator
caches that bridge across reloads, so clearing it removed the recovery path for
good -- after a reload with more than MAX_PENDING updates and no listener,
evicted paths stayed marked delivered and could never be re-offered. It is kept
now; the listeners and the state owed to them are still cleared.

The guide still described the build that the second translation replaced --
telling readers the watch starts the phone lifecycle and that CN.isWatch() is
what selects the watch UI. That would send someone's watch screens into the
wrong class. It now says watchMain is the entry point on both platforms, and
keeps the CN.isWatch() advice only for the one case where it still applies:
naming the phone's own main class, which keeps a single translation.

Merged master (the branch was 9 behind, which is where the cast gate came from).

Verified: cast gate clean of wearable entries, Vale 0, snippets 659, SpotBugs 0,
PMD 0, Checkstyle 0, core 4754/0, plugin 492/0, harness 30/30.

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

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

Nine defects in the second-translation work, every one found by building the
watch target and none visible to a unit test -- the tests assert what the
generator writes, these live in what the toolchain does with it.

- The generated watch stub used NativeLookup without importing
  com.codename1.system. This is what CI is failing on right now, and it only
  appears in a project that HAS a native interface.
- The translator refuses a classpath carrying two mains. Both stubs compiled
  into one classes tree, so BOTH passes broke; each stub is now moved into a
  directory of its own and each pass gets exactly one.
- The staged tree was addressed as "watch-src" rather than
  "<Main>-src/watch-src". Most translated files share a basename with the
  phone's, so Xcode resolved them against the PHONE tree and quietly compiled
  those -- only the watch-only names failed, which made a wholly mis-wired
  target look like 16 missing files.
- A pch's quoted includes resolve against its own directory before any search
  path, so the phone's pch gave every watch source a class index that never
  declares the watch stub. The watch tree gets its own copy.
- GCC_PREFIX_HEADER and HEADER_SEARCH_PATHS were set and then overwritten
  further down the same generated script; they now live at the authoritative
  assignment, and the header paths use the SDK-conditional keys that replace
  the plain one.
- Files absent from the watch translation are skipped, not shared: they belong
  to the phone's program, and sharing them pulled in class ids the watch index
  never declares.
- The file set comes from the watch translation, not the app target: the
  phone's translation shakes out what the phone never reaches, which includes
  the watch lifecycle class itself, and the link failed without it.
- The stub's C main is neutralised in BOTH modes. The watch app is
  SwiftUI-rooted, so @main is the entry and the translator's C main is a
  duplicate symbol; only which stub carries the flag differs.

Verified: the watch target now COMPILES AND LINKS -- HelloCodenameOneWatch.app
is produced from a watchMain-rooted translation. Plugin suite 492/0. The
screenshot half of the golden suite is still running locally.

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

ℹ️ 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/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
shai-almog and others added 2 commits August 7, 2026 20:57
…idge

A cn1lib Objective-C source can need -fobjc-arc while the port builds with ARC
off, and the watch target forces CLANG_ENABLE_OBJC_ARC=NO. The shared branch
copies bf.settings for that reason; taking the file list from the watch tree
left no bf to copy from, so those sources would have compiled under the wrong
memory model. The app target's per-file flags are now indexed by basename and
applied to the matching watch source.

JavaSEPort.getWearableBridge was a lazy singleton with side effects behind an
unsynchronized null check: two threads registering listeners at once each built
a bridge, each started a data watcher and a rendezvous thread, and each replaced
the shared dropped-delivery handler -- so the losing instance stayed alive
through its threads, the same peer file produced duplicate callbacks, and its
evicted deliveries recovered through the wrong instance. The accessor is
synchronized.

Plugin suite 492/0; JavaSE port compiles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The phone stub calls SVGRegistry.installGlobal(), and that static reference is
the only thing holding the generated SVG classes in the reachable graph -- they
are reached reflectively otherwise. The watch stub omitted it, so the watch
translation shook out all 24 of them. Correct tree-shaking, wrong result: SVG
and Lottie fell back to a placeholder render on the watch while working on the
phone, which is what the three failing watch goldens were.

The watch stub now emits the same install. Plugin suite 492/0; the golden rerun
that confirms the three tests recover has not completed yet.

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: 48a643c0eb

ℹ️ 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 on lines +807 to +810
com.codename1.ui.Component focused = f.getFocused();
com.codename1.ui.Container c = focused == null ? null : focused.getParent();
while (c != null && !c.isScrollableY()) {
c = c.getParent();

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 Include the focused component in rotary targeting

When the focused control is itself vertically scrollable but has no scrollable parent—for example, a small focused TextArea or List inside a non-scrollable content pane—this starts the search at focused.getParent() and falls back to the content pane's center. The synthetic wheel gesture can therefore land on an unrelated component instead of the focused control, so rotating the Wear OS crown does not scroll it. Test focused.isScrollableY() before walking its parents and use the focused component as the anchor when it qualifies.

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