Skip to content

feat(wear): let every tile be added more than once - #85

Merged
SiteRelEnby merged 2 commits into
mainfrom
feat/wear-multi-instance-tiles
Sep 20, 2026
Merged

SiteRelEnby merged 2 commits into
mainfrom
feat/wear-multi-instance-tiles

Conversation

@SiteRelEnby

Copy link
Copy Markdown
Collaborator

Wear will only offer a second copy of a tile whose provider declares two private clockwork metadata keys:

<meta-data android:name="com.google.android.clockwork.tiles.MULTI_INSTANCES_SUPPORTED"
           android:value="true" />
<meta-data android:name="com.google.android.clockwork.tiles.PROVIDER_CONFIG_ACTION"
           android:value="systems.lupine.sheaf.wear.tile.CONFIG_QUICK_SWITCH" />

Both are required - the flag alone does nothing, which is why our tiles sat at one apiece however much per-instance state they kept. Nothing in androidx.wear.tiles hints at this; it came from reading a shipping app's manifest.

The plumbing was mostly here already: every request has carried a tileId and the member-set tiles have always stored their roster against it. The work was declaring the keys, adding an action per tile type to the existing member picker, and teaching it the system's launch shape - the tile id arrives as com.google.android.clockwork.EXTRA_PROVIDER_CONFIG_TILE_ID rather than our own extra, and which tile to refresh has to come from the action, since the system sends no class hint.

Actions are fully qualified rather than the bare strings the reference app uses. An implicit action matches device-wide, and ConfigQuickSwitchTile is one collision away from dropping somebody into another app's settings.

So that configuring a tile isn't a screen that does nothing

The four tiles that had no configuration now honour the same roster the other two always have. orderedFronters takes a tile id and narrows to that copy's members, an empty roster meaning everybody - which is what an unconfigured tile has, and therefore what every tile did before this. History keeps an entry if anyone on the roster appears in it, so a co-front involving one of them stays in their history. The fronters-list tile joins its own names now instead of reading the pre-joined blob, which is the whole system's list and not necessarily this tile's.

The avatar tiles declare their images in a separate request, and that had to follow the same filter: a superset only wastes a decode, but a subset renders blank.

Not in this PR

Group and tag filters, hiding custom fronts, and sort order. Those need the watch's member model widened and the bulk membership reads landing in server 1.5.1, and are the next piece of work. The member roster came free here because it already had storage and a picker.

Checked

  • Verified in the built APK's manifest with aapt2: six tiles declaring multi-instance, six config actions, six PROVIDER_CONFIG intent-filters, and still six tile services, so no extra rows in the picker.
  • Multi-instance behaviour confirmed on a real watch.

Wear will only offer a second copy of a tile whose provider declares two
private clockwork metadata keys: MULTI_INSTANCES_SUPPORTED, and
PROVIDER_CONFIG_ACTION naming an action handled by an activity registered
under the tiles PROVIDER_CONFIG category. The flag alone does nothing, which
is why our tiles sat at one apiece however much per-instance state they kept.
Nothing in androidx.wear.tiles hints at this; it was found by reading a
shipping app's manifest.

The plumbing was mostly here already. Every request has carried a tileId and
the member-set tiles have always stored their roster against it, so the work
was declaring the keys, adding an action per tile type to the existing member
picker, and teaching it the system's launch shape: the tile id arrives as
com.google.android.clockwork.EXTRA_PROVIDER_CONFIG_TILE_ID rather than our own
extra, and which tile to refresh has to come from the action rather than a
class hint the system does not send.

Actions are fully qualified rather than the bare strings the reference app
uses. An implicit action matches device-wide, and "ConfigQuickSwitchTile" is
one collision away from dropping somebody into another app's settings.

So that configuring a tile is not a screen that does nothing, the four tiles
that had no configuration now honour the same roster the other two always
have: orderedFronters takes a tile id and narrows to that copy's members, an
empty roster meaning everybody, which is what an unconfigured tile has and
therefore what every tile did before. History keeps an entry if anyone on the
roster appears in it, so a co-front involving one of them stays in their
history. The fronters-list tile joins its own names now instead of reading the
pre-joined blob, which is the whole system's list and not necessarily this
tile's.

The avatar tiles declare their images in a separate request, and that had to
follow the same filter: a superset only wastes a decode, but a subset renders
blank.

Group and tag filters, hiding custom fronts, and sort order are the rest of
this and are not here. They need the watch's member model widened first - it
carries no is_custom_front, no privacy, no tags and no group membership, and
the watch fetches neither groups nor tags. The member roster came free because
it already had storage and a picker.

These are private keys, so whether the picker honours them varies by OEM and
Wear version. Verified in the built manifest; needs a watch to confirm the
picker behaviour.
@SiteRelEnby
SiteRelEnby merged commit afaadc9 into main Sep 20, 2026
1 check passed
@SiteRelEnby
SiteRelEnby deleted the feat/wear-multi-instance-tiles branch September 20, 2026 05:10
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.

1 participant