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>
87 lines
5.1 KiB
Markdown
87 lines
5.1 KiB
Markdown
# 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. Built 10 Nov 2026 (below).
|
||
- **Builds on:** ADR 0014 (halkyon_app, every app's shell, with its `SyncService`).
|
||
|
||
## Context
|
||
|
||
Every Halkyon app syncs whenever something changes and on a timer, on whatever network
|
||
the device has. Learn's library holds tens of thousands of rows and hundreds of
|
||
megabytes of PDFs; a first sync, or an import on another device, can take a phone's
|
||
monthly data on the go. The developer asked (10 Nov 2026): *"It should hold back
|
||
everything, asking the user whether one wants to sync. Then, give also the option to
|
||
exclude files, images etc."*
|
||
|
||
## Decisions
|
||
|
||
- **D1.** On mobile data, every app holds back its sync, rows and files alike, and asks
|
||
first. Built once, in halkyon_app, around its `SyncService`; nothing an app does on
|
||
the device waits on it (local-first as before: what is written is kept and sent
|
||
later).
|
||
- **D2.** *Mobile data* is a **metered** connection as the operating system says: on
|
||
Android its `NET_CAPABILITY_NOT_METERED` missing, which takes in cellular and also a
|
||
phone's hotspot or a Wi-Fi network marked metered; read by a few lines of Kotlin in
|
||
halkyon_app's plugin code (`connectivity_plus` tells only the kind of network, which
|
||
misses the hotspot). iOS: `NWPath.isExpensive`, once the iOS builds ship. Desktops
|
||
are never metered.
|
||
- **D3.** It asks with a **quiet banner** above the lists, never a dialog: *On mobile
|
||
data · 12 changes waiting* with *Sync now*, *Without files* and *Not now*. *Not now*
|
||
hides it until something new waits. Pulling a list down to sync is an explicit ask
|
||
too, and syncs as *Sync now* would.
|
||
- **D4.** An answer **holds until the connection changes**: while the device stays on
|
||
this metered network; asked again after Wi-Fi came and went, or when the app starts
|
||
again on mobile data.
|
||
- **D5.** Settings › Sync has *On mobile data*: **Ask** (the default), **Without files**,
|
||
**Everything**. No *Never*: *Not now* holds back already. A device setting (as the
|
||
look), not synced: a tablet on Wi-Fi alone and a phone differ.
|
||
|
||
## What "without files" means
|
||
|
||
Rows sync; files neither go up nor come down. Each app names its files to halkyon_app
|
||
as a queue of its own (Learn's asset uploads and downloads, its PDFs and Jellyfin
|
||
covers; Todo's attachments, ADR 0043; Train's maps' tiles stay as they are, fetched
|
||
when shown), which `SyncService` pauses and resumes. What is waiting shows in Sync's
|
||
card (*3 files waiting for Wi-Fi*).
|
||
|
||
## To work out when building
|
||
|
||
- Where the banner sits in each app's frame: halkyon_ui's `NavigationFrame` has no
|
||
place for one above every tab yet.
|
||
- The sync engine's stream: whether held back means not opening it at all (the
|
||
recommendation: nothing goes over the network, not even the handshake) or opening it
|
||
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`).
|
||
|