Complications on the watch, and a Wear artifact beside the phone APK - #5583
Complications on the watch, and a Wear artifact beside the phone APK#5583shai-almog wants to merge 64 commits into
Conversation
Three preliminaries for generating watch complications, each of which stands on
its own.
A result entry's role suffix now survives into the copied artifact name. Every
entry used to land on target/<finalName><extension>, keyed on the extension
alone, so a build returning two artifacts of the same kind -- a phone APK and a
companion Wear APK beside it -- collapsed both onto one path and the last one
written won. That corrupts the primary artifact, not merely the secondary one,
and it does it silently.
The family classification moves into SurfaceKindFamilies, which also reads the
portable "families" key with "iosFamilies" as its legacy spelling. The Android
builder has to tell a home-screen kind from a complication kind and is not going
to parse a key with "ios" in its name. Delegation rather than a second copy,
because the rule is subtle enough that three call sites once implemented it as
startsWith("watch") and all three got accessoryCircular wrong.
The shared surfaces Swift now compiles for watchOS. Four WidgetKit system
families are @available(watchOS, unavailable) -- unnameable, not merely absent --
and UIColor.systemBackground, UIColor(dynamicProvider:) and
UIGraphicsImageRenderer are all API_UNAVAILABLE(watchos); the file named all of
them unconditionally. The substitutes are the right answers rather than
degradations: a watch face composites over black and has no light appearance, so
the background role is black and a light/dark pair resolves to its dark half.
Images downsample through ImageIO, which decodes at the target size so the
full-size bitmap is never resident, at a quarter of the phone's ceiling.
Verified by typechecking the sources against both the watchOS and iOS SDKs.
SurfacesSwiftWatchPortabilityTest is the half that also runs on a CI leg with no
Xcode; it was confirmed to fail when the systemBackground guard is removed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
setWatchTarget(true) builds a second WidgetKit extension for the watch app, where the first one is built for the phone. They share every Swift source that can be shared and differ in which families they may name. The dead watchTarget parameter that has been threaded through familiesSwift, watchOnlyFamiliesSwift and mapFamily since the families were introduced finally has a caller -- but it was not correct as written. With watchTarget true it still mapped small/medium/large onto .systemSmall and friends, which are @available(watchOS, unavailable): unnameable there, not merely absent, so the watch bundle would have failed to compile rather than showing a widget nobody wanted. Those four and lockscreen now resolve to no family in a watch target, and the home-screen fallback for a kind with no usable family is suppressed there too. In the other direction accessoryCorner needs no os(watchOS) guard inside a target whose SUPPORTED_PLATFORMS is watchOS alone. The rest follows the same split: the two ActivityKit sources are never shipped to the watch and the live activity never joins its bundle, the widget-count limit counts the kinds this flavour actually hosts, and the build settings describe a watch target -- WATCHOS_DEPLOYMENT_TARGET, SDKROOT, device family 4, arm64_32 -- with ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES off, because the watch app already embeds the runtime for everything nested inside it. The floor is watchOS 10.0, not WidgetKit's own 9.0: every generated widget applies containerBackground(for:), which is watchOS 10, so a lower target does not lose the background -- it fails the build. A lower one is refused with that reason. Verified by generating a watch extension from a mixed manifest and typechecking the whole thing against the watchOS 26.2 SDK, and by generating the iOS one from the same manifest and typechecking it against the iOS SDK. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CN1_USE_WIDGETS was undone for watchOS alongside tvOS, so every surfaces native compiled to its unsupported stub and Surfaces.publish() from a watch app was a hard no-op that reported success. tvOS keeps the undef -- it has no WidgetKit at all -- but the watch does not: a complication is a WidgetKit widget in an accessory family, hosted by the watch app's own extension and fed from the watch's own App Group container. That container is the counter-intuitive part and is now written down where the guard used to be. The identifier is the same string as the phone's; the container behind it is a separate directory on the watch. So the watch has to publish for itself rather than reading what the phone wrote, which is why restoring these natives is what makes a complication possible at all. cn1SurfacesMinOSSupported compares the plist floor against the OS actually running, so its fallback has to be per-platform too. The iOS default of 16.1 compared against a watchOS version is never met, and every watch would have reported no widget support whatever the plist said. The four ActivityKit natives now answer for the watch explicitly instead of relying on the Swift bridge having compiled its bodies out. They keep their symbols -- the Java methods are reachable from shared code, so removing them would fail the watch link rather than tree-shake -- and the guard uses #else rather than an early return so the watch slice compiles no unreachable statement. Verified by compiling the surfaces native block against both the watchOS and iOS SDKs, and by typechecking the app-target Swift glue against the watchOS SDK. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The watch app now gets a CN1WatchWidgets target embedded in its own PlugIns folder, and that single choice is what makes both distributions need no separate handling: the companion case already copies the finished watch app into the phone app with the .appex inside it, and the platform filter keeping the watch tree out of the Mac Catalyst slice covers the extension for free; the standalone case ships the watch app as the product. There is no branch for either. The target type is :app_extension. :watch2_extension is the legacy paired WatchKit app extension -- the same trap as :application versus :watch2_app for the app target -- while a WidgetKit extension is a plain app extension wherever Apple ships it. Generating it belongs here rather than beside the iOS extension because the watch app target does not exist yet when the schemes ruby runs. So it is written immediately before the watch builder's own xcodeproj script and wired by that. Two things the watch target could not previously reach. Its own translation carries no CN1SurfaceBridge -- only the phone's -src does -- so the natives found no bridge through NSClassFromString and answered unsupported; the bridge and its config constant are now added to the watch target by name, de-duped so the shared-translation case is unaffected. And the entitlements file was gated on HealthKit alone, which was the only capability the watch did not inherit from the phone until now; publishing complications adds an App Group. Both are opt-in and neither implies the other, because granting one that is unused is refused by entitlement validation rather than ignored. parseSurfacesManifest no longer returns early when nothing reaches iOS. A manifest whose every kind is a complication produces no iOS extension and no phone app-group entitlement, and must still produce a watch one -- that is the case the watch families exist for. The build now says which of the two happened instead of reporting that watch kinds appear nowhere. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The cn1ss sample already declares codename1.watchMain and a surfaces kind, so adding two watch families to that kind is enough to make the build-ios-watch job generate and compile CN1WatchWidgets for watchOS. That is the only automated check that the shared surfaces Swift stays portable to a platform with no UIGraphicsImageRenderer, no UIColor dynamic provider and no system widget families -- every one of which was a real break. Keeping small and medium on the same kind preserves the existing iOS coverage, and the manifest switches to the portable "families" spelling so that path is exercised too. The script then asserts the .appex is actually in the watch app's PlugIns folder, declares the WidgetKit extension point and carries an app group. The screenshot comparison cannot see any of that, and simctl cannot exercise a complication at all -- there is no API to place one on a watch face -- so the wiring needs checking directly or it is not checked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tapping a complication launches the watch app with the widgetURL. There is no UIApplicationDelegate on watchOS, so the SwiftUI scene's onOpenURL is the only place that URL can be caught -- and nothing was catching it, so the tap opened the app and the action went nowhere. The cn1surface:// decode moves out of CodenameOne_GLAppDelegate.m, which is entirely #if !TARGET_OS_WATCH, into IOSNative.m, which compiles on both. The delegate now calls it rather than carrying its own copy, so the two platforms cannot drift on what a surface action means. Surfaces.dispatchAction already queues until the app registers its handler, which is what makes this work at all: a complication tap is almost always a cold start. The C entry point is declared in the generated watch bridging header, because a plain C function is invisible to Swift otherwise, and only when the app actually publishes complications -- an app without them keeps the scene and the header it had. Verified by compiling the surfaces native block for watchOS and iOS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A developer designing a complication previously had nothing to look at. simctl cannot place one on a watch face, so short of building to a device and adding it by hand there was no way to see the layout at all -- while every phone family had a preview from the start. The Widgets window now lists the four watch families at the accessory families' own point sizes, and clips the round ones the way a face does. That clip is the point rather than decoration: a watch face shows nothing a circular complication draws into its corners, so previewing it square would make a design look fine that loses content on the device. layoutForSize gains the two substitutions the platform renderers already make, so the preview and the device agree on what gets shown. watchCorner borrows the circular layout -- a corner complication is round, and Wear OS has no corner slot at all -- and watchRectangular borrows lockscreen, which is the same WidgetKit family on Apple. Both are closer to what the developer designed than "default", which may well be a rectangular phone widget. What this previews is the node tree at the right size and shape, not the per-platform lowering: Wear OS reduces a complication to typed ComplicationData, so a layout that looks right here can still lose detail on a face. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An App Group container is device-local: the watch resolves the same identifier to a directory of its own, which nothing on the phone can write. So a phone-side Surfaces.publish() was invisible to a complication however well everything else was wired, and the developer's only recourse was to hand-roll the transfer. IOSSurfaceBridge now forwards the descriptor after the local write has already succeeded, so nothing here can leave the phone's own widget wrong. Which kinds are worth sending is decided at build time and written into the plist as CN1SurfacesWatchKinds, so publishing a phone-only kind costs one dictionary lookup. The delivery ladder is the interesting part. transferCurrentComplicationUserInfo is the only WCSession API that wakes the watch app in the background to refresh a complication, and it is budgeted at roughly fifty a day. Spending one when the user has placed no complication wastes what the app will want later, so both that case and an exhausted budget fall back to transferUserInfo -- queued, unbudgeted, and applied whenever the watch app next runs. That is materially weaker, which is why it is the fallback rather than the default. Over the 48KB property-list cap the imagery is shed first, on the grounds that a complication rendering its numbers with a missing glyph beats one that never updates; over the cap even then, it gives up and says so. Imagery travels in the same dictionary rather than through transferFile, which is a separate unordered queue with no atomicity against the descriptor -- a complication could render against art that had not landed, which is worse than a gap. Applying it on the watch is deliberately headless: a file write and a WidgetKit poke, touching no Java. The background wake exists to refresh a complication, and starting the whole application to do a file write would bring a UI forward nobody asked for. Reserved keys are routed before anything app-visible, the same way acknowledgement traffic already is, so the app never sees a message it did not send. publishRemote grows an images overload, which also fixes a latent gap: it discarded the side-map unconditionally, so a server-pushed descriptor referencing art has never rendered it. Verified by compiling the surfaces natives for watchOS and iOS, and by check-native-signatures against a rebuilt port -- which reports 0 fatal, the new byte[][] mangling included. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Android surfaces codegen never looked at families, so a kind declaring only watch complications quietly became a home-screen widget -- a surface the manifest never asked for. It now splits: a kind with a phone family still gets its AppWidgetProvider, and a watch-bearing kind is collected for the Wear services instead. iOS has always refused the same thing, so this is the two platforms agreeing rather than a new rule. That silence was the real problem, and the "companion Wear APK is not produced yet" log is replaced by diagnostics that name what actually happens: which kinds become complications, that watchCorner renders as circular because Wear OS has no corner slot, that watchRectangular earns a Tile as well, and -- when the build produces no Wear product at all -- that the declaration reaches no device and what to set to change that. watchModuleName answers "which module is the watch product" once, because everything downstream is the same code and differs only in the destination: "app" for a standalone build where the single APK is the watch app, "wear" for a companion build, null for a project that never asked for a watch. complicationTypes is the mapping WidgetSize already documents, made executable. It decides whether a complication can be placed in a given slot at all -- a watch face asks for one specific type and gets nothing if the source does not offer it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Complications and Tiles now exist on Android, generated per watch-bearing kind, along with the companion Wear artifact that has never been produced. The split is deliberate: everything that does not touch androidx.wear lives in the port and is compiled by this repository, and only the two thin androidx-facing services ship as build-time resources. An app publishing no complication must not carry those libraries, but keeping the reader in the port is what lets CI catch a break in it -- and WearGlueCompilesTest compiles the injected services against the REAL CN1WatchSurface plus a stub tree, so a service that drifts from the reader's contract fails here rather than in a customer's Gradle build naming a file they never wrote. A complication is not a small widget, and the code says so. A watch face asks for one typed value and composes it into its own design, so the node tree is flattened and mined for content rather than rendered; padding, alignment and colour are the face's business. What is dropped is logged once per render, so a developer whose careful layout arrives as one number learns that is by design. A Tile really does render the tree, and two things come out better there than on a phone widget: circular progress renders natively where RemoteViews degrades to a linear bar, and per-node taps work where a small iOS widget honours only the root. The honest limitation is time -- a countdown is frozen and refreshed from the timeline, because ProtoLayout's dynamic expressions are version-sensitive and a frozen value that is always correct beats a ticking one that works on some watches. The companion module shares the app module's source, resource and asset dirs rather than copying them, which would roughly double disk and dex time on a cloud builder for a tree identical apart from one class. Both modules declare the same namespace -- required, not merely convenient, because the shared sources refer to R unqualified from the app's package. That AGP permits it was verified with a throwaway two-module project before this was written. The mirror lives in the port for a reason that is the opposite of the iOS one and points the same way: Executor.scanClassesForPermissions reads the app's own classes and not the core, so a core-level reference to com.codename1.wearable would fail to turn the Data Layer glue on and the mirror would silently do nothing. Reserved paths are routed before anything app-visible and without waking the app, matching how acknowledgement traffic is already handled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five statements in the wearables chapter said the opposite of the truth -- that Wear OS has no companion form, that no complication target is generated on either platform, that a companion Android build hands back one artifact. They are deleted rather than reworded, and the summary table with them. What replaces them is mostly a warning, because the surprising part of this feature is not that it works but how much a watch face discards. A complication is not a small widget: the face asks for one typed value and composes it into its own design, so the node tree is mined for content rather than rendered, and on Wear OS a kind supplies at most two text nodes and one image. That has a section of its own, with the per-node mapping in the surfaces chapter, because someone reading only the "declare a family" paragraph would design something the face will not show. Two places the Tile beats the phone widget are written down too -- native circular progress and per-node taps -- along with the one place it loses, a frozen countdown, and why a frozen value that is always right beats a ticking one that works on some watches. The mirror gets its own section, leading with the fact that makes it necessary: a watch app has its own storage, so a phone-side publish reaches a complication only because the framework carries it. Its budgets, caps and degradation are stated rather than left to be discovered, and so is the cost -- declaring a watch family on Android puts play-services-wearable in the phone APK. Both blog posts still documented the retired android.wear hint; they now say what drives the build, with a note that the old hint keeps working. Vale, LanguageTool, the paragraph capitalization check and asciidoctor all report zero across the whole guide. 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: 822e641e8c
ℹ️ 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".
Blog prose gate✅ No net-new prose findings introduced by this PR. |
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
|
Compared 149 screenshots: 149 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 149 screenshots: 149 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 149 screenshots: 149 matched. |
|
Compared 149 screenshots: 149 matched. |
|
Compared 149 screenshots: 149 matched. Benchmark ResultsDetailed Performance Metrics
|
Five review findings, four of which were the same shape: code that compiles, never throws, and renders nothing. **Containers were read under the wrong key.** `SurfaceContainer` serializes its children as `ch`; both the complication reader and the Tile renderer looked for `c`. Every row, column and box therefore looked empty, so a complication mined a layout with no text, no progress and no imagery in it and a Tile rendered nothing. That is indistinguishable from an app that published nothing, which is why it survived a green build. **Dynamic nodes carry no text to interpolate.** A `dyn` node serializes a style plus a date or a dateKey, so asking it for `text` resolved to an empty string and every countdown, clock and relative date vanished. They now go through the core's own formatter -- made public rather than copied -- so a countdown reads the same on a watch face as in the simulator preview and on a home screen. Both are now pinned by SurfaceWatchWireFormatTest, which asserts the field names against the serializer itself. The readers live in the Android port and cannot be unit tested from there, but the wire format can be, and that is what makes this kind of drift visible instead of silent. **A Tile tap dropped its action.** A Clickable's id is ProtoLayout interaction metadata and never reaches the started activity, so the trampoline -- which dispatches only when EXTRA_ACTION_ID is present -- opened the app and discarded the action id, source and parameters. The extras are attached explicitly now, the same three a widget tap sends. **A companion build raised the phone's minSdk.** Declaring a watch family pushed the shared floor to 26 before the phone module's Gradle file was written, so a phone APK that had supported API 21-25 became uninstallable on the devices it already served. The floor now rises only for a standalone build, where the app module IS the watch product; the wear module sets its own. **Mirrored artwork never triggered a redraw.** A file transfer is asynchronous and unordered against the descriptor, so art routinely lands after the timeline that references it -- and only the descriptor asked for a refresh. The first render showed a gap and nothing asked again until the next publish. Also fixes the two SpotBugs findings that failed CI: mkdirs() return values were ignored. The naive check is wrong here, since mkdirs() answers false both when the directory could not be created and when it already exists -- which is the common case -- so existence afterwards is what the callers test. SpotBugs is now zero across android, ios, codenameone-maven-plugin and core-unittests; 917 plugin and 5202 core tests pass. 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: ad8aed6721
ℹ️ 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".
|
Compared 12 screenshots: 12 matched. |
Four more findings from the second review round, three of which meant a generated artifact could not work at all. **The watch slice lost the surfaces define.** CN1_USE_WIDGETS was still flipped only for surfacesExtensionEnabled, so a manifest declaring nothing but complications compiled the watch slice without it -- and since the WatchConnectivity delegate calls cn1_watch_apply_mirrored_surface, which that define guards, the watch slice failed to LINK rather than merely doing nothing. That edit was written once before and lost: a patch script asserted on a later anchor and never wrote the file. The same failure ate the daemon's appendWidgetExtension call site. Both are now verified present rather than assumed. **The Wear module declared no Data Layer listener.** Its manifest is selected outright by the module's sourceSets rather than merged with the phone's, so nothing the phone declares reaches it -- and the watch needs this one more than the phone does, being the half that RECEIVES a mirrored complication. Play services had nothing to bind in the watch APK, so every mirrored descriptor was dropped and complications stayed at whatever the watch had published for itself. **Tile padding was read as an object.** SurfaceNode serializes it as the array [top, right, bottom, left], which is what the RemoteViews renderer reads, so asking for an object returned null for every valid descriptor and all declared padding was silently discarded. **A Tile's vector resources were keyed by object identity.** The layout request and the resources request are separate calls that each re-read and re-parse the timeline, so the two ids never matched: the layout referenced a resource the returned map did not contain and every vector rendered as a missing image. The id now comes from the node's serialized content, which is equal across parses -- and two identical vectors sharing one resource is correct, since they draw the same thing. SurfaceWatchWireFormatTest grows the padding and vector cases, so the class of bug that produced three of these four -- reading a field the serializer does not write -- is pinned against the serializer rather than found by review. SpotBugs zero across android, ios, codenameone-maven-plugin and core-unittests; 917 plugin and 5204 core tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cloudflare Preview
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: badd9cd1fa
ℹ️ 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 Android CI failure and four more review findings, all of them the same mistake in different places: the phone and the watch are separate products in a companion build, and things scoped to one kept reaching the other. **The androidx.wear libraries reached the phone module.** They went into the shared gradleDependencies hint, which in a companion build feeds both modules -- and they declare minSdk 26 while the phone keeps its own floor. A phone app on API 24 therefore stopped building the moment a watch family was declared, failing its manifest merge against libraries it never uses. They now go into the wear module's own dependency block, and only a standalone build -- where that single module IS the watch -- puts them in the shared one. **The Data Layer glue was decided before the kinds were parsed**, so watchSurfaceKinds was always empty at that point. An app that publishes complications and never writes a line of com.codename1.wearable got no glue, no dependency and an empty listener declaration -- the mirror had no transport at either end. The block moves after the surfaces parse. **A watch-only manifest could not mirror at all.** The phone was deliberately left without the App Group entitlement, so its container did not resolve, areWidgetsSupported() answered false, and Surfaces.publish() returned before the bridge -- taking the mirror with it. The one manifest this feature exists for was the one that could not update its own complications. The group is genuinely part of the plumbing on both bundles and is now entitled on both. **The Wear manifest declared no INTERNET permission.** It receives only the scanned permissions, not the base ones, and is selected outright rather than merged -- so a watchMain making an ordinary Codename One network request failed while the same code worked on the phone. **The watch bundle declared no cn1surface URL scheme.** A complication supplies a cn1surface:// widgetURL and the generated scene waits for it in onOpenURL, but the watch is a separate bundle inheriting none of the phone's URL types. watchOS had nothing to route the tap to, so the whole tap-dispatch path was inert. Two new tests pin the module boundary: that a companion phone module carries no androidx.wear dependency, and that the watch bundle declares the scheme when it hosts a complication and does not otherwise. SpotBugs zero across android, ios and codenameone-maven-plugin; 920 plugin tests pass. 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: e4952d5c96
ℹ️ 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 CI failure and two more review findings, all from the same root: the Wear
module's build.gradle is derived from the phone's by textual substitution, and a
generated Gradle file only fails when Gradle evaluates it -- twenty minutes after
the mistake, in a job that names none of it.
**The androidx.wear dependency landed in the buildscript block.** The anchor was
"dependencies {", which matches the indented buildscript block FIRST -- and
String.replace hits every occurrence -- so an implementation() call went into
buildscript's dependency handler, where the method does not exist. The whole
:wear project failed to evaluate. Anchored on "\ndependencies {" now, which only
the project block matches.
**The Wear module looked for a keystore beside itself.** Gradle resolves
file("keyStore") relative to the project it appears in, and the certificate is
written only to the app module -- so a companion release build failed to
CONFIGURE, taking the phone artifact with it. Not the watch half degrading: the
whole build not starting.
**The generated services were written into the phone's source root.** The wear
module shares that directory, so the phone compiled androidx.wear imports it has
no libraries for -- the exact mirror of the dependency-scoping fix that preceded
it. They now go to the wear module's own root. The kind-list resource stays on
the phone deliberately, because the mirror reads it THERE to decide what to send.
**The Tile service was copied whether or not a Tile was declared**, while its
dependencies were added only for a rectangular family. Gradle compiles every
source in the tree, so a complication-only build failed on unresolved imports.
The derivation is now a static function, and WearModuleGradleTest pins each
substitution against a build.gradle shaped like the real one -- the dependency
landing in the project block and not buildscript's, the keystore reachable, the
libraries shared, the phone's floor untouched. It calls the real builder rather
than reproducing it, and was confirmed to fail on the exact anchor bug that broke
CI. Two more tests cover where the services are generated and which are copied.
SpotBugs zero across android, ios and codenameone-maven-plugin; 928 plugin tests
pass.
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: d2c4b52438
ℹ️ 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".
…ying wolf Three fixes, two of them the same bug in different clothes. xmlize escaped a supplementary character one UTF-16 half at a time, so an emoji in a display name became �� -- a pair of surrogate code points, which are not legal XML character references. The manifest carrying one did not show the wrong glyph, it failed to parse. It now walks code points; every BMP character escapes byte for byte as before, so nothing that was already valid changes. This is repo-wide rather than watch-specific, and the phone manifest had it too. The complication's short-text cut had the same shape: substring(0, 7) on a UTF-16 index can land between the halves of one character and leave a lone high surrogate, which PlainComplicationText replaces or rejects. It now counts code points, which is also what the limit MEANS -- Wear's guidance is about characters a face can show. And registerWidgetKind treated a missing CN1Widget_ receiver as proof the kind was absent from surfaces.json, when a kind declaring only watch families has no receiver ON PURPOSE. Every correct watch-only registration printed a prominent error telling the developer to add a kind that was already there. The build-time watch-kind list settles it, and a kind in neither still reports. 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: 06e75e49b0
ℹ️ 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 cn1surface deep-link decode moved out of the app delegate so the watch could reach it -- watchOS has no UIApplicationDelegate, and a complication tap was launching the watch app and then dropping the action. What did not move with it was the app delegate's include of the generated IOSSurfaceCallbacks header, so the call in IOSNative.m had no declaration and C invented one. The Catalyst leg builds with -Werror=implicit-function-declaration and failed on it, which is how this surfaced; it had been on the branch since the decode was extracted, and was mostly invisible because a following push cancels that job before it finishes. The diagnostic is not the real danger though: an invented prototype passes three JAVA_OBJECTs and a thread state through whatever registers C guesses, which links and then misbehaves. The app delegate has included this same header since before the branch, so the header is generated for every build and the include is safe. 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: 289bd819b4
ℹ️ 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".
Four fixes. The Tile resource response had no aggregate limit. The renderer's bitmap budget is reset for every bitmap() call, so each image is measured alone and a handful of individually acceptable ones still add up past the one Binder transaction the whole response has to fit. Over that ceiling the host does not render a partial tile, it fails the request -- so dropping the images that do not fit is strictly better than sending all of them: a tile with a gap beats no tile. The drop is logged with what to publish instead. A spacer went to the host without passing through sized(), so it was the one node whose declared setSize Tiles ignored. min is the spacer's own length, not a replacement for the shared contract, and now both apply. A complication timeline gave up entirely when the CURRENT entry could not render the requested type, even when a later one could. Nothing would ever ask again -- a complication is handed the whole timeline once and UPDATE_PERIOD_SECONDS is 0 by design -- so a RANGED_VALUE slot whose progress node appears in the next entry stayed empty for good. No-data now covers the gap until the first renderable entry, which is what the timeline's default is for; a type nothing can render still declines. And the guide claimed the Wear version-code offset is +1. It has been 100,000,000 since the review that pointed out +1 is consumed by the phone's own next release, and a developer planning explicit Play codes was being handed the collision-prone rule. 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: b872e76108
ℹ️ 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".
|
Compared 148 screenshots: 148 matched. Benchmark Results
Detailed Performance Metrics
|
A URL scheme is a GLOBAL registration, and every Codename One app claimed the bare cn1surface. On the watch that is the whole routing story -- a complication tap carries a widgetURL and nothing else decides where it goes -- so two such apps on one watch were two claims on one name and a tap could open the other app. Any other app can also claim a known scheme and hand us whatever src and id it likes. The scheme is now cn1surface.<bundle id>, computed in one place so the plist that registers it and the widget that generates it cannot drift, and pinned by a test. That test asserts the bare name is ABSENT from the watch plist as well, because re-adding it costs nothing at build time and hands the collision straight back. A scheme can never make a payload trusted; what this fixes is a tap landing in the wrong app, which happened with nobody being hostile. The watch registers the scheme built from the WATCH bundle id, not the phone's. The extension is built with setHostBundleId(<pkg>.watchkitapp) so that is what its widgetURL carries -- I had this wrong first, and the symptom would have been a plist that reads correctly and a complication that does nothing when touched. The phone keeps accepting the bare name too: it has always registered it and something may hold a link built with it. Separately, the Tile resource version was a 32-bit String.hashCode over user-controlled text, which collides on request -- "Aa" and "BB" hash equally. Two different snapshots could advertise one version, and then Wear reads changed artwork as unchanged and rebuild() matches the wrong entry and rasterizes against the wrong state. Neither corrects itself, because both read the collision as nothing having happened. It is a truncated SHA-256 now. 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: 9b1006a0aa
ℹ️ 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 injectivity fix two rounds ago renamed the generated provider of every kind whose id contains an underscore: delivery_status moved from CN1Widget_DeliveryStatus to CN1Widget_DeliveryStatus_8. Android remembers a pinned widget by its provider ComponentName, so on update the widget the user pinned named a receiver that no longer existed and the home screen dropped it. That is a real cost paid by users, silently, and it bought a collision that only two ids differing solely in underscore placement can cause. So the fold goes back to exactly what shipped, and the ambiguity is resolved at the level it belongs to -- the declared SET. The first kind claiming a folded name keeps it; a later kind that would collide takes the positional form, and the build says so, because that kind's provider is then not found under the name its id suggests. Every project that builds today gets byte-identical names. The runtime cannot recompute which kind won the plain name, since that is a property of the set. Rather than shipping the mapping and trusting two copies to agree -- the exact failure this branch already had once -- it tries both names the build can produce and takes the one that exists. An id without an underscore has one candidate, so the usual path is unchanged. Two more, both about showing something false rather than nothing: token() returned a value it had failed to persist, so a later tap read the preference, found nothing, generated a different token and rejected the app's own action; two nodes in one render could even carry different unusable ones. It returns null now and the caller leaves the action off, so the node is inert rather than broken-looking. And a timeline entry this complication type cannot render was skipped, which leaves nothing covering that interval -- and what shows then is the timeline's default, the CURRENT reading. A published timeline that moved on kept displaying the old value as though it were still current. It gets a no-data entry for its interval instead. 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: 825a05ea27
ℹ️ 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".
Last round's runtime lookup tried both names the build can produce and took the one that exists. That is wrong in exactly the case it was written for: CN1Widget_Status exists because "status" declared it, so "status_" probing the plain name first finds the OTHER kind's provider. Every publish, reload and installed-count for status_ would have gone to status, and the status_ widget would sit there stale -- a worse failure than the collision this all started from, because it silently crosses two kinds. Which kind holds the plain name is a property of the whole declared set, so a runtime holding one id genuinely cannot work it out. The build states it: a generated cn1_surface_kind_classes array, written from the same table that named the classes. That is data, not a second copy of an algorithm, so there is nothing for the two sides to disagree about -- the distinction that matters after this branch already shipped one drift bug. The watch module gets its own copy, because in a companion build its resources are not the phone's. An APK built before the map existed has no such resource and falls back to the plain fold, which is what that APK was built with. 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: 3486f1815a
ℹ️ 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 rest A mirrored image the watch failed to store was reported with confirmTransferDelivered(..., false), which returns without touching the in-memory claim claimTransfer had already made. That claim then suppressed every retry, and since the DataItem is unchanged nothing generates a fresh callback either -- so the artwork stayed missing until the process restarted. It relinquishes the claim now, which both drops it and goes back to read the item, exactly as the tracked-delivery path beside it does when the listener never got the payload. A vector's resource id was still a 32-bit String.hashCode over the node's JSON while the resource VERSION had moved to a digest. The id is the identity of a rendered vector and imageNodes keys its map by it, so two colliding nodes lose one mapping and both elements draw the second one's artwork. While there: the complication's PendingIntent request code had the same shape. A request code is an int by API so it cannot be widened, but it can avoid the collisions short human-chosen strings actually hit -- extras are not part of filterEquals, so two complications colliding there would share one PendingIntent. The three request codes that predate this branch are left alone. 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: d18b7120fc
ℹ️ 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".
… packages CN1SurfaceMirror.receive caught its own write failures and returned void, so the listener could not tell a stored descriptor from a lost one and moved on either way. A Data Layer item that has not changed produces no further callback, so nothing would ever offer that descriptor again: the watch went on showing content the phone had already replaced, until the next publish. It reports now, and a failed apply is re-attempted on the same worker the transfer retries use, with the payload still in hand so no round trip is needed. Six attempts over about twenty minutes, which outlasts the transient conditions this is for -- storage momentarily full, a directory briefly unwritable -- and then says it gave up. What it deliberately does not do is persist the payload to survive the process dying mid-outage, because that means writing to the storage that just refused a write. That case waits for the phone's next publish, and the comment says so. Separately, the generated Wear manifest carried the phone's permissions but not its <queries>. That manifest is selected outright rather than merged, so nothing else supplies them, and on API 30+ an undeclared query makes resolveActivity and queryIntentActivities return filtered results. The watch module compiles the SAME sources, so code that finds a package on the phone found nothing on the watch -- which reads as a broken feature rather than a missing declaration. The daemon already had this; the two copies had drifted. 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: 96c37b1e65
ℹ️ 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 comment beside this write argued the orphan was a fixed one-publish cost. It is not, and the reason is that the two halves of a publication reach the watch by different routes: the descriptor is a Data Layer item and COLLAPSES to the newest value when delivery is delayed, while every image is a transfer with its own sequence and none of them collapse. A watch that was away for ten publications therefore receives one descriptor and then ten publications' worth of artwork behind it, with no later descriptor promised to collect the nine that are stale. Refusing the write is still the wrong answer -- images are sent before the descriptor that names them precisely so a descriptor is never live against art that has not landed, and rejecting what the stored descriptor does not name rejects exactly the art the next one is waiting for. That was tried and reverted earlier in this branch. So the sweep runs after the write and settles it by AGE rather than by reference: art still waiting for its descriptor is seconds old, art from a superseded publish is not. It runs here rather than only in receive() because the case it exists for is the one where no further descriptor arrives. The ordinary publish path keeps a zero grace, where the descriptor is written first and the reference set is authoritative at once. 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: e52904fb8d
ℹ️ 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".
transferCurrentComplicationUserInfo: keeps only the MOST RECENT transfer: handing it a second payload while the first is still pending discards the first outright. Each payload carries one kind, so two watch-bearing kinds published before the first was delivered -- the ordinary case while the watch is away -- meant the earlier kind simply never arrived. The generated providers disable periodic updates by design, so nothing would have refreshed it either. They are queued per kind now and handed over one at a time, so the payload the session is holding is always one we have not yet been told was delivered. A repeat of the same kind replaces its own entry, which is right: only the newest timeline for a kind is worth sending. The queue retires a kind on failure as well as success, because a payload the watch refused will not start working by being kept at the head of the queue, and holding it there strands every kind behind it. Separately, the generated Wear manifest never declared the share receiver, though the wear module compiles that receiver and the lifecycle that handles what it delivers. ACTION_SEND and ACTION_SEND_MULTIPLE therefore could not resolve to the watch app at all, so a companion whose watch half is meant to accept shared content never appeared in the share sheet there. Both repos had this. 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: 6969b2169d
ℹ️ 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 queue added last round retired a kind by name when its transfer finished. Publishing the same kind again while that transfer was in flight replaces the queued value with the newer timeline -- and the completion then removed it, so the newest publication was never sent and the watch stayed on the older timeline for good, the generated provider having no periodic update to fall back on. That is the same failure the queue was built to prevent, moved one step along. The payload actually handed to WCSession is now remembered and compared by IDENTITY at completion: the entry is retired only if nothing replaced it, and a replacement stays queued. It also moves to the back of the queue rather than keeping its position, so a kind republished in a tight loop cannot hold the head and starve the kinds behind it. 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: f83fefd1da
ℹ️ 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".
…he mode Four, three of them holes in work from the last few rounds. The complication completion callback matched on kind alone. The fallback transferUserInfo: path queues its own transfers for the same kinds, so one of those finishing cleared a newer complication transfer that was still in flight -- and the next publish then displaced it before the watch saw it. The transfer object WCSession hands back is remembered and compared by identity, so a completion that is not ours is ignored. The mirrored descriptor path still collected with a zero grace. The descriptor and the images are independent Data Layer items and can arrive out of order across publications, so artwork for publication B can already be staged when A's descriptor is handled -- and deleting it there is permanent, because that transfer has been acknowledged and will not be resent. It uses the same age grace the image path got. A failed descriptor write scheduled a retry that applied its payload unconditionally, so a retry could overwrite a descriptor newer than itself or resurrect a surface whose tombstone had already been processed. Each reserved path now carries a generation, bumped by every mirror event, and a retry that has been overtaken discards itself -- checked when it fires as well as when it is scheduled, since the overtaking event usually lands while it is sitting on the timer. And the generated Wear launcher took Android's default standard launch mode rather than the project's resolved one. That became load-bearing when the custom intent filter started being carried across: an app link arriving while the watch app is running would start a second stub and lifecycle, where the same configuration delivers it to the running one on the phone. 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: b2f1557d17
ℹ️ 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".
…ss name Two bugs of my own making, both from the last two rounds. WCSession can invoke didFinishUserInfoTransfer: before transferCurrentComplicationUserInfo: has returned to the caller, so the recorded transfer slot is briefly nil while a transfer is genuinely in flight. Last round's exact-transfer match then discarded that completion as "not ours" -- and the sending thread went on to record an already-finished transfer, leaving the kind in flight for ever and stalling the queue: every later publication silently unsent. That is worse than the coalescing it replaced. A completion arriving in that window is now PARKED, and the sending thread retires it the moment it records. The park is cleared whenever a kind is retired, so it cannot accumulate. surfaceKindClassSuffixes assumed the positional form was free once the plain name was taken. It is not: an id with no underscore has no positions to record, so its disambiguated form IS the plain name it just lost. Declare "status_" before "status" and both wanted Status -- the failed taken.add was ignored and both kinds generated the same class, so one surface served the other's data, which is exactly what the disambiguation exists to prevent. It now takes whatever is actually free. The test declares the pair in that order and fails without the fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A kind declaring accessoryCircular for the iPhone lock screen AND watchRectangular for the watch was admitted to the watch extension by the watch family, and then advertised .accessoryCircular there too -- so it grew a circular complication the manifest never asked for. The system already says these are not watch families: SurfaceKindFamilies excludes them from hasWatchFamily, so a kind declaring only accessoryCircular produces no watch extension at all. Letting one INTO an extension that some other family opened was the same rule answering two ways depending on what else the kind happened to declare. They are filtered on the watch target now, exactly as lockscreen already was, and this is the mirror of the rule the iOS target has had all along -- watch* families never reach the phone bundle. Nothing is lost by refusing them: every accessory family the watch can show has a watch* name that maps to it, which is how a developer asks for it there. Both halves are pinned -- the mixed kind gets only its rectangular complication, and all three watch* names still reach their accessory families. Removing the filter fails the first test. 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: 4b7567e49d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… a name Three. SurfaceDynamicText was resolved to a string and handed over as plain text, so a countdown, stopwatch or relative date froze at the value from the last request -- and there is no later request, the generated provider setting no update period on purpose. The three time-RELATIVE styles are now given to the face as TimeDifferenceComplicationText, which it advances from its own clock with no wake-up. The port's own javadoc has said this is what a data source should do since it was written; this is that. time and date deliberately stay plain. They format the node's OWN timestamp -- a published moment, not the current one -- so a clock text would replace the value with whatever time it is now, a different number and a wrong one. Nothing about them moves, so nothing is lost. The long-text content description carried only the title. TalkBack reads that instead of the layout, so it announced a complication's label without the thing it actually says -- the order status, the message, the number the user wanted. It carries both now, while the visible title stays the first node. And publishRemote forwarded server-supplied image names unchanged. Those names arrive from OUTSIDE this process -- a server push or the watch mirror -- and every platform turns one into a file in the kind's directory; the iOS bridge composes dir + "/" + key + ".png" with no sanitizing of its own, so "../other_kind/hash" plants a blob under a different kind, where a later legitimate publish will not replace it because a content-hash name is assumed to already hold the right bytes. Names that are not plain blob names are dropped -- not the whole publish, since a missing image renders as a gap every renderer already tolerates. 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: 0f480b4eb3
ℹ️ 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".
| return; | ||
| } | ||
| b.publishWidgetTimeline(kindId, timelineJson, Collections.<String, byte[]>emptyMap()); | ||
| b.publishWidgetTimeline(kindId, timelineJson, safeImageNames(images)); |
There was a problem hiding this comment.
Validate the remote kind before using it as a path
When a push forwards a malformed kind such as ../activities/foo, the new image-bearing overload passes it unchanged to the bridge. On iOS, IOSSurfaceBridge.publishWidgetTimeline() concatenates kindId into container + "/cn1surfaces/" + kindId, so this new path can write the supplied PNGs and timeline outside the intended kind directory, potentially overwriting other surface state. Reject kind IDs that do not match the documented [a-z][a-z0-9_]* grammar before invoking the bridge.
Useful? React with 👍 / 👎.
| + name); | ||
| continue; | ||
| } | ||
| safe.put(name, e.getValue()); |
There was a problem hiding this comment.
Drop null remote image payloads
When an externally assembled image map contains a valid name whose blob is null—for example, after one attachment failed to decode—this filter retains the entry. The iOS bridge then calls OutputStream.write(null) in IOSSurfaceBridge.writeImages(), which throws a NullPointerException outside its IOException catch, aborting the publish instead of installing the otherwise valid timeline; Android already skips null-valued entries. Filter null blobs here along with invalid names.
Useful? React with 👍 / 👎.
Closes the gap the wearables chapter has been documenting since watch support shipped: a developer could declare
WidgetSize.WATCH_CIRCULAR, the value serialized onto the wire, and nothing on any device ever showed it.Five places in the code said so out loud. All five are now deleted rather than reworded.
Apple Watch
A
CN1WatchWidgetsWidgetKit extension, embedded in the watch app rather than the phone app. That single choice is what makes companion and standalone need no separate handling at all: the companion case already copies the finished watch app into the phone app with the.appexinside it, and the platform filter that keeps the watch tree out of the Catalyst slice covers the extension for free.Three premises turned out to be wrong, and each changed the design:
CN1DescriptorWidget.swiftdid not compile for watchOS.systemSmall/Medium/Large/ExtraLargeare@available(watchOS, unavailable)— unnameable, not merely absent — and the file named all four unconditionally.UIColor.systemBackground,UIColor(dynamicProvider:)andUIGraphicsImageRendererare allAPI_UNAVAILABLE(watchos)too.CN1_USE_WIDGETSwas explicitly switched off on the watch slice, soSurfaces.publish()from a watch app was a hard no-op that reported success.containerBackground(for:)is watchOS 10 and every generated widget applies it, so 9.0 fails the build rather than losing the background.The substitutes are the right answers rather than degradations: a watch face composites over black and has no light appearance, so the background role is black and a light/dark pair resolves to its dark half.
Also here: complication taps reach the action handler through the SwiftUI scene (there is no
UIApplicationDelegateon watchOS), and the phone→watch mirror.Wear OS
Complications and Tiles, generated per watch-bearing kind, plus the companion Wear artifact that has never been produced.
A complication is not a small widget, and the code says so. A watch face asks for one typed value and composes it into its own design, so the node tree is flattened and mined for content rather than rendered — at most two text nodes and one image. Everything dropped is logged once per render, so a developer whose careful layout arrives as one number learns that is by design.
A Tile does render the tree, and two things come out better than on a phone widget: circular progress renders natively where RemoteViews degrades to a linear bar, and per-node taps work where a small iOS widget honors only the root. The honest limitation is time — a countdown freezes and refreshes from the timeline, because ProtoLayout's dynamic expressions are version-sensitive and a frozen value that's always right beats a ticking one that works on some watches.
The companion module shares the app module's source tree rather than copying it. Both modules declare the same namespace — required, not merely convenient, because the shared sources refer to
Runqualified from the app's package. That AGP permits this was verified with a throwaway two-module Gradle project before the phase was written.The mirror
A watch app has its own storage; nothing the phone writes is visible there. On Apple the App Group identifier is the same string but resolves to a watch-local container, and on Wear OS the two apps are separate installs. So a phone-side publish reaches a complication only because the framework carries it.
It lives in the port on both platforms, for opposite reasons that point the same way. On Apple a core reference to
com.codename1.wearablewould flipusesWearablefor every surfaces app and link WatchConnectivity into apps that never asked. On AndroidExecutor.scanClassesForPermissionsreads the app's own classes and not the core, so the same reference would fail to flip it and the mirror would silently do nothing.Apple uses the one WatchConnectivity API that wakes the watch app in the background, which is budgeted at about fifty transfers a day; spending one when no complication is placed wastes what the app will want later, so that and an exhausted budget fall back to a queued transfer. Over the size cap the imagery is shed first — a complication rendering its numbers with a missing glyph beats one that never updates.
Applied on the watch headlessly: a file write and a re-render request, touching no framework state. The wake exists to refresh a complication, not to bring a UI forward nobody asked for.
Two fixes that stand alone
CN1BuildMojocollapsed same-extension artifacts onto one path. Two.apkentries inresult.zipoverwrote each other, corrupting the primary artifact, silently. Role suffixes now survive into the copied name.publishRemotediscarded its image side-map unconditionally, so a server-pushed descriptor referencing art has never rendered it.Verification
check-native-signaturesreports 0 fatal (down from 4 — the earlier "fatals" were stale-build artifacts).WearGlueCompilesTestcompiles the injected Wear services against the realCN1WatchSurfaceplus a 37-file stub tree, so a service that drifts from the reader's contract fails here rather than in a customer's Gradle build. Confirmed to fail when drift is injected.SurfacesSwiftWatchPortabilityTestis the half that runs on a CI leg with no Xcode; confirmed to fail when a guard is removed.build-ios-watchcompiles the extension on every PR and asserts the.appexis embedded at the right nesting.What is not verified
.appex. Nothing in this repo does that work. Localios-sourcewith automatic signing is unaffected.cn1-artifacts.properties. That contract is written but its other half lives out of repo — until it ships, the companion Wear artifact is verifiable throughandroid-sourceplus a local Gradle run.Twin PR in BuildDaemon mirrors the builder half.
🤖 Generated with Claude Code