Watch apps: one entry point, a phone-watch channel, and a simulator that can run the pair - #5487
Watch apps: one entry point, a phone-watch channel, and a simulator that can run the pair#5487shai-almog wants to merge 158 commits into
Conversation
…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>
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
"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>
Cloudflare Preview
|
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>
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>
|
Compared 12 screenshots: 12 matched. |
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>
…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>
There was a problem hiding this comment.
💡 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".
…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>
There was a problem hiding this comment.
💡 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".
…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>
There was a problem hiding this comment.
💡 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".
…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>
There was a problem hiding this comment.
💡 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".
…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>
There was a problem hiding this comment.
💡 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".
…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>
There was a problem hiding this comment.
💡 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".
…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>
There was a problem hiding this comment.
💡 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>
There was a problem hiding this comment.
💡 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".
… 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>
There was a problem hiding this comment.
💡 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".
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>
There was a problem hiding this comment.
💡 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".
…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>
There was a problem hiding this comment.
💡 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".
…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>
There was a problem hiding this comment.
💡 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".
…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>
There was a problem hiding this comment.
💡 Codex Review
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>
There was a problem hiding this comment.
💡 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".
A value read out of ios.plistInject is SERIALIZED text. A disclosure written as "Health & 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 "&" 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. & is decoded last: doing it first would turn "&lt;" into "<" rather than the literal "<" 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>
isWatch()existed without a product on top of it. This turns the watch renderslice 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:
codename1.watchMainbecame abuild argument only on the local path; the server lifts only
codename1.arg.*keys out of the uploaded settings file, so the daemon asked for
watchMainandgot nothing.
companiondefault never embedded the watch app.watchNative.embedCompaniondefaulted tofalse, so the "Embed Watch Content"phase was actively removed even in companion mode.
watchMainreached only iOS. Wear OS was enabled by an unrelatedandroid.wearhint, so a project had to declare the same intent twice.JavaSEPortnever overrodeisWatch(), so the guide's advice to iterate on a watch layout locally wasuntrue.
onGenericMotionEventread only the mouseaxes; rotary input arrives on
SOURCE_ROTARY_ENCODER/AXIS_SCROLL.so the safe area came back zero.
What is new
One setting.
codename1.watchMainis the entire opt-in on both platforms.Nine build hints are deleted; bundle id, deployment target, team id and display
name are derived.
codename1.watchStandaloneis the only other setting — the onething not inferable from the project. Net new hints: zero.
com.codename1.wearable— the phone↔watch channel, same API on Apple Watchand Wear OS, modelled on
com.codename1.car(portable API, SPI bridge, inertwhen 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":
sendMessagefor a live answer,putDataforstate that survives sleep and relaunch,
transferFilefor bulk. Callbacks arriveon the EDT and are queued across a cold start — the platform starts an app purely
to hand it a payload. Backed by
WCSessionon Apple and the Wearable Data Layeron 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 aWatch menu that launches
watchMainin a second process wired to the first — sosendMessageandputDatagenuinely round-trip on the desktop. Two processes,not two windows:
Displayis a singleton and sharing it would hide the bugs thatonly 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/CORNERjoinWidgetSizerather 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
that render the watch families are not generated yet. The guide says so.
phone Stub, so the watch binary is not separately tree-shaken.
Verification
codenameone-maven-plugin: 322 pass, 1 skipped, including 14 new tests--failure-level WARN, Vale, capitalizationbuild-ios-watchgolden suite needs Xcode 26. It is thereal gate for the companion-embed change, which alters the generated Xcode
project.
Server-side half: codenameone/BuildDaemon#watch-apps-product
🤖 Generated with Claude Code