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 158 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 158 commits into
masterfrom
watch-apps-product

Conversation

@shai-almog

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 still compiles the shared ParparVM translation and boots the
    phone Stub, so the watch binary is not separately tree-shaken.
  • Companion mode on Android does not yet emit a second wear APK; standalone works.

Verification

  • codenameone-maven-plugin: 322 pass, 1 skipped, including 14 new tests
  • core, JavaSE, iOS and Android ports all build; settings tool compiles
  • docs gates green: snippet validation (634 blocks), Asciidoctor --failure-level WARN, Vale, capitalization
  • Not run here: the build-ios-watch golden suite needs Xcode 26. It is the
    real gate for the companion-embed change, which alters the generated Xcode
    project.

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.

…spatched one

Suppressing the dispatch was right; reusing the monotonic recorder for the
bookkeeping was not. After a deletion the recorded stamp describes the item just
removed and a survivor routinely carries a LOWER sequence, so an outranks test
left the baseline sitting on a dead item and filtered out every later peer
publication beneath it, with no callback guaranteed to correct that.

The two updates also have to be one step. Splitting the stamp from the snapshot
let a peer publication land between them: it advanced the stamp, cached and
delivered its own payload, and this older resolver then overwrote the value
alone, so getData answered with stale local bytes the listener had never seen.
recordLocalWinnerIfStampUnchanged does both under one lock, and the non-deletion
case uses the existing atomic recordLocalEcho.

The watch's team id now follows ios.buildType exactly as the phone's does.
Preferring the release team unconditionally paired a debug provisioning profile
with the release team on any project that set both hints, which fails manual
signing of the embedded target. Three assertions pin debug, release and the
plain ios.teamId fallback.

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: 8ac177d5f6

ℹ️ 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
…rivacy string

The cap dropped the oldest replicated entry whatever its path, on the reasoning
that a later publication supersedes it -- true only when the paths match. A
burst across many paths therefore discarded callbacks nothing would replace, for
paths the ports have already marked as seen, so registering a listener could not
recover them. Parked replicated deliveries now carry their path, and eviction
prefers an entry the incoming delivery actually supersedes; an unrelated one is
dropped only when nothing does, and a transfer only when nothing else is parked
at all.

The watch Info.plist copied the two HealthKit descriptions and nothing else, so
a watch app using location, the microphone or motion had no purpose string in
its own bundle -- authorization fails, or watchOS terminates the app, on a
project whose hint was set correctly. Every ios.NS*UsageDescription is now
mirrored, collected exactly as the phone builder collects them, with the
whitespace-only rule preserved.

Harnesses cover both: 11/11 on eviction, including that repeating a queued path
leaves the distinct-path count unchanged, and four assertions on the plist.

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: 58541f7063

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

Removing it afterwards left an interval in which both records existed, and the
peer scans every 500ms with no ordering guarantee from listFiles() -- so it
could deliver the removal after the new value and leave the listener showing a
path as deleted while getData returned the replacement. A failed delete would
have made that permanent.

Removing it first leaves only a window in which neither exists, and the next
scan finds the value. Every observable ordering converges on the replacement.

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

ℹ️ 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 Ports/JavaSE/src/com/codename1/impl/javase/JavaSEWearableBridge.java Outdated
Comment thread Ports/JavaSE/src/com/codename1/impl/javase/JavaSEWearableBridge.java Outdated
…tombstone faults

Coalescing by path fixed the supersedable case and left the rest silent: when
nothing matches, an unrelated path is still dropped, the ports have already
recorded it as delivered, and a removal cannot be reconstructed from getData at
all. That path is now remembered and handed to a port-registered handler after
the drain, when a listener exists and there is room. Android forgets the
delivery stamp and resolves the path again; the simulator forgets the file's
seen-marker so the next scan treats it as new. Neither would re-offer anything
without that, which is exactly why dropping the runnable alone was not enough.

Tombstone age now comes from the stamp INSIDE the file. setLastModified can be
refused -- writeValue says so -- and then the mtime is an ordinary wall-clock
value whose half looks decades old, so the author deleted its own tombstone on
the next scan and an offline peer never saw the removal.

And a republish whose write fails restores the tombstone it had already deleted.
writeValue now reports success rather than only logging, so the removal survives
as the last thing that actually happened instead of leaving the peer with
neither the value nor the removal.

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: 09184f4d7f

ℹ️ 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
Comment thread CodenameOne/src/com/codename1/wearable/WearableConnection.java
Comment thread Ports/iOSPort/nativeSources/CN1WatchConnectivity.m
…mbstone faults

The recovery I added last round covered two ports of three. iOS never registered
a handler, and the drain cleared the record anyway -- so on the platform with no
per-path redelivery at all, a discarded callback was gone for good. It now
registers one, and the drain only takes the record when a handler exists to act
on it. The native side forgets the path's received marker, so the next
whole-context replace looks new instead of unchanged.

The record itself is now a bounded set. Recovery is per path, so a repeat adds
nothing, and an app that never registers a listener while the peer churns
through paths would otherwise grow it without limit -- defeating the very cap it
serves. Past the bound the oldest goes and the port's startup replay is the
backstop, as it was before this record existed.

An iOS acknowledgement whose publish FAILS no longer records the tombstone as
announced: it would take every later context update down the unchanged branch,
so the acknowledgement could never be retried and the peer would keep its
tombstone for good.

And the simulator no longer treats a FAILED tombstone delete as an absent one:
publishing over it left both records durable, and a peer reading them in
whatever order listFiles() gave could settle in the removed state while getData
returned live data.

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

ℹ️ About Codex in GitHub

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

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

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

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

Comment thread Ports/iOSPort/src/com/codename1/impl/ios/IOSWearableBridge.java
Comment thread Ports/JavaSE/src/com/codename1/impl/javase/JavaSEWearableBridge.java Outdated
…hat did not remove

Forgetting the received marker recovers nothing on its own: it makes the path
eligible and then waits for a context update the peer has no reason to send --
it has already published that value. The context currently held IS the value, so
the delivery is now re-run over it, through the ordinary path so every rule
about winners, tombstones and acknowledgement still applies. Coalesced to one
pass, since core hands paths back one at a time and re-processing per path would
deliver every other path in the context that many times.

And removeData no longer publishes a tombstone when the value could not be
deleted. Both records would then be durable, a peer reading them in whatever
order listFiles() gave could settle in the removed state while getData returned
the value, and once the tombstone expired the value had long been marked seen
and was never re-delivered. A delete that failed is not a removal, so nothing is
recorded as one.

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

ℹ️ 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
Comment thread Ports/iOSPort/nativeSources/CN1WatchConnectivity.m
Comment thread Ports/JavaSE/src/com/codename1/impl/javase/JavaSEWearableBridge.java Outdated
…S replay

Bounding the dropped-path record stopped it growing and started it forgetting,
which for a removal is unrecoverable -- nothing reconstructs one from getData,
and the port's startup replay may already have run in the same listener-less
process. Overflow now sets a flag and the drain hands the port a null path,
meaning "more was lost than can be named, re-offer what you can". Android
forgets every recorded delivery and re-enumerates, the simulator forgets every
seen file, and iOS drops every received marker and re-runs the held context.
A duplicate is something an app can recognise; a missing update is not.

The iOS recovery replay also re-ran the context handler on the main queue while
WCSession can be running the same handler on its delegate queue -- both
iterating, mutating and replacing _lastReceived with only narrow locks around
the replay flag. The handler body now holds the instance monitor, which is what
makes those two callers exclusive.

And a delivered transfer keeps its seen-marker when the delete fails: dropping
it left an unchanged one-shot file that the next scan read as new, redelivering
it for as long as the deletion kept failing.

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: 22cbe53a1f

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

…tion fallback

A discarded delivery was handed back as a bare path, so the port re-resolved it
under the ordinary rule -- which reads an empty path as "nothing to announce".
That is exactly the state a removal leaves behind, so recovering one announced
nothing and the listener kept the value indefinitely. The record now carries the
kind alongside the path, and Android resolves a dropped removal with deletion
semantics.

The watch plist also translates ios.locationUsageDescription itself. It is a
supported hint -- the phone builder even supplies one when it detects location
use -- and the phone turns it into NSLocationWhenInUseUsageDescription AFTER
this plist is written, so a loop over ios.NS* alone left the watch bundle with
no purpose string while the project was configured correctly. An explicit NS key
still wins, and is not emitted twice.

Harness 18/18, including that a dropped removal comes back flagged as one.

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: 751ca26634

ℹ️ About Codex in GitHub

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

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

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

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

Comment thread Ports/iOSPort/src/com/codename1/impl/ios/IOSWearableBridge.java Outdated
… actually is

Three separate reports, one cause: I was asking the ports to re-derive a dropped
removal, and none of them can. The evidence of a removal is an item that is not
there -- absent from Android's enumeration, absent from the iOS received
context, absent from the simulator's directory -- so every recovery route I
wired led somewhere that had nothing to find. The Android path also erased the
stamp its own announcement then required, and an overflow rescan could not
name what it had lost.

A removal's entire content is its path, and this class already has it. Discarded
removals are now kept apart from discarded changes and re-announced directly on
the drain: a complete recovery rather than a request for one. Ports keep only
what they can actually do -- re-offer a value they still hold -- so the kind
flag added last round is gone again rather than left as a parameter that is
always false.

Keeping them in their own bounded set also stops a burst of ordinary changes
evicting them, which is the one thing that would put them beyond recovery.

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: 8f3a66bd37

ℹ️ 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/CN1WatchConnectivity.m Outdated
The birth records were deleted while assembling the context, before
updateApplicationContext had accepted it. A failed publish then left every
tombstone the prune had "removed" still authoritative in the live context, with
no birth record -- so the next pass read each as newly born and granted it
another full retention window. An oversized context could stay stuck that way
after one transient failure, which is the opposite of what the prune is for.

The prune now collects the paths it retired instead of forgetting them, and all
three callers -- putData, removeData and the standalone sweep -- forget those
records only after the update returns without error. removeData still forgets
its OWN new tombstone's record when the publish fails, because that tombstone
never entered the context and nothing would ever reach the record again.

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: 15c08af4e7

ℹ️ 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
Comment thread Ports/iOSPort/nativeSources/CN1WatchConnectivity.m Outdated
…ombstone's birth

The removal record evicted its oldest entry silently, and that is the one loss
in this class nothing downstream can repair -- a discarded change can be
re-offered by the port and an overflowing change record asks for a rescan, but a
removal has no fallback, because the item is gone and there is nothing anywhere
to rediscover. It now remembers far more of them (a removal is a path and
nothing else, so remembering one is cheap) and logs the path when one finally
falls off, naming the cause. An app that loses one has evidence rather than
silence.

And removeData no longer overwrites the birth record of a tombstone that is
already published, nor deletes it when the publish fails. That tombstone is
still out there with a retention window already running; restarting it -- or
worse, deleting the record so the next pass reads it as newly born -- grants it
another full day. A record this call created is still forgotten on failure,
because that one never entered the context and nothing could ever reach it
again.

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: 089bdd5b67

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

A send captures peerOut before it blocks, so its IOException can surface after
the rendezvous thread has already verified a REPLACEMENT connection. Tearing
down unconditionally then closed a link that was working, and nothing could be
sent until another election completed -- an outage inflicted by the previous
connection's death rather than by anything wrong with the current one.

dropPeer now takes the stream that failed and does nothing when it is no longer
the one in use. The reader keeps the unconditional form: it owns the connection
it is reading, so there is no newer one to protect.

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

ℹ️ 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/WearableMessage.java
…cted purpose strings

A WearableMessage with 32768 or more entries wrote a NEGATIVE signed short, the
reader looped zero times, and the peer accepted a message with every value gone
-- no error on either side. The count is now read unsigned, so the field's real
range works, and a message past that range is refused rather than encoded as
something else. A harness round-trips 40000 entries, which used to arrive empty,
and asserts the refusal above the limit.

The watch plist also mirrors purpose strings supplied through ios.plistInject.
That fragment is a supported way to set plist keys and the phone honours it, but
it is a raw blob rather than arguments, so the ios.NS* loop never saw them and
the watch was terminated when its lifecycle used the API the project had
correctly declared. Only privacy keys are copied, not the whole fragment.

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


P2 Badge Abort builds that lack a watch HealthKit purpose string

When watchNative.health=true is used without the required HealthKit purpose strings, this calls Executor.error(), which only writes to the logger and returns; execution then writes the entitlements and Info.plist and continues the build. The intended fail-fast validation therefore produces an entitled watch app without a purpose string, so its HealthKit authorization fails at runtime. Throw a build exception or otherwise stop generation here instead of merely logging.

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

Mirroring the injected keys created three problems that all came from reading
the two sources separately.

The fallback check looked only at arguments, so an injected
NSLocationWhenInUseUsageDescription did not suppress ios.locationUsageDescription
-- the key was emitted twice and a default could overwrite the developer's own
disclosure. The HealthKit validation read arguments only as well, so it aborted
the build over a purpose string the plist it had just written did contain. And
the membership test I used, request.getArg("ios." + injectedKey, null), reads to
the Shield hint guard as a literal hint named "ios.", which fails every Ant CI
run -- verified by running the guard, which reported it at line 571.

There is now one map, built from the injection and then from the arguments so an
explicit value wins, written out in one pass and read by both checks. The
dynamic getArg is gone with it. The guard reports every interpolated hint
refused or recorded safe again, and three tests pin the behaviours: the injected
key appears exactly once, the fallback yields to it, and an injected HealthKit
string satisfies the validation.

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: 58d446d489

ℹ️ 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 03:05
A value read out of ios.plistInject is SERIALIZED text. A disclosure written as
"Health &amp; Fitness" arrives with the entity intact, and re-emitting it
through the plist escaper escaped the ampersand again -- so the phone showed the
intended wording while the watch permission dialog showed "&amp;" literally, in
front of the user, at the moment they decide whether to grant access.

The five predefined entities are decoded on the way out of the fragment, which
puts the value back into the plain form the escaper expects. &amp; is decoded
last: doing it first would turn "&amp;lt;" into "<" rather than the literal
"&lt;" the author wrote.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Expiring it on age alone reintroduced the failure the tombstone exists to
prevent: a peer closed for longer than the window came back to neither a value
nor a removal, so an app that persisted the value never learned it was gone. The
device ports both keep theirs until the peer acknowledges; this one now does the
same.

The consumer writes an acknowledgement beside the tombstone after delivering the
removal, and the author retires it only once that exists AND it has aged out. A
count cap is the backstop, so an app whose peer never runs cannot fill the shared
directory -- past it the oldest go, acknowledged or not. Acknowledgements are
bookkeeping: never delivered, never enumerated as a path, and their
disappearance is not a removal.

The harness ages a tombstone by rewriting its frame rather than touching the
mtime -- the age comes from the stamp inside the file precisely so the
filesystem cannot decide it -- and asserts both directions: unacknowledged
survives its TTL, acknowledged and aged is retired with its acknowledgement.

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