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
2 changed files with 1511 additions and 62 deletions

View file

@ -259,6 +259,7 @@ class MeditateDependencies({
screen: device.screen,
speech: device.speech,
guide: device.guide,
mobileData: device.mobileData,
onClosed: closed.complete,
);
} on Object {
@ -298,6 +299,7 @@ Future<MeditatePerson> _openPersonData(
required ScreenAwake screen,
required SpeechInput speech,
required GuidePlayer guide,
required MobileDataGate? mobileData,
required void Function() onClosed,
}) async {
final db = await client.createSyncSession(
@ -319,6 +321,7 @@ Future<MeditatePerson> _openPersonData(
),
pendingChanges: kept,
syncedBefore: (await kept.read()).synced,
mobileData: mobileData,
)..start();
// The person's key, with which everything of theirs is sealed (D5); read
// again whenever the rows are, as a sync may have brought it.
@ -398,6 +401,10 @@ class MeditateDevice({
/// The player of the library's recordings, one for the whole app (D16).
final GuidePlayer guide = const NoGuidePlayer(),
/// What sync may do on the network the device is on (Halkyon's ADR
/// 0042): every person's on this device.
final MobileDataGate? mobileData,
}) {
/// How long the recogniser may wait through a pause while a dream is
/// told, and how long one listen may last: as long as the platform allows
@ -412,6 +419,9 @@ class MeditateDevice({
final support = (await getApplicationSupportDirectory()).path;
return MeditateDevice(
support: support,
mobileData: await MobileDataGate.forDevice(
File(p.join(support, 'mobile_data.json')),
),
storage: InstanceStorage(support, builtIn: builtIn),
appBuild: int.tryParse(package.buildNumber),
appVersion: package.version,

View file

@ -23,6 +23,8 @@ class const MeditateShell({
final l10n = AppLocalizations.of(context);
return WhatsNewOnUpdate(
child: NavigationFrame(
// Sync held back on mobile data asks above every tab (ADR 0042).
banner: mobileDataBannerOf(context),
// Open into the sidebar or not, as the device keeps it (decided
// 10 Nov 2026).
sidebarOpen: context.select<LookCubit, bool>(