halkyon/docs/adr/0042-mobile-data.md
Tizian 「ティツィアーン」 59fb9c4d6d
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>
2026-11-10 23:46:29 +01:00

5.1 KiB
Raw Blame History

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).