Skip to content

Watch apps: one entry point, a phone-watch channel, and a simulator that can run the pair - #5487

Open
shai-almog wants to merge 262 commits into
masterfrom
watch-apps-product
Open

Watch apps: one entry point, a phone-watch channel, and a simulator that can run the pair#5487
shai-almog wants to merge 262 commits into
masterfrom
watch-apps-product

Conversation

@shai-almog

@shai-almog shai-almog commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

isWatch() existed without a product on top of it. This turns the watch render
slice 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:

  • A cloud build never produced a watch app. codename1.watchMain became a
    build argument only on the local path; the server lifts only codename1.arg.*
    keys out of the uploaded settings file, so the daemon asked for watchMain and
    got nothing.
  • 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.
  • watchMain reached only iOS. Wear OS was enabled by an unrelated
    android.wear hint, so a project had to declare the same intent twice.
  • The simulator had no watch form factor. JavaSEPort never overrode
    isWatch(), so the guide's advice to iterate on a watch layout locally was
    untrue.
  • A Wear app could not scroll. onGenericMotionEvent read only the mouse
    axes; rotary input arrives on SOURCE_ROTARY_ENCODER / AXIS_SCROLL.
  • A round Wear face clipped its own corners. No display cutout is reported,
    so the safe area came back zero.

What is new

One setting. codename1.watchMain is the entire opt-in on both platforms.
Nine build hints are deleted; bundle id, deployment target, team id and display
name are derived. codename1.watchStandalone is the only other setting — the one
thing not inferable from the project. Net new hints: zero.

com.codename1.wearable — the phone↔watch channel, same API on Apple Watch
and Wear OS, modelled on com.codename1.car (portable API, SPI bridge, inert
when 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": sendMessage for a live answer, putData for
state that survives sleep and relaunch, transferFile for bulk. Callbacks arrive
on the EDT and are queued across a cold start — the platform starts an app purely
to hand it a payload. Backed by WCSession on Apple and the Wearable Data Layer
on 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 a
Watch menu that launches watchMain in a second process wired to the first — so
sendMessage and putData genuinely round-trip on the desktop. Two processes,
not two windows: Display is a singleton and sharing it would hide the bugs that
only 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/CORNER join
WidgetSize rather 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

  • The watchOS widget extension target and the Wear complication/tile services
    that render the watch families are not generated yet. The guide says so.
  • The watch target compiles its OWN ParparVM translation, rooted at
    codename1.watchMain, and boots a watch stub. Verified end to end: the watch
    target 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,
    SVGAnimatedScreenshotTest and LottieAnimatedScreenshotTest fall back to a
    placeholder 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.
  • Companion mode on Android does not yet emit a second wear APK; standalone works.

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 skipped
  • core-unittests: 4754 pass, with SpotBugs, PMD and Checkstyle at zero
  • core, JavaSE, iOS and Android ports build; the injected Android bridge sources
    type-check against a stub harness (nothing in CI compiles them) and the
    WatchConnectivity native passes clang -fsyntax-only for iphoneos/arm64
  • docs gates green: snippet validation (659 blocks), Asciidoctor
    --failure-level WARN, Vale, capitalization
  • build-ios-watch does run in CI -- it is a job in Test iOS UI build
    scripts
    , on macos-15 with DEVELOPER_DIR pointed at Xcode 26, and this PR
    touches 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

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants