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 202 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>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6a59a945a7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… cap size() and toArray() as two steps is not a snapshot. Codename One's ArrayList.toArray(T[]) fills a caller-sized array, so a listener removed between the two leaves the last slot null -- and a null-terminated array is not empty, so the "no listeners" recovery does not run and the loop dereferences null. For a tracked transfer that throws before either callback, leaving the port holding its claim with the payload unrecoverable in that process. All three listener lists now snapshot under the monitor their registration uses. The state list had no monitor at all: its add and remove were unsynchronized, so locking only its snapshot would have guarded nothing. It now guards itself, with the same test-and-add-in-one-step shape as the other two. The startup replay retry stopped at the retention window. An unclaimed transfer is by definition unacknowledged and the sender now keeps those to the seven-day cap, so giving up at 24 hours abandoned an item still there on day six -- the same coupling as the unreadable-asset retry, which moved last round. Verified: SpotBugs 0, PMD 0 forbidden, Checkstyle 0, 4754 core unit tests, EvictCheck 25/25, injected Android sources type-check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a1ab69a7e5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Files.move with REPLACE_EXISTING is the normal path, but some filesystems refuse it -- Windows renameTo over an existing file being the usual one -- and the fallback unlinked the destination before retrying. The peer can publish in that gap, and destroying THAT installed this side's older staging file as the current value with nothing left to correct it. The fallback now looks before it unlinks. If the value already there is newer than the one being written, ours is superseded: the staging file is dropped rather than forced over it, which is the same outcome the ordering rules produce everywhere else. It is still reported as published, because the path does hold a value and the caller must not resurrect a tombstone over it. Otherwise the delete is version-checked, so a write that lands between the check and the unlink is not destroyed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2976a4db34
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The drain guard made deliverDataRemoved PARK rather than dispatch, which is what keeps recovery ordered -- but it also meant a captured removal could land behind a publication that parked earlier in the same window. The next drain then dispatched the value and the stale removal after it, leaving the listener removed while getData returned the replacement: the very failure the recovery exists to prevent, produced by the recovery. The records are no longer taken out of droppedRemovals up front. They are copied and left in place, so deliver() -- which already removes an incoming path from that set -- can still cancel one, and each announcement claims its record first and stays silent if a publication got there. Verified: SpotBugs 0, Checkstyle 0, 4754 core unit tests, EvictCheck 25/25. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: acc24230aa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The finally added last round released the drain guard, but the exception still escaped and aborted the drain loop. The parked deliveries stayed parked, and because the queue was then non-empty every later publication parked behind them -- so an app with a registered listener stopped receiving data until some other listener happened to register. Each recovery callback is isolated now: the hand-back to the port, the port's replay, and the removal re-announcement, which reaches the port too because an eviction hands a payload back. A failing path costs that path and is logged; the drain continues and the queue empties. Verified: SpotBugs 0, PMD 0 forbidden, Checkstyle 0, 4754 core unit tests, EvictCheck 25/25. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dc9173d9b5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The menu is built from the "skins" preference and only the skin actually loaded was ever added to it, so launching the companion registered the 45mm face and the other three shipped skins could never be chosen. The round face is the one that matters most here -- it is where a layout that assumes a rectangle falls apart -- and it was unreachable. The bundled watch skins present on the classpath are merged into the preference when the menu is built. Merged, not assigned, so anything the user added stays; skipped when already listed, so it is idempotent. Built at menu time rather than at launch, so a developer who never starts the companion can still open a watch skin to check a layout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ac1a3a263c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
mapFamily documents that both the portable names and the WidgetKit spellings are
accepted, and accessoryRectangular honoured that -- accessoryCircular,
accessoryInline and accessoryCorner did not. Worse than being ignored: the
classification tests startsWith("watch"), so those kinds looked like iOS
surfaces, mapFamily skipped the unrecognised names, and familiesSwift fell
through to its systemSmall/Medium/Large default. A complication became three
home-screen widgets rather than being withheld.
Normalised in one place, because four decisions read these names -- the Swift
family list, the watch-only classification, whether a kind has any watch family,
and the corner complication's platform guard -- and three of them tested the
prefix directly.
accessoryRectangular is deliberately not folded into watchRectangular: it is the
spelling of the portable lockscreen family, which the iOS target does host, and
watchRectangular already emits the same Swift family with the renderer picking
the more specific layout.
Plugin suite 466/0 with a case covering both spellings and pinning that
accessoryRectangular keeps its iOS surface; daemon 205/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
My previous commit folded accessoryCircular and accessoryInline into the portable watch names. That is wrong in the mirror-image way: both are iPhone LOCK-SCREEN families as well as watch ones, and CN1DescriptorWidget.swift already renders all three accessory families on iOS -- they sit under `if #available(iOS 16.0, watchOS 9.0)`, and only .accessoryCorner is inside `#if os(watchOS)`. Classifying them as watch-only withheld a lock-screen widget the manifest had asked for, in exchange for fixing the case where one was emitted as three home-screen widgets. accessoryRectangular had said so all along: it maps to the portable lockscreen, not to watchRectangular. So the two namings are NOT interchangeable here -- the portable watch* names mean "complication only", the WidgetKit spellings mean the WidgetKit family, which on iOS is the lock screen. Only accessoryCorner normalises to a watch name, because only it has no iOS surface. Plugin 466/0, daemon 205/0, with the test rewritten to pin both directions: accessory spellings keep their iOS surface and emit their own families rather than the home-screen default, and accessoryCorner stays watch-only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0cfc65f23f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ansfer claims removeData deleted the value and then wrote the tombstone, so a writeValue that failed -- a full disk, a directory gone read-only -- left the value gone and nothing saying it ever existed. A peer offline for that call came back to no value and no removal and kept its stale copy for good. The tombstone is written first now and nothing is destroyed until it is on disk; if the value then refuses to delete, the tombstone is the newer record and the existing reconciliation drops the value on this or any later scan. Writing it first was previously avoided because both records can briefly exist, and that is no longer a durable state. The transfer deletion had the same shape one level down: a refused delete kept the file, and the only thing stopping redelivery was the in-memory seen-marker. primeSeenData deliberately restores nothing, so after a restart the surviving one-shot file looked new and was delivered again. A ".consumed" marker is written beside it instead, honoured by the scan -- which retries the delete and drops both once it succeeds -- and excluded from delivery, getDataPaths and gone-detection like the other bookkeeping files. Verified: SpotBugs 0, Checkstyle 0, 4754 core unit tests, TombCheck 38/38 including a read-only directory proving the removal destroys nothing when it cannot record itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The startup replay and onDataChanged both inspect the same item on a cold start -- that race is expected, not exceptional -- and each inspection started its own retry chain. Every chain then reopens the same asset on the shared transfer timer for up to the hard cap, each further callback added another, and the work queued behind them (replay, cleanup) waited longer for it. The chain is claimed per URI. The claim IS the retryStarts entry, which every exit already clears -- decode success, our own echo, the item having disappeared, the hard-cap give-up -- so a chain that ends releases the URI for a later one with no new bookkeeping to leak. Claiming it at the first attempt also means the window the chain is bounded by starts when the caller actually first tried, rather than when it first reached the hard-cap check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e8e24f36d4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 369a62e687
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The watch target compiled the PHONE's ParparVM translation and had the phone stub's main defined away with -Dmain=cn1_watch_phone_main_unused. So watchMain was never really the watch's entry point, and the watch binary carried everything the phone reaches rather than what the watch does. The watch now gets a stub of its own, written into the same source folder so one javac pass compiles both, and a second translator pass runs over the SAME classes from that root -- which is what produces the tree-shaking, since the translator walks out from the entry point it is given. The pass is skipped when watchMain names the phone's own main class, because then the two roots reach the same code and it would be the same tree twice. The staged tree is copied next to the project and the watch target compiles it instead of the app target's sources; the -Dmain neutraliser now applies only in the shared case, where it is still needed to stop one binary defining main twice; and CN1WatchBootstrap.m enters the watch stub, whose symbol is the only one present in a watch-rooted binary. applyXcodeSettings generated the xcodeproj script AND ran it, which is why none of its decisions were tested. Split: buildXcodeScript returns the script and the tests pin both modes -- own translation compiles the staged sources with no -Dmain, shared keeps one translation with it -- plus the bootstrap target. Plugin suite 469/0. NOT yet verified end to end: the generated watch target has to compile and link, which needs the sample build and the watch golden suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The hot reload builds a new app instance while this class -- static, in a class loader the reload does not replace -- keeps the old one's listeners. Every later event then reached both instances: side effects ran twice and callbacks landed on UI objects belonging to a screen that no longer exists. The dead instance also stayed strongly reachable through those lists, so it never collected. resetForReload clears the listeners and the state owed to them -- the parked deliveries, the dropped-path and dropped-removal records, the replay requests, the drain guard and the port's recovery handler. Handing those to the replacement would deliver, as new, what the previous instance had already been told. Executor's reload hook calls it reflectively so the JavaSE port keeps building against a core without the wearable API. Only the simulator needs this; on a device the process dies instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…fact The summary table said "Companion or standalone" for Android while the section above it, and AndroidGradleBuilder, say the opposite: leaving codename1.watchStandalone unset produces only the phone APK. A reader trusting the summary would pick companion mode and wait for a wearable artifact that is never built. This is a correction to the table, NOT a substitute for the feature. Emitting a second Wear APK in companion mode is still outstanding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 715c28b547
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…d the old build The cast-semantics gate is right and this was a real iOS bug, not a style point: AndroidWearableSupport cast the reflectively created bridge inside a try whose handler catches Throwable. ParparVM does not throw ClassCastException, so on iOS the handler would never run and the wrong object would simply be used. Tested with instanceof and branched instead. resetForReload cleared the port's DroppedDeliveryHandler, which is not app state: the bridge registers it once from its constructor and the simulator caches that bridge across reloads, so clearing it removed the recovery path for good -- after a reload with more than MAX_PENDING updates and no listener, evicted paths stayed marked delivered and could never be re-offered. It is kept now; the listeners and the state owed to them are still cleared. The guide still described the build that the second translation replaced -- telling readers the watch starts the phone lifecycle and that CN.isWatch() is what selects the watch UI. That would send someone's watch screens into the wrong class. It now says watchMain is the entry point on both platforms, and keeps the CN.isWatch() advice only for the one case where it still applies: naming the phone's own main class, which keeps a single translation. Merged master (the branch was 9 behind, which is where the cast gate came from). Verified: cast gate clean of wearable entries, Vale 0, snippets 659, SpotBugs 0, PMD 0, Checkstyle 0, core 4754/0, plugin 492/0, harness 30/30. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c8a1e5fd56
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Nine defects in the second-translation work, every one found by building the watch target and none visible to a unit test -- the tests assert what the generator writes, these live in what the toolchain does with it. - The generated watch stub used NativeLookup without importing com.codename1.system. This is what CI is failing on right now, and it only appears in a project that HAS a native interface. - The translator refuses a classpath carrying two mains. Both stubs compiled into one classes tree, so BOTH passes broke; each stub is now moved into a directory of its own and each pass gets exactly one. - The staged tree was addressed as "watch-src" rather than "<Main>-src/watch-src". Most translated files share a basename with the phone's, so Xcode resolved them against the PHONE tree and quietly compiled those -- only the watch-only names failed, which made a wholly mis-wired target look like 16 missing files. - A pch's quoted includes resolve against its own directory before any search path, so the phone's pch gave every watch source a class index that never declares the watch stub. The watch tree gets its own copy. - GCC_PREFIX_HEADER and HEADER_SEARCH_PATHS were set and then overwritten further down the same generated script; they now live at the authoritative assignment, and the header paths use the SDK-conditional keys that replace the plain one. - Files absent from the watch translation are skipped, not shared: they belong to the phone's program, and sharing them pulled in class ids the watch index never declares. - The file set comes from the watch translation, not the app target: the phone's translation shakes out what the phone never reaches, which includes the watch lifecycle class itself, and the link failed without it. - The stub's C main is neutralised in BOTH modes. The watch app is SwiftUI-rooted, so @main is the entry and the translator's C main is a duplicate symbol; only which stub carries the flag differs. Verified: the watch target now COMPILES AND LINKS -- HelloCodenameOneWatch.app is produced from a watchMain-rooted translation. Plugin suite 492/0. The screenshot half of the golden suite is still running locally. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ebba9d5fe2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…idge A cn1lib Objective-C source can need -fobjc-arc while the port builds with ARC off, and the watch target forces CLANG_ENABLE_OBJC_ARC=NO. The shared branch copies bf.settings for that reason; taking the file list from the watch tree left no bf to copy from, so those sources would have compiled under the wrong memory model. The app target's per-file flags are now indexed by basename and applied to the matching watch source. JavaSEPort.getWearableBridge was a lazy singleton with side effects behind an unsynchronized null check: two threads registering listeners at once each built a bridge, each started a data watcher and a rendezvous thread, and each replaced the shared dropped-delivery handler -- so the losing instance stayed alive through its threads, the same peer file produced duplicate callbacks, and its evicted deliveries recovered through the wrong instance. The accessor is synchronized. Plugin suite 492/0; JavaSE port compiles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The phone stub calls SVGRegistry.installGlobal(), and that static reference is the only thing holding the generated SVG classes in the reachable graph -- they are reached reflectively otherwise. The watch stub omitted it, so the watch translation shook out all 24 of them. Correct tree-shaking, wrong result: SVG and Lottie fell back to a placeholder render on the watch while working on the phone, which is what the three failing watch goldens were. The watch stub now emits the same install. Plugin suite 492/0; the golden rerun that confirms the three tests recover has not completed yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 48a643c0eb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| com.codename1.ui.Component focused = f.getFocused(); | ||
| com.codename1.ui.Container c = focused == null ? null : focused.getParent(); | ||
| while (c != null && !c.isScrollableY()) { | ||
| c = c.getParent(); |
There was a problem hiding this comment.
Include the focused component in rotary targeting
When the focused control is itself vertically scrollable but has no scrollable parent—for example, a small focused TextArea or List inside a non-scrollable content pane—this starts the search at focused.getParent() and falls back to the content pane's center. The synthetic wheel gesture can therefore land on an unrelated component instead of the focused control, so rotating the Wear OS crown does not scroll it. Test focused.isScrollableY() before walking its parents and use the focused component as the anchor when it qualifies.
Useful? React with 👍 / 👎.
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