feat(app,ui): sync waits on mobile data and asks first

The developer's request of 10 Nov 2026, decided in ADR 0042: on mobile
data every app holds its sync back, rows and files alike, and asks.

halkyon_app becomes an Android plugin, its first native code, registered
in every app by Flutter: HalkyonAppPlugin.java says which network the
device is on and whether it is metered as Android has it (no
NET_CAPABILITY_NOT_METERED: cellular, a phone's hotspot, Wi-Fi marked
metered). Java, so it applies no Kotlin Gradle Plugin.

MobileDataGate, one per device, holds the setting (Ask first, Sync
without files, Sync everything; kept in <app support>/mobile_data.json),
the network, and the person's answer, which holds until the device moves
to another network or off a metered one. SyncService asks it before each
sync: held back, nothing goes, the status counts what waits and asks; an
answer or Wi-Fi syncs at once, and syncing now while held back is Sync
now. Each app's frame shows the banner above every tab, in halkyon_ui's
new NavigationFrame.banner slot (On mobile data · 3 changes waiting, with
Not now, Without files and Sync now); the Sync card in Settings chooses
the setting on a phone and says when files wait for Wi-Fi. Learn's files
neither go up nor come down while held back.

Whats-New: On mobile data the app no longer syncs by itself: a banner asks first, and Settings › Sync chooses whether to ask, sync without files, or sync everything.
Whats-New-De: Mit mobilen Daten synchronisiert die App nicht mehr von selbst: Ein Hinweis fragt zuerst, und unter Einstellungen › Synchronisierung wählst du, ob sie fragt, ohne Dateien oder alles synchronisiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-10 23:38:17 +01:00
commit 59fb9c4d6d
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1,7 +1,7 @@
# ADR 0042: Sync held back on mobile data
- **Status:** decided 10 Nov 2026 by the developer: D1 is their own request, D2 to D5
the agent's recommendations, which they chose. Not built yet.
the agent's recommendations, which they chose. Built 10 Nov 2026 (below).
- **Builds on:** ADR 0014 (halkyon_app, every app's shell, with its `SyncService`).
## Context
@ -53,3 +53,35 @@ card (*3 files waiting for Wi-Fi*).
and sending nothing.
- Learn's offline reads (b578fcb9) must not wait on a sync held back: the tests of
`ContentKeys` and the gateways already run without one.
## As built (10 Nov 2026)
The developer chose two more things while it was built: the native code as halkyon_app's
own Android plugin, not a Kotlin file in each app, and the banner in the frame, above
every tab.
- **halkyon_app's plugin** (`android/`, `HalkyonAppPlugin.java`): the first native code of
a shared package, registered in every app by Flutter. Java, so it applies no Kotlin
Gradle Plugin. It answers the default network on `app.halkyon/network` and sends each
change on `app.halkyon/network/changes`: its handle as `id`, and `metered` where
`NET_CAPABILITY_NOT_METERED` is missing (unless temporarily not metered, Android 11 on).
It declares `ACCESS_NETWORK_STATE`. Dart: `AndroidNetworkWatch`, `NeverMetered` on other
platforms, `deviceNetworkWatch()`.
- **`MobileDataGate`** (the device's, one for every person's sync): the setting, kept in
`<app support>/mobile_data.json`; the network; the answer, bound to the network's `id`
and dropped when the device moves to another network or off a metered one; `allowance`
(`nothing`, `rows`, `everything`) and `filesAllowed`. Nothing syncs until it knows the
network, or for 3 s at most.
- **`SyncService(mobileData:)`**: a trigger while nothing is allowed syncs nothing; the
status says `heldBack`, `asking` (until *Not now*, and again once another change
waits), `filesHeld` and the setting (`mobileData`, null on a desktop). `syncNow` while
held back answers *Sync now*. The gate allowing more syncs at once.
- **The banner**: `mobileDataBannerOf(context)` in each app's `NavigationFrame.banner`
(halkyon_ui: above the tabs, their screens without the top inset, which the banner keeps
clear of itself). **Settings**: the Sync card's *On mobile data* field and its lines
*Held back on mobile data* and *Files wait for Wi-Fi*.
- **Files**: Learn's `AssetRepository(filesAllowed:)` uploads nothing and downloads
nothing while files are held, as without a connection; what waits goes after the next
sync that may carry it. Todo's attachments (ADR 0043) will do the same.
- **Not built**: the count of files waiting in the Sync card; iOS (`NWPath.isExpensive`).