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 262 commits into
Conversation
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
Cloudflare Preview
|
|
Compared 12 screenshots: 12 matched. |
|
Compared 151 screenshots: 151 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
|
Compared 181 screenshots: 181 matched. |
isCompanionAppInstalled asked the node list, which answers "is a device connected" rather than "is that device running this app" -- a bare watch reported the companion as installed. The build now declares a cn1_wearable capability that the peer half advertises, and the question is asked of that. A build with codename1.watchMain but no codename1.watchStandalone now logs that the companion Wear APK is not generated yet, so the gap is visible where a developer is looking rather than only in the guide. Mirrored to the BuildDaemon. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sample declares a watchMain, so its app scheme carries an embedded watch app with a ParparVM translation of its own and xcodebuild builds that before running a single test. I raised this step's budget in scripts-ios-native.yml when it first timed out and missed that ios-packaging.yml runs the very same script -- it timed out there at 20 minutes for exactly the same reason. Nothing else in that job was wrong: its iOS UI smoke step reports pass=48 fail=119 both here and on the previous commit, which passed, so those screenshot diffs are pre-existing on this branch and non-gating rather than something this change caused. 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: c9f1cb52ee
ℹ️ 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".
…nd tags are elements mirrorSecondaryEntryPointsToBuildArgs runs AFTER overlayCommandLineBuildHints, so a -Dcodename1.arg.watchMain= passed on the command line was already sitting in the properties -- and the mirror overwrote it with the project file's codename1.watchMain. The standard build-argument override silently did nothing on a cloud build: the wrong lifecycle, or a companion where a standalone Wear build was asked for. An existing value wins now; the mirror only carries a project-file setting into the args channel when nothing has put it there already. And the plist scanner matches ELEMENTS rather than literal tag text. `<key >` is the same element as `<key>` to an XML parser -- which is what reads the phone's plist -- so the phone accepted an override the watch never found, and the pair shipped with different versions. Opening and closing tags are both patterns now, and the scan advances past the content rather than by a fixed tag width, since `</key >` is not the length of `</key>`. 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: 28745cb6c1
ℹ️ 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".
…() off the EDT `#if !TARGET_OS_WATCH` around a phone-only @import is the standard Objective-C spelling, and the conditional scanner only understood Swift's os() expressions -- so it read the guard as unevaluable, kept the import, and attached an iOS-only package to the watch target over code the compiler excludes. TargetConditionals are read now, with TARGET_OS_IPHONE deliberately absent from the excluding list: it is 1 on watchOS, so a block guarded by it does compile there. `#ifdef TARGET_OS_WATCH` is not a platform test either -- every one of those macros is always defined, as 0 or 1 -- so it stays unevaluable. And the durable transfer claim no longer writes from the EDT. The delivery callback runs there once the listener has had the payload, and commit() is a synchronous disk write, so a slow or contended store froze rendering and input for the duration of every received transfer. It moves to the same single-threaded timer the rest of the transfer bookkeeping uses -- claims still land in confirmation order -- and the acknowledgement moves with it, staying behind the write rather than reopening the window that ordering exists to close. Nine conditional shapes checked, including the Swift ones from earlier rounds. 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: 9212138f1b
ℹ️ 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".
…and negation is read A spooled record was claimed and charged an attempt even when no listener of its kind existed. Replaying it then only parked the callback in memory, so the confirmation never ran -- and a service process reclaimed over and over charged an attempt per lifetime for a delivery that never reached application code, deleting it on the budget alone after four of them. That is precisely the loss the budget exists to bound. A record nobody can take is now left exactly as it was, for the launch where someone can. `#elif` is the C spelling of `#elseif`, and reading only the Swift form left a suppressed first arm suppressed straight through the watch arm of an Objective-C `#if TARGET_OS_IOS ... #elif TARGET_OS_WATCH ... #endif`. And a NEGATED platform test is the opposite answer. `!TARGET_OS_IOS` is true on watchOS, so that arm is the one the watch compiles -- excluding it merely because the condition mentions TARGET_OS_IOS removed the active arm and dropped the package imported there. The same rule was wrong on the Swift side for `!os(iOS)` and is fixed with it. Ten shapes checked across both spellings. 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: 1433739fac
ℹ️ 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".
…are quoted The clock seed was not enough on its own, as the review points out: twenty requests and a restart ten milliseconds later reissues half of them, and a delayed reply then completes the NEW process's handler with the old payload. Tokens now come from a block reserved in Preferences, and the block after it is recorded BEFORE any of it is handed out -- so a process that dies mid-run has still moved the stored base past everything it could possibly have issued. Exact rather than probabilistic, for one small write per process. With no storage at all it falls back to the clock, which is a weaker discriminator than the counter and a better one than starting from 1 again. And the staged watch sources are emitted as quoted strings rather than a %w[] word list. A translated native source is named after the class it came from, so a project with a space in one -- `My Bridge.m` -- had that name split into two words: the watch target referenced two files that do not exist and linked without the symbols the real one carries. The excluded-sources list keeps %w[] and says why -- it is a constant in this file that no project-supplied name reaches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dropping every .xcassets was too broad. A project that keeps its images in a custom catalog had none of them on the watch, so a UIImage(named:) from watch-reachable code returned nil at runtime with nothing in the build to explain it. Only a catalog carrying an app icon or a launch image is iOS-specific -- those are the sets with no watch-applicable content, and the reason the filter existed -- while a catalog of ordinary image and colour sets compiles for watchOS like any other. A catalog whose path cannot be read keeps the old conservative answer: a missing asset is a runtime nil, but an uninspectable catalog might be the app's own icon set, and that is a build error for everybody. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…module The BuildDaemon build failed with "Could not find the selected project in the reactor: cn1-hardening". Its CI builds the COMPANION CodenameOne branch when one exists, so it was building this branch -- which was ten commits behind master and did not yet have that module. Merging master brings it in; nothing here needed changing for it. putData and transferFile allocated their sequence on the calling thread, and allocating one advances the durable clock floor with a synchronous commit(). Both are called from application code that is usually the EDT, so an ordinary UI-driven publish blocked rendering and input on storage I/O. The whole publication moves to the transfer worker, which preserves the two things that matter: the commit still completes before the item is published, and the worker is single-threaded, so two publications keep the order their callers made them in -- which is exactly what their sequence stamps are meant to record. 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: e147610106
ℹ️ 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".
…able Deferring publication onto the worker made the caller's array a shared reference: a caller that reuses or refills its buffer after putData or transferFile returns had the worker publish whatever the array said by then rather than what was handed over. Both clone before scheduling. And a message or removal always goes through the spool now, even when a listener is registered. Delivering directly looked like the fast path and left the same hole the spool exists to close -- deliverMessage only QUEUES onto the EDT, so a process killed between the queueing and the callback lost a one-shot the Data Layer does not retain. One record, one ordering, and a release that happens when the listener has actually had it. The cost is a small write per message on a Data Layer worker, which is not a UI thread. A reply-bearing request keeps its direct path, and deliberately: its answer cannot be deferred, because the peer times out long before a later launch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deferring putData onto the worker left removeData issuing its delete straight from the caller, so `putData(path, value); removeData(path);` could delete first and let the put land behind it -- leaving the value published after the very call that removed it. The delete goes to the same single-threaded worker now, so the Data Layer sees these operations in the order the application asked for them. That is every mutation of the data namespace on one queue: the publication, the transfer, the transfer acknowledgement and now the removal. What stays off it is sendMessage, which is a live delivery rather than a change to a published value and has no ordering relationship with one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing for it The acknowledgement is published under /cnxk and the listener service's manifest filter routed only /cn1, so the sender was never woken when a receiver confirmed a delivery. Nothing else arms the sender's sweep -- the bridge registers no DataClient listener of its own -- so an acknowledged transfer stayed replicated on both devices until the seven-day hard cap, unless the app happened to send another file or restart. For a large file that is days of storage nobody is using. The filter now carries both namespaces, and the service treats an acknowledgement as what it is: bookkeeping between the two ports, never an app-visible callback, on a path no application names. It arms the sweep through the same coalescing entry point every other caller uses, and builds a bridge if the wake-up found none -- an acknowledgement-only wake-up is exactly the cold case that needs 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: 0e01ba6e96
ℹ️ 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".
…ble, both watch slices required getData collapsed a failed query into null when nothing was cached, which is the public API's "nothing is published" -- so the first read in a cold process that happened to time out told the caller an item was gone while it sat there, and a caller acting on that discards valid state. A cold read is retried now, and the retries are spent only when there is no snapshot to fall back on, since a cached value settles the question on the first failure. When it still cannot be answered the log says so, because the return value cannot. A live request is written down before it is delivered. The live delivery only queues onto the EDT, so a process killed in that window lost the request entirely -- the sender times out either way, but the receiver never learned it had been asked. The record survives and replays on the next launch as a plain message, which is what the request has become by then; the delivery confirmation releases it, so the normal case leaves nothing behind and nothing is delivered twice. And a vendored bundle needs BOTH watch variants before it is linked. The target builds for watchos and watchsimulator, so an .xcframework carrying only the device library, or a .framework declaring only WatchOS, links on hardware and breaks every simulator run -- the same asymmetry the system-framework check already guards by requiring presence in both SDKs. Four bundle shapes checked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sendMessage returns while the node query is still running when the cache is stale -- the first send of a process, typically -- and hands the caller's array to fanOut from the completion callback. A caller that reuses its buffer, which is the natural thing to do with one, therefore had the peer receive whatever the array said by then rather than the message it passed. The publication paths already copy for this reason; this is the send that can outlive its call, and it was the one still holding a reference. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
onPeerConnected arrives on an EXPORTED service with no binding permission, so any installed app can invoke it with a node id of its choosing -- and the id went straight into the provenance allowlist that isKnownNode consults first. The same app could then forge a message or data callback under that id, launch this app and reach its listeners, walking past the check that exists to stop exactly that. Peer notifications are state, not identity. The callback no longer feeds the allowlist: trust comes only from asking Play services -- connectedNodeIds, capabilityNodeIds, getLocalNode -- which isKnownNode already does for itself on the first event it cannot vouch for. A pushed CONNECT no longer inserts its node into the reachable snapshot either, since isPaired(), isReachable() and fanOut answer from that; it is treated as a hint that something changed and answered with a real query. A DISCONNECT is still applied as it arrives -- removing a node can only understate what is reachable, and the next query corrects it. Audited the other feeders while here: every remaining rememberAll() takes the result of a getConnectedNodes() query, and capabilityChanged writes only the bonded set, which no provenance decision reads. 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: 20b966ea5d
ℹ️ 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".
Clearing the phase queue before delivering is necessary -- a delivery runs translated code and must not be replayed re-entrantly -- but on its own it left a window. The bootstrap thread could copy a queued BACKGROUND phase and be descheduled; the main thread's foreground transition then found an empty queue, delivered itself immediately, and the older background arrived after it. The app ends up stopped and minimized while visibly on screen. An empty queue is therefore the wrong question. A new transition asks whether a drain is in flight, and queues behind the batch if one is; the drain loops until the queue is empty, so it delivers whatever arrived while it was working and ownership is released in one place. A static archive now needs a simulator-capable slice as well as a watch device one: arm64_32 and armv7k exist nowhere but watchOS, but the target also builds for watchsimulator, and a device-only archive fails every simulator run. The log names the architectures found so a rejection is attributable. And a DEFINEDNESS test is not a platform test. TargetConditionals defines every one of its macros on every platform, as 0 or 1, so `#ifdef TARGET_OS_IOS` and `#if defined(TARGET_OS_IOS)` are both true on watchOS and the branch compiles there -- reading them as iOS-only dropped the package imported inside an arm the watch does build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The scanningClass contract now states what a null means and what a consumer owes it: no attribution, and DISCARD whatever was being held. That is the client half of a daemon-side gap -- its non-ASM fallback parser, used for every class compiled to a version ASM cannot read, never named the class it was reading, so a setWriteToStore(true) found there was credited to the last ASM-scanned class and the per-root health walk entitled the wrong target or neither. The client's Executor has no fallback path, so the fix itself lives in the BuildDaemon copy; what belongs here is the contract both sides implement, and IPhoneBuilder already ignores a null rather than keeping the previous class. 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: df8757d74c
ℹ️ 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".
…rator A java.util.Timer heap is keyed on execution time alone, so among the schedule(task, 0) calls a burst of mutations makes it can pop tasks out of submission order -- putData/removeData/putData could issue the last put before the delete and leave the path absent, which is the opposite of the ordering two comments in that file claimed. Same single worker thread, now a ScheduledThreadPoolExecutor, which breaks the tie on a submission sequence. The watch phase drain released ownership outside the critical section that saw the queue empty. A transition arriving in that window was queued (a drain was in flight) and then declined a drain thread (that drain's thread had not finished), so it sat there unowned -- unrecoverable in the background, where no later paint comes to notice. The queue test and the release are now one decision. Three in the generated Xcode script, all behaviour a text assertion cannot see: an arm reader that knew #if os(watchOS) but not the equally valid parenthesized spelling, and so mirrored a phone-only import into the watch target; an archive filter calling any arm64 slice watch-simulator-compatible when it is equally an iPhone's, now reading the Mach-O platform; and a catalog filter that skipped a whole mixed Assets.xcassets over its app icon, now staging a watch copy without the incompatible sets. Both builders gain a test that runs the emitted ruby. The watch skins described a landscape they do not ship artwork for. skin_l.png is the portrait image because there is no landscape watch, but safeLandscape* was the transpose -- so rotating measured layouts against a screen wider than the one being drawn. The metadata now matches the image, and the simulator disables rotation for a watch skin. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replacing the Timer with a scheduled executor fixed the ordering but changed what happens to an uncaught throwable. A Timer took its thread down with it -- catastrophic, but impossible to miss. schedule() instead reports the failure into the Future it returns, and nothing here keeps those, so a bug in a mutation or a retry would have vanished without a word. afterExecute reads the completed Future and logs what it finds. A cancellation is routine -- an armed sweep deadline that a nearer one replaced -- and is not reported. 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: 2e4aa700d0
ℹ️ 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".
| watchCmd.set(outIndex + 1, | ||
| WatchNativeBuilder.translationRoot(request.getMainClass())); |
There was a problem hiding this comment.
Preserve reflective asset roots in the watch translation
When watchMain differs from the phone entry point, this second translator pass roots the watch binary only at the generated watch stub, so classes reached exclusively through reflection are removed. The watch UI suite consequently renders placeholders in SVGStatic, SVGAnimatedScreenshotTest, and LottieAnimatedScreenshotTest; watch apps loading generated SVG or Lottie classes the same way will also ship without those assets. Pass the phone translation's reflective keep roots into this watch-specific translation.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e99b7e550c
ℹ️ 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".
| @@ -489,14 +512,26 @@ private void renderPage() { | |||
|
|
|||
| private void renderBasic() { | |||
| page.add(pageTitle("Basic", "Core application settings - title, version, package and icon.")); | |||
| Container grid = new Container(new GridLayout(3, 2)); | |||
| Container grid = new Container(new GridLayout(4, 2)); | |||
There was a problem hiding this comment.
Allocate enough rows for all basic settings
The grid now contains nine components after adding the watch main, standalone switch, and TV main fields, but it declares only four rows by two columns. GridLayout uses the declared row count when calculating preferred height even when extra components force a fifth row during layout, so the Basic settings page underestimates its required height and compresses or clips the final row. Declare five rows (or use a column-driven layout) so all fields contribute to the preferred size.
Useful? React with 👍 / 👎.
The Cloudflare Pages step required both an immutable URL and a "Deployment alias URL:" line in wrangler's output, and failed the job when either was absent. wrangler does not print the alias line reliably -- one deploy of this branch printed it and the next, to the same preview branch, did not -- while the upload, the alias and the deployment had all succeeded. The job went red over a line of console output. The alias still existed: the branch URL this build could not scrape serves the site. PREVIEW_BRANCH is built from a PR number, so it is always lowercase, hyphenated and well inside the 28 characters Cloudflare truncates an alias to, and the URL can simply be rebuilt from it. Only accepted if it responds; otherwise the immutable deployment URL is used and a warning is emitted, since that is a working preview link too. A missing immutable URL is still fatal -- there is nothing to fall back to. 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: cd639b4b63
ℹ️ 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".
| if (dataListeners.isEmpty()) { | ||
| replayRequests.put(key == null ? replay.toString() : key, replay); | ||
| return; |
There was a problem hiding this comment.
Defer replays while the data queue is draining
When a listener registers after more than MAX_PENDING tracked transfers accumulated, the drain runs a queued port replay while drainingData > 0, so every re-offered transfer is parked despite the listener being present. If that replay fills the queue, evicting a one-shot invokes requestReplayAfterDrain() again; this condition sees the registered listener and runs the replay immediately, recursively rescanning and evicting the same durable backlog until the process can overflow its stack or stall. Treat an active drain (and preferably a nonempty queue) like the no-listener case and retain the keyed replay for the next drain pass.
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