Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
f5863b4
Watch build: one entry point, no hints, and a watch app that reaches …
shai-almog Jul 29, 2026
dbb6ae5
Add com.codename1.wearable and make the simulator able to run a paire…
shai-almog Jul 29, 2026
4d070a3
Carry com.codename1.wearable over WatchConnectivity on Apple
shai-almog Jul 29, 2026
aeef641
Wear OS: Data Layer link, rotary input and round-screen safe area
shai-almog Jul 29, 2026
2398d09
Complications as surfaces watch families
shai-almog Jul 29, 2026
2151fc9
Rewrite the wearables guide around the two-app model
shai-almog Jul 29, 2026
ceb7835
Fix two LanguageTool findings in the wearables chapter
shai-almog Jul 29, 2026
3800ecc
Add the GPLv2 + Classpath Exception header to files this branch touches
shai-almog Jul 29, 2026
66234f9
Use HashMap rather than EnumMap in WidgetTimeline
shai-almog Jul 29, 2026
ab400fd
Use Integer/Long/Double.valueOf rather than the boxing constructors
shai-almog Jul 29, 2026
3014bc1
Fix the bugs raised in review
shai-almog Jul 29, 2026
8d1574a
Address the remaining review findings
shai-almog Jul 29, 2026
d4448c6
Advertise a wearable capability, and say when no Wear APK is produced
shai-almog Jul 29, 2026
b8a27f2
Fix the second round of review findings
shai-almog Jul 29, 2026
14ac12c
Keep the push helpers and the capability cache honest
shai-almog Jul 29, 2026
4930d72
Generate the watch skins at build time, and finish the transfer path
shai-almog Jul 29, 2026
b83880e
Pass -force when launching the simulated watch app
shai-almog Jul 29, 2026
86b0ffe
Keep transferred files on their own path, and stop leaking request or…
shai-almog Jul 29, 2026
65f1b1f
Pin the watch simulator slice to arm64
shai-almog Jul 29, 2026
3975b78
Keep the embedded watch app out of the Mac Catalyst variant
shai-almog Jul 29, 2026
64fecf5
Carry per-file compiler flags into the watch target
shai-almog Jul 30, 2026
cc8e8b2
Stop forcing -sdk and project-wide ARCHS on multi-target iOS builds
shai-almog Jul 30, 2026
777ddf6
Merge remote-tracking branch 'origin/master' into watch-apps-product
shai-almog Jul 30, 2026
ea15e13
Wearables: fix 19 review findings across both platforms
shai-almog Jul 30, 2026
15f70da
Wearables: state the cold-start queue's guarantee accurately
shai-almog Jul 30, 2026
146d51e
Wearables: document what the watch app actually runs today
shai-almog Jul 30, 2026
a73910f
Wearables: correct the watch icon comment
shai-almog Jul 30, 2026
9a2d202
Wearables: fix 8 follow-up findings, mostly from the previous round
shai-almog Jul 30, 2026
9c12707
Wearables: don't generate an empty widget bundle; guard two lifecycle…
shai-almog Jul 30, 2026
b1d9b34
Wearables: delimit the transfer namespace, keep multi-watch delivery …
shai-almog Jul 30, 2026
f7c2565
Wearables: fix the final nine review findings
shai-almog Jul 31, 2026
ace780f
Merge remote-tracking branch 'origin/master' into watch-apps-product
shai-almog Aug 1, 2026
a92645b
Wearables: four more review fixes, three in the simulator
shai-almog Aug 1, 2026
5cb1e37
Wearables: close eight more review findings, two security-relevant
shai-almog Aug 1, 2026
5694945
Wearables: correct the watch-main comparison and three more findings
shai-almog Aug 1, 2026
e6c97bf
Wearables: record the three watch distribution gaps in the guide
shai-almog Aug 1, 2026
29c38a2
Wearables: four more findings, including a dependency conflict
shai-almog Aug 1, 2026
5068da3
Wearables: stop the simulator stamp compounding; bound transfer bookk…
shai-almog Aug 1, 2026
48a671c
Wearables: scope the transfer sweep to this device; keep the capabili…
shai-almog Aug 1, 2026
f3088c1
Wearables: close the asset stream when a transfer read fails
shai-almog Aug 1, 2026
840e0fa
Wearables: order writes with a logical clock; address only nodes runn…
shai-almog Aug 1, 2026
97cdc95
Wearables: three consequences of the logical clock
shai-almog Aug 1, 2026
a911324
Wearables: break sequence ties on the publishing node
shai-almog Aug 1, 2026
d5e0a44
Wearables: one resolution path, and keep the stamp when resolution fails
shai-almog Aug 1, 2026
cdd642d
Wearables: record the surviving item's stamp outright after a deletion
shai-almog Aug 1, 2026
b05004e
Wearables: restore valueMap and payloadOf, deleted by the getData ref…
shai-almog Aug 1, 2026
ba71503
Wearables: retry local identity; keep transfer claims out of the valu…
shai-almog Aug 1, 2026
92624c6
Wearables: don't re-announce a value when a shadow replica is deleted
shai-almog Aug 1, 2026
000632e
Wearables: read capability readiness and nodes as one snapshot
shai-almog Aug 1, 2026
81f822a
Wearables: resolve the real winner on the first event after a restart
shai-almog Aug 1, 2026
81609cd
Wearables: read the capability cache atomically; retry winner resolution
shai-almog Aug 1, 2026
32460db
Wearables: version the capability cache; coalesce transfer sweeps
shai-almog Aug 1, 2026
ec6286b
Wearables: generation-guard the remaining two cache paths
shai-almog Aug 1, 2026
f7a5bd4
Wearables: guard the send-time refresh; unpin the watch simulator arch
shai-almog Aug 1, 2026
b69e37c
Wearables: guard the last unguarded cache write, and audit the set
shai-almog Aug 1, 2026
ae85394
Wearables: retry the deletion resolution too
shai-almog Aug 1, 2026
252ae90
Wearables: fix a main-thread block, a bypassed retry, and an unlocked…
shai-almog Aug 1, 2026
c03cd8b
Wearables: guard watch-excluded sources in the source, not in a build…
shai-almog Aug 2, 2026
e3080f2
Wearables: filter capability callbacks by name; fix headers and a sta…
shai-almog Aug 2, 2026
beb8e45
Watch suite: pin the simulator locale and timezone
shai-almog Aug 2, 2026
be5544a
iOS builds: stop passing per-SDK conditional settings on the xcodebui…
shai-almog Aug 2, 2026
10062b8
Merge remote-tracking branch 'origin/master' into watch-apps-product
shai-almog Aug 2, 2026
cf7d432
Wearables: defer winner resolution instead of delivering an unverifie…
shai-almog Aug 2, 2026
7306d51
Android port: restore master's line endings in the two files this bra…
shai-almog Aug 2, 2026
da6ae58
Merge remote-tracking branch 'origin/master' into watch-apps-product
shai-almog Aug 2, 2026
801722c
Health: keep the EDT contract when a listener attaches after completion
shai-almog Aug 2, 2026
a446e4c
Wearables: defer deletion resolution too, instead of going silent
shai-almog Aug 2, 2026
e0c66f9
BrowserComponent test: time out against the clock, and allow a loaded…
shai-almog Aug 2, 2026
9c7731d
Wear hints: standalone must not imply Wear mode; make the release ret…
shai-almog Aug 3, 2026
4df7d3a
Wearables: compare-and-replace the delivery stamp after a blocking re…
shai-almog Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .github/workflows/ios-packaging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,18 @@ jobs:
| sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
POM_HASH=$(find . -name 'pom.xml' -not -path './scripts/*' 2>/dev/null \
| sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
# The build INVOCATION belongs in the key, not just the sources. Derived data
# embeds the SDK, destination and per-target product layout an invocation
# produced; restoring a tree built by a different one leaves Xcode's build
# database claiming targets are up to date when their products are absent
# ("Build input file cannot be found ... output of a script phase").
SCRIPT_HASH=$(shasum -a 256 \
scripts/setup-workspace.sh \
scripts/build-ios-port.sh \
scripts/build-native-themes.sh \
scripts/build-ios-app.sh \
scripts/run-ios-device-release-build.sh \
scripts/run-ios-ui-tests.sh \
.github/workflows/_build-ios-port.yml \
| shasum -a 256 | awk '{print $1}')
echo "hash=${SRC_HASH:0:16}-${POM_HASH:0:16}-${SCRIPT_HASH:0:16}" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -165,8 +173,10 @@ jobs:
with:
path: ${{ runner.temp }}/cn1-ios-device-release-derived
key: ${{ runner.os }}-ios-device-release-derived-${{ steps.src_hash.outputs.hash }}
restore-keys: |
${{ runner.os }}-ios-device-release-derived-
# Deliberately no restore-keys prefix. A partial match here restores derived
# data produced by a DIFFERENT build invocation, and a stale Xcode build
# database is worse than a cold compile: it reports success for targets it
# skipped. An exact hit or nothing.

- name: Build Release iOS device app without signing
env:
Expand Down
12 changes: 12 additions & 0 deletions CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Original file line number Diff line number Diff line change
Expand Up @@ -6000,6 +6000,18 @@ public boolean isCarConnected() {
return b != null && b.isConnected();
}

/// Returns the platform bridge that carries the `com.codename1.wearable` phone-to-watch API over
/// the native transport (Apple's `WCSession` / Google's Wearable Data Layer), or null when this
/// device has no wearable counterpart (the base implementation). When null, the
/// `com.codename1.wearable` API degrades to a harmless no-op.
///
/// #### Returns
///
/// the wearable bridge, or null when unsupported
public com.codename1.wearable.spi.WearableBridge getWearableBridge() {
return null;
}

/// Returns the platform bridge used by the `com.codename1.surfaces` API to render external
/// surfaces (home-screen widgets and live activities). Ports supporting surfaces override
/// this; the base implementation returns null which renders the whole API an inert no-op.
Expand Down
68 changes: 68 additions & 0 deletions CodenameOne/src/com/codename1/impl/health/EdtResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,72 @@ void superComplete(T value) {
void superError(Throwable t) {
super.error(t);
}

/// Marshalling `complete` is not enough on its own: a listener attached
/// *after* the resource has settled is run by
/// `AsyncResource.ready`/`except` inline, on whichever thread attaches it.
///
/// That is the whole contract leaking back out. The window is small but
/// entirely reachable -- `openHealthSettings` and `openProviderSetup`
/// complete before they return, so an off-EDT caller writing the ordinary
/// `openHealthSettings().onResult(cb)` races the `callSerially` above
/// between two adjacent statements. Win the race and the callback arrives
/// on the EDT; lose it and the same line of app code delivers off the EDT.
/// It is exactly the "may or may not be on the EDT" asymmetry this class
/// exists to remove, and it fails intermittently, which is worse than
/// failing always.
///
/// So the guarantee is applied where the callback is *invoked* rather than
/// where the resource is completed: whatever thread gets here, delivery
/// hops to the EDT if it is not already on it. Already on the EDT still
/// runs inline, so the no-runnable-per-link property above is kept.
@Override
public com.codename1.util.AsyncResource<T> ready(
final com.codename1.util.SuccessCallback<T> callback,
com.codename1.util.EasyThread t) {
if (t != null) {
// An explicit thread is the caller overriding the default on
// purpose; honouring the request beats honouring the default.
return super.ready(callback, t);
}
return super.ready(new OnEdt<T>(callback), null);
}

/// `except` is deliberately NOT wrapped the same way.
///
/// A late `except` on an already-failed resource fires synchronously, and
/// that is relied on to read an error back without waiting -- see
/// `HealthFallbackTest.errorOf`, which documents the trick and shares it
/// with `BtTestUtil`. Hopping the error to the EDT breaks every such
/// reader: the callback has not run yet when the value is read, so the
/// error reads as null and a failing call looks like a succeeding one.
///
/// So the asymmetry is left in place on purpose rather than overlooked.
/// Closing it means changing that read pattern everywhere it is used,
/// which is a wider behavioural change than the delivery bug this fixes
/// and belongs in its own change.

/// Named, not anonymous, for the same SpotBugs reason as `Deliver`.
private static final class OnEdt<X> implements com.codename1.util.SuccessCallback<X> {

private final com.codename1.util.SuccessCallback<X> delegate;

OnEdt(com.codename1.util.SuccessCallback<X> delegate) {
this.delegate = delegate;
}

@Override
public void onSucess(final X value) {
if (Display.getInstance().isEdt()) {
delegate.onSucess(value);
return;
}
Display.getInstance().callSerially(new Runnable() {
@Override
public void run() {
delegate.onSucess(value);
}
});
}
}
}
18 changes: 13 additions & 5 deletions CodenameOne/src/com/codename1/surfaces/SurfaceSerializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ public final class SurfaceSerializer {
private SurfaceSerializer() {
}

/// True when the timeline carries a layout for at least one size family. Iterating the enum
/// rather than naming families keeps this correct as the catalog grows -- the watch
/// complication families joined it without touching this method.
private static boolean hasAnyExplicitContent(WidgetTimeline timeline) {
for (WidgetSize size : WidgetSize.values()) {
if (timeline.getExplicitContent(size) != null) {
return true;
}
}
return false;
}

/// Serializes a widget timeline.
///
/// #### Parameters
Expand All @@ -63,11 +75,7 @@ private SurfaceSerializer() {
/// the timeline JSON
public static String serializeTimeline(String kindId, WidgetTimeline timeline,
Map<String, byte[]> imagesOut) {
if (timeline.getDefaultContent() == null
&& timeline.getContent(WidgetSize.SMALL) == null
&& timeline.getContent(WidgetSize.MEDIUM) == null
&& timeline.getContent(WidgetSize.LARGE) == null
&& timeline.getContent(WidgetSize.LOCKSCREEN) == null) {
if (timeline.getDefaultContent() == null && !hasAnyExplicitContent(timeline)) {
throw new IllegalArgumentException("A widget timeline needs content: call "
+ "setContent(...) before publishing");
}
Expand Down
65 changes: 60 additions & 5 deletions CodenameOne/src/com/codename1/surfaces/WidgetSize.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,41 @@
*/
package com.codename1.surfaces;

/// The size families a widget kind supports. iOS maps these to the WidgetKit families
/// (`systemSmall` / `systemMedium` / `systemLarge` and `accessoryRectangular` for `LOCKSCREEN`);
/// Android and desktop treat them as size hints. `LOCKSCREEN` is ignored on Android in this
/// version.
/// The size families a widget kind supports.
///
/// The first four are the phone families: iOS maps them to the WidgetKit families
/// (`systemSmall` / `systemMedium` / `systemLarge`, and `accessoryRectangular` for `LOCKSCREEN`);
/// Android and desktop treat them as size hints, and `LOCKSCREEN` is ignored on Android.
///
/// The `WATCH_*` families are **complications** -- the small live readouts on a watch face. They
/// live here rather than in an API of their own because they are the same concept as a widget:
/// content-driven, rendered while your app is not running, and fed by the same [WidgetTimeline]. On
/// Apple a complication is literally a WidgetKit widget in an accessory family; on Wear OS the
/// simple families become complication data and the richer ones become a Tile.
///
/// Design them for a glance. A complication is a few dozen pixels someone reads in under a second,
/// so a `SurfaceVector` gauge or a single number beats any layout that has to be read.
public enum WidgetSize {
/// Small square home-screen widget. iOS `systemSmall`.
SMALL("small"),
/// Medium home-screen widget. iOS `systemMedium`.
MEDIUM("medium"),
/// Large home-screen widget. iOS `systemLarge`.
LARGE("large"),
LOCKSCREEN("lockscreen");
/// Lock-screen widget. iOS `accessoryRectangular`.
LOCKSCREEN("lockscreen"),
/// Round complication -- the corner or centre slots of a watch face. iOS `accessoryCircular`;
/// Wear OS `RANGED_VALUE` or `MONOCHROMATIC_IMAGE`. Room for a gauge or one glyph.
WATCH_CIRCULAR("watchCircular"),
/// Wide complication, a band across the watch face. iOS `accessoryRectangular`; Wear OS
/// `LONG_TEXT`, or a Tile when the layout is richer than text. The roomiest family.
WATCH_RECTANGULAR("watchRectangular"),
/// One line of text alongside the time. iOS `accessoryInline`; Wear OS `SHORT_TEXT`. Text only --
/// anything else is dropped.
WATCH_INLINE("watchInline"),
/// Curved complication hugging the bezel of a round face. iOS `accessoryCorner`; renders as the
/// circular family on Wear OS, which has no corner slot.
WATCH_CORNER("watchCorner");

private final String jsonName;

Expand All @@ -42,4 +68,33 @@ public enum WidgetSize {
public String getJsonName() {
return jsonName;
}

/// True for the watch complication families, which are published to a watch face rather than to
/// a home or lock screen.
///
/// #### Returns
///
/// true if this is a complication family
public boolean isWatchFamily() {
return this == WATCH_CIRCULAR || this == WATCH_RECTANGULAR
|| this == WATCH_INLINE || this == WATCH_CORNER;
}

/// Resolves a wire-format name back to its family.
///
/// #### Parameters
///
/// - `jsonName`: the name produced by [#getJsonName()]
///
/// #### Returns
///
/// the matching family, or null when the name is unknown
public static WidgetSize fromJsonName(String jsonName) {
for (WidgetSize s : values()) {
if (s.jsonName.equals(jsonName)) {
return s;
}
}
return null;
}
}
62 changes: 14 additions & 48 deletions CodenameOne/src/com/codename1/surfaces/WidgetTimeline.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,12 @@ public Map<String, Object> getState() {
}

private SurfaceNode defaultContent;
private SurfaceNode smallContent;
private SurfaceNode mediumContent;
private SurfaceNode largeContent;
private SurfaceNode lockscreenContent;
/// Per-family layout overrides. A map rather than a field per family: the catalog grows (the
/// watch accessory families joined the phone ones) and a switch per accessor did not. A plain
/// HashMap rather than an EnumMap -- the Codename One runtime has no EnumMap, and lookups here
/// are by key so the ordering an EnumMap would give buys nothing.
private final Map<WidgetSize, SurfaceNode> overrides =
new java.util.HashMap<WidgetSize, SurfaceNode>();
private final List<Entry> entries = new ArrayList<Entry>();
private int reloadPolicy = RELOAD_AT_END;

Expand Down Expand Up @@ -105,21 +107,12 @@ public WidgetTimeline setContent(SurfaceNode root) {
///
/// this timeline, for chaining
public WidgetTimeline setContent(WidgetSize size, SurfaceNode root) {
switch (size) {
case SMALL:
smallContent = root;
break;
case MEDIUM:
mediumContent = root;
break;
case LARGE:
largeContent = root;
break;
case LOCKSCREEN:
lockscreenContent = root;
break;
default:
break;
if (size != null) {
if (root == null) {
overrides.remove(size);
} else {
overrides.put(size, root);
}
}
return this;
}
Expand Down Expand Up @@ -172,41 +165,14 @@ public WidgetTimeline setReloadPolicy(int policy) {
///
/// the layout root, or null when neither an override nor a default was set
public SurfaceNode getContent(WidgetSize size) {
SurfaceNode override = null;
switch (size) {
case SMALL:
override = smallContent;
break;
case MEDIUM:
override = mediumContent;
break;
case LARGE:
override = largeContent;
break;
case LOCKSCREEN:
override = lockscreenContent;
break;
default:
break;
}
SurfaceNode override = size == null ? null : overrides.get(size);
return override != null ? override : defaultContent;
}

/// Returns the explicit per-size override, or null when the size family falls back to the
/// default content. Used by the serializer so only real overrides are emitted per size.
SurfaceNode getExplicitContent(WidgetSize size) {
switch (size) {
case SMALL:
return smallContent;
case MEDIUM:
return mediumContent;
case LARGE:
return largeContent;
case LOCKSCREEN:
return lockscreenContent;
default:
return null;
}
return size == null ? null : overrides.get(size);
}

/// Returns the layout used for size families without an explicit override, or null.
Expand Down
12 changes: 12 additions & 0 deletions CodenameOne/src/com/codename1/ui/Display.java
Original file line number Diff line number Diff line change
Expand Up @@ -4713,6 +4713,18 @@ public com.codename1.car.spi.CarBridge getCarBridge() {
return impl.getCarBridge();
}

/// Returns the platform bridge used by the `com.codename1.wearable` API to talk to the
/// counterpart watch or phone app, or null when this device has no wearable counterpart.
/// Internal -- application code uses the `com.codename1.wearable` API rather than this bridge
/// directly.
///
/// #### Returns
///
/// the wearable bridge, or null
public com.codename1.wearable.spi.WearableBridge getWearableBridge() {
return impl.getWearableBridge();
}

/// Returns the platform bridge used by the `com.codename1.surfaces` API to render external
/// surfaces (home-screen widgets and live activities), or null when unsupported on this port.
/// Internal -- application code uses the `com.codename1.surfaces` API rather than this bridge
Expand Down
Loading
Loading