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 239 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>
…d cache getData() and getDataPaths() answered an EDT caller from a cache rather than blocking it, which is the right instinct and the wrong contract. Both document absence -- null, and an empty list -- and after a cold launch the cache is empty for durable state that still exists, because the Data Layer has no reason to re-announce an item it delivered before the restart. A UI action reading either one concluded there was no state and discarded it, and priming in the background only makes the NEXT call right when there may not be a next call. invokeAndBlock is what this needed all along: the query runs off the EDT while the EDT keeps painting and handling input, so the caller gets the real answer without a five-second freeze. Android's main thread keeps the cache-and-prime path -- it has no invokeAndBlock and blocking it is an ANR -- and only internal Play services callbacks arrive there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f4f37f3ded
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
NativeLookup.register(X.class, XStub.class) is a hard reference and XStub holds one to the generated implementation, so a stub installing the app-wide list roots every native implementation in its own translation. With a distinct watchMain that meant a phone-only native interface had its Objective-C staged and compiled for watchOS, where a UIKit import or an iOS-only symbol fails the build in code the watch never calls. The watch stub's registrations are now filtered by the watch root's reachable set, like its health bindings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ery native registration Two watch build failures, one of them mine. Revert of "the watch stub registers only the natives the watch reaches". The safety argument for that filter had a hole: the reachability walk only follows references it can resolve INSIDE classesDir, and stops at the first class that lives in the core jar or a cn1lib -- so app-lifecycle -> core class -> app native interface reads as unreachable and the registration was dropped. That is the under-approximating direction I claimed it could not take, and the watch screenshot suite hung at its first capture with the app running and streaming nothing. Filtering these needs a walk over the whole classpath, not over classesDir, which is not a change to make under a red build. The GoogleSignIn break is older and is a real one. The builder switches GOOGLE_SIGNIN and INCLUDE_GOOGLE_CONNECT on by EDITING CodenameOne_GLViewController.h, so both arrive on every slice at once -- and the watch translation stages that same header. Neither Google SDK ships a watchOS slice, so the device build failed with "'GoogleSignIn/GoogleSignIn.h' file not found" from a native source the watch never calls into. Undefining both under TARGET_OS_WATCH also empties the implementation blocks in the .m files, since every one of them is gated on the same two macros -- which is exactly the state an app without Google login already builds in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 42fc550449
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…e health walk Four review findings plus the native-ios timeout. A one-shot message and a data removal were the two deliveries that could be lost outright. Both were handed to WearableConnection's in-memory queue from a service process, on the assumption that the background startActivity() above would bring a lifecycle up to drain it -- and Android 10+ refuses that start. The Data Layer retains neither: a message is not an item at all, and a removal's item is by definition gone, so startup replay reports nothing. Both now go through a durable spool that is written with commit() before anything else, drained oldest first when the app comes up, and cleared before replay so a throwing listener cannot turn a one-shot into a permanent one. A replicated value and a reply-bearing request keep the in-memory path: the first is still published and replayed, and an answer produced after the peer timed out is not an answer. A transfer acknowledgement no longer outlives a failed claim write. commit() returning false was ignored, so a full or unwritable store let the sender retire an item this device has no durable claim on -- and a restart then lost the transfer. The acknowledgement is withheld instead, which costs the sender an item until the hard cap and buys a redelivery this device will accept. The per-root health walk now starts at the generated registries as well as the lifecycle class. Both stubs install the route dispatcher and the annotation bootstraps, and each names every target it can dispatch to, so ParparVM retains a HealthKit screen reached only by route string while a lifecycle-rooted walk cannot see it -- that target shipped unentitled and was refused at runtime. The health binding factory is deliberately not a root: it names every listener, which is the circularity this attribution exists to break. The guide's summary table no longer advertises complications as a feature: the row says where those surfaces will come from, and the targets are not generated. native-ios timed out because the sample declares a watchMain now, so xcodebuild builds an embedded watch app with a ParparVM translation of its own before it can run a test -- 16 of its 25 minutes went on that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b61f329ad3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…nly native shaking The watch readiness predicate proved nothing. It asked `[CodenameOne_GLViewController instance] != nil` on the reasoning that the implementation creates the view controller during Display.init -- but that accessor LAZY-ALLOCATES, so asking made it non-nil and the answer was yes from the moment the runtime flag was set. A phase forwarded in that window reached a half-built VM, or delivered stop() before init() and start() had ever run. The generated bootstrap now publishes readiness itself, after the watch stub's main returns: Display.init has completed by then, the implementation exists and the EDT is running. Spool sequence allocation is serialized. The message worker and the data worker are different threads, and two of them reading the same seq before either committed wrote the same key -- the later commit then overwrote the earlier record and lost a delivery that has no other copy. Allocation, trim, commit and take are one critical section now; replay stays outside it, because a listener runs app code and must not block the next write. Native registrations are filtered per translation root again, and this time only for APP natives. My earlier revert said the walk could not see through the core jar -- that was wrong: the app zip, iOSPort.jar and the ParparVM Java API are all unzipped into classesDir before any of this runs, so the walk has the whole translation classpath. What is true is that com.codename1 plumbing gets reached in ways a constant-pool walk cannot always see, and an absent registration there does not fail the build, it silently disables a service. Framework natives are therefore kept unconditionally and only the developer's own natives are shaken out -- which is the entire case, since it is their Objective-C that imports an iOS-only SDK. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…mport Copying the phone's whole dependency set onto the watch target made Xcode resolve and build every one of them for watchOS, so an iOS-only package used solely by the phone broke the watch build for code the watch never references. No SDK check is possible for a package -- Package.swift is not resolved until long after the project is generated -- but the staged watch tree is the complete set of sources the target compiles, so a product no file in it imports cannot be needed. A skip is logged with the product name, because a module named differently from its product would otherwise fail to link with nothing pointing at the cause. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9f4e301526
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…s on module boundaries The readiness call could never execute. The stub's main reaches Display.init -> postInit -> IOSNative.initVM, whose watch branch blocks its thread forever -- exactly as UIApplicationMain does on the phone -- so a call placed after that main was dead code. The flag stayed false and every background and foreground transition queued for ever, which is the stop()/start() the watch app was missing in the first place. It is published from inside that branch now, immediately after the IOSImplementation callback that installs the lifecycle: safe that early because both forwarders serial-dispatch onto the EDT, so a released phase queues behind the app start the callback just scheduled. The Swift package import test matched substrings. A product Foo looked used by a source importing FooBar, mirroring an unrelated iOS-only package onto the watch -- the exact failure the gate exists to prevent. It matches module boundaries now, and it also reads Swift's declaration-scoped form: `import struct Foo.Bar` names the module Foo just as `import Foo` does, and missing it dropped a product the source genuinely needs. Sixteen cases checked against the generated regex, including both reviewers' examples. And `durable` was declared in claimTransfer instead of confirmTransferDelivered, so the generated bridge did not compile for any Android app that references com.codename1.wearable. My syntax check missed it because javac stops at 100 errors by default and the real one was past the cap, behind the expected noise of unresolved Android and Play services types. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 37559394a2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…used clock floor The non-circular skins advertised a safe area with no horizontal inset, on the reasoning that a rounded rectangle keeps its full width. It keeps it along the middle and nowhere near the corners: the display is drawn with a 28px radius, so the advertised rectangle's own corners fell outside it and a component correctly honouring getDisplaySafeArea() could still be clipped by the bezel -- the one failure this metadata exists to prevent, and the one that only shows on hardware. The inset floor is now derived from the radius, r * (1 - 1/sqrt(2)), and the radius is a named constant so the artwork and the metadata cannot drift. A new test walks every skin's safe corners against the shape actually drawn, circle or rounded rectangle. persistClock advanced its in-memory record BEFORE the write, so a refused commit() was remembered as a success: every later call saw the floor as already persisted, returned early, and nothing ever reached disk. The next process then restored the older floor and could publish below a peer item that is still there, which deliverIfOutranks ranks stale and drops silently. The record now moves only on a successful write, which makes the next observation or publication a retry. And the Swift import matcher accepted only @_exported in front of an import. @preconcurrency, @_implementationOnly and @_spi(Name) are all valid there, as are access-level imports, and each of those lines was read as "unused" -- dropping a module the source cannot compile without. Any attribute list is accepted now; fifteen cases checked against the generated regex. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 079bece2c2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…hrough to its caller The spool removed every record before replaying any of them, so a process reclaimed in that window took the batch with it -- the crash the spool exists to survive. Each record is now claimed rather than deleted: the attempt count is written back first, the callback runs, and only then is the record forgotten. The count is what keeps that bounded, so a listener that throws or a payload that kills the process is retried three times and then abandoned by name, rather than replaying on every launch for ever. Sensor write-through is attributed to the class that asked for it. The flag was app-wide, so a watch lifecycle calling setWriteToStore(true) made the PHONE reach health the moment its own code touched any sensors class -- and entitling the phone against a profile without HealthKit fails release signing. The scanner now reports which class it is reading, and a root reaches health through the sensors package only when it reaches one of the callers. The clock floor is re-derived from what is actually published, in the background at startup. A refused commit leaves nothing on disk and the retry only happens on the next observation, so the first putData of a run could stamp a sequence below a peer item that is still there and have its own update ranked stale. The published items are durable whatever the disk did, and one pass over them puts the clock above all of it -- without refusing to publish, which would trade a possible loss for a certain one. A static archive is judged on its architectures instead of excluded outright. arm64_32 and armv7k exist on watchOS and nowhere else, so either is unambiguous proof of a watch slice -- unlike arm64, which is an iPhone and an Apple silicon watch simulator at once. Skipping every .a left the watch target compiling a caller and failing on its symbols; libPods-*.a stays excluded, since it is generated for the iOS target and never has one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
replaySpooled only QUEUES onto WearableConnection -- the listeners run later on the EDT, and not at all while the delivery is parked for want of one. Forgetting the record at queue time therefore lost exactly what the spool exists to survive: a process that dies between the queueing and the callback. WearableConnection gained a delivery-confirmation callback for the two paths that need it, run once every listener has been offered the payload and never while it is parked, and the bridge releases its record from there. Attempts are charged one record at a time. Incrementing the whole batch up front meant the oldest record crashing the process three times threw away every later message and removal with it -- none of which had been tried even once. The phone stub's native registrations are filtered by the phone root, as the watch stub's already were by its own. With two translations the app-wide list roots every native implementation in both, so a watch-only NativeInterface whose Objective-C imports WatchKit broke the phone target: the mirror image of the failure the watch filter fixes. And an injected plist key is matched on its resolved CONTENT rather than as serialized markup. An XML parser reads <key><![CDATA[CFBundleShortVersionString]]></key> as that key, which is what the phone's plist does -- so the phone suppressed its default while the watch failed to find it and fell back, and the pair shipped with different marketing versions, which archive validation rejects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d0c8bb6b0d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The drain runs from deliverableNow, which every incoming message and removal calls, so a second event re-entered it while the first record was still queued on the EDT. Nothing marked the record as being delivered, so it was claimed a second time: the same callback queued twice, and three such events inside one process burned the whole attempt budget and deleted the durable copy before any listener had run -- turning the protection into the loss it was meant to prevent. Claimed keys are tracked in memory under the same lock that guards the store. A record parked for want of a listener stays claimed for the life of the process, which is right: the parked runnable still holds it, and re-claiming would duplicate the delivery rather than rescue it. A release whose write fails also stays claimed -- the listeners have already had it, so a retry here could only duplicate it, and the next launch is bounded by the attempt count. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2b26070f81
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…hen it cannot decide Two Data Layer workers could both walk the spool: the first claims the oldest key and is descheduled, the second skips that in-flight key and queues the NEXT record, and the oldest then arrives after it. Sorted keys buy nothing if two threads walk the list at once. A single drain owner now holds the walk, and a second thread returns rather than blocking -- it has a live event to deliver, and the drain already running loops until the store is empty, so a record written meanwhile is still picked up. A Swift package product name is not always its module name: package FooKit can export module Foo, and staged code saying `import Foo` matched no product, so the gate concluded FooKit was unused and the watch failed to compile against a module it does import. The mapping lives in Package.swift, which xcodebuild does not resolve until long after this runs -- but whether the assumption HOLDS can be established here: every module the watch sources import is either a product name or a watchOS SDK framework. When that is true the gate is exact and stays on. When some import is attributable to neither, product and module names demonstrably differ, the gate cannot decide, and it steps aside and says so. A package that then fails on watchOS is named by Xcode; a module silently withheld is not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2d35ae605b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…not overtake the drain resetForReload cleared pendingMessages outright while the data queue beside it carefully hands its undelivered paths back. A parked message is one nothing has received yet and there is nothing to receive it from twice: the port consumed the socket frame to park it, and no later enumeration reconstructs a live message the way a replicated value is re-read. Clearing it lost the payload, and a reply-bearing request left the peer waiting out its whole timeout. The queue survives now -- safe because a parked delivery captures the payload and resolves its listeners when it runs, which is the reloaded app's set, and addMessageListener drains it. The single drain owner still let a live event overtake it. A second worker returned from drainSpool immediately, deliverableNow answered true, and it handed the app a new event before the owner had queued an older spooled one -- the reversal the owner was meant to end, arriving through the other door. A live event is spooled instead while a drain is running or any claimed record is unconfirmed, so it takes a later key than everything outstanding and the owner replays it in order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… key is not a key A reply-bearing request arriving at a stopped app went into the process-local queue and nowhere else. On Android 10+ the background activity start is refused, so nothing ever drained that queue and the app never learned it had been asked -- while the ordinary message branch beside it spools for exactly this case. The answer genuinely cannot be saved (the peer waits out a timeout measured in seconds), but the request is still information the app subscribed to: it is spooled as an ordinary one-shot message now, and the listener sees it on the next launch with expectsReply false, which is precisely true -- nobody is waiting any more. The local token is allocated only on the live path, so no origin is recorded for a reply that will never be sent. And the plist scan skips comments and CDATA when looking for the tags themselves, not only inside them. A fragment carrying an example -- <!-- <key>CFBundleVersion</key><string>9.9</string> --> -- is invisible to the phone's XML parser, but the raw search treated it as live: the watch took the commented version while the app it is embedded in kept its real one, and archive validation rejects that mismatch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
When a watch UI contains a drag-dependent control such as a Slider, swipe gesture, or touch-scrolled container, this root view recognizes only a completed SpatialTapGesture and sends a press/release at the final point. No start or movement events reach the existing CN1WatchHost.pointerPressedAtX, pointerDraggedToX, and pointerReleasedAtX methods, so those controls cannot be operated on watchOS; add a drag gesture that forwards all phases while preserving tap behavior.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The watch root view recognised only a completed SpatialTapGesture, so the host saw a press and a release at one point and nothing in between. Every drag-driven control was inert while appearing to have been touched: a Slider could not move, a scrolled container could not be dragged, a swipe never fired. A DragGesture(minimumDistance: 0) forwards all three phases now -- the first onChanged is the press, the rest are drags, onEnded releases -- and covers a plain tap as the one gesture it replaces did, including the case where a gesture ends with no onChanged at all. Drain ownership had a gap at its very end. A worker seeing a drain in progress spools its event and does not start one, and if the owner cleared the flag before that write landed, nobody was left looking and the record waited for unrelated traffic or a restart. Writers raise a dirty flag under the same lock the owner checks before it stops, so the owner either loops or has not finished; and every writer now attempts a drain of its own, which is a no-op when an owner exists. An import the Swift compiler never sees is no longer read as a watch dependency. `#if os(iOS) import PhoneSDK #endif` attached an intentionally iOS-only package to the watch target and broke watchOS resolution over excluded code. Only a condition demonstrably not watchOS is dropped -- an os() test naming another platform, or one negating watchOS -- because dropping an import the watch does need is the worse failure and the one with no explanation attached. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`#if os(watchOS) ... #else ... #endif` kept BOTH arms. A single "am I suppressed" flag only ever switched arms when the FIRST one was suppressed, so a watch-first branch never suppressed its else -- and an iOS-only package imported only by that else was mirrored onto the watch target, breaking watchOS resolution over code the compiler excludes there. The state is per nesting level now, and it records two different facts: whether this arm is suppressed, and whether an arm that POSITIVELY applies to watchOS has already been taken. The second is what closes the else. A condition that cannot be evaluated -- a custom flag, an ObjC macro, a compiler-version test -- still keeps every arm, because guessing either way risks dropping an import the watch needs. Ten shapes checked against the generated stripper: watch-first with and without an else, iOS-first, negated watchOS, elseif chains selecting either platform, a nested conditional inside a watch arm, an unevaluatable condition keeping both arms, and a file with no conditionals at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
When the generated Swift view uses digitalCrownRotation(... by: 1), ordinary crown changes arrive in roughly one-unit steps, but this forwards that unit directly as a pixel delta (and truncates any fractional movement). The shared wheel pipeline therefore moves a container by only about one logical point per crown step—unlike the Android rotary path, which applies the platform scroll factor—making scrolling through watch forms impractically slow and potentially dropping sub-unit changes entirely. Convert the crown movement using an appropriate point-per-step factor before invoking the wheel callback.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…all survive The Digital Crown moved a form by ONE POINT per detent. The Swift view asks for crown values `by: 1`, and that unit went straight to the wheel callback as a pixel delta -- the Android rotary path multiplies by the platform's scroll factor, and watchOS has no equivalent to consult. A line-height-sized step per unit replaces it, and the fractional remainder is carried between events: truncating each one independently threw away every rotation smaller than a point, so a slow deliberate turn produced a stream of zeroes and scrolled nothing. An unevaluable `#elseif` no longer closes its branch. `#if FEATURE_A / #elseif FEATURE_B / #else import FallbackSDK` suppressed the else, and when both flags are off that else is the arm the watch actually compiles -- so its package was dropped and the target failed on a missing module. Only an arm that demonstrably selects watchOS decides a branch now. And an import inside a comment is not an import. A documentation example or a commented-out line still contained the words the regexes look for, so an iOS-only product named in one was attached to the watch target over code that does not exist. Seventeen shapes checked against the generated stripper, including the ten from the previous round to confirm none of them regressed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0aebc3d59c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // deregisters on pause -- and replying anyway handed the sender an empty SUCCESS, | ||
| // so replyReceived fired for a request no application code ever saw. Staying | ||
| // silent lets the sender's own timeout report the failure it actually had. | ||
| if (replyToken != 0 && copy.length > 0) { |
There was a problem hiding this comment.
Repark messages when the listener snapshot is empty
When a message listener is removed after deliver() observes a nonempty list but before this EDT runnable executes, copy is empty and this condition merely suppresses the reply; execution still reaches runDeliveredCallback(delivered). For Android cold-start records, that callback invokes releaseSpooled() and deletes the durable copy even though no listener received the one-shot message, so it cannot be replayed after the listener returns or the process restarts. Repark the message without invoking delivered, as the adjacent tracked-data dispatch does for the same listener-removal race.
Useful? React with 👍 / 👎.
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.
codename1.watchMain, and boots a watch stub. Verified end to end: the watchtarget 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,SVGAnimatedScreenshotTestandLottieAnimatedScreenshotTestfall back to aplaceholder 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.
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 skippedcore-unittests: 4754 pass, with SpotBugs, PMD and Checkstyle at zerotype-check against a stub harness (nothing in CI compiles them) and the
WatchConnectivity native passes
clang -fsyntax-onlyfor iphoneos/arm64--failure-level WARN, Vale, capitalizationbuild-ios-watchdoes run in CI -- it is a job in Test iOS UI buildscripts, on macos-15 with
DEVELOPER_DIRpointed at Xcode 26, and this PRtouches 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