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:
parent
5d47efd7c5
commit
59fb9c4d6d
1 changed files with 1511 additions and 62 deletions
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
|
|
@ -168,4 +170,122 @@ void main() {
|
|||
|
||||
expect(find.textContaining('Update it to keep syncing'), findsNothing);
|
||||
});
|
||||
|
||||
group('on mobile data (ADR 0042)', () {
|
||||
late FakeNetworkWatch network;
|
||||
late int syncs;
|
||||
|
||||
/// Shows the banner above the Sync card, on a phone ([offered]) on
|
||||
/// mobile data, sync started; [changes] made here wait.
|
||||
Future<(SyncService, MobileDataGate)> showPhone(
|
||||
WidgetTester tester, {
|
||||
bool offered = true,
|
||||
int changes = 0,
|
||||
}) async {
|
||||
final gate = MobileDataGate(network: network, offered: offered);
|
||||
addTearDown(gate.dispose);
|
||||
final sync = SyncService(({required onMerge}) async {
|
||||
syncs++;
|
||||
}, mobileData: gate)..start();
|
||||
for (var change = 0; change < changes; change++) {
|
||||
sync.requestSync();
|
||||
}
|
||||
await tester.pumpWidget(
|
||||
BlocProvider(
|
||||
create: (context) =>
|
||||
SyncStatusBloc(sync)..add(const SyncStatusEventStarted()),
|
||||
child: testApp(
|
||||
Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) => Column(
|
||||
children: [
|
||||
?mobileDataBannerOf(context),
|
||||
const Expanded(
|
||||
child: SingleChildScrollView(child: SyncStatusCard()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
dependencies: TestAppDependencies(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(seconds: 3));
|
||||
await tester.pumpAndSettle();
|
||||
return (sync, gate);
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
network = FakeNetworkWatch()..state = FakeNetworkWatch.mobile;
|
||||
syncs = 0;
|
||||
});
|
||||
|
||||
testWidgets('the banner counts the changes waiting; Not now puts it off '
|
||||
'until another waits', (tester) async {
|
||||
final (sync, _) = await showPhone(tester, changes: 2);
|
||||
expect(find.text('On mobile data · 2 changes waiting'), findsOneWidget);
|
||||
expect(find.text('Held back on mobile data'), findsOneWidget);
|
||||
expect(syncs, 0);
|
||||
|
||||
await tester.tap(find.text('Not now'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(MobileDataBanner), findsNothing);
|
||||
|
||||
sync.requestSync();
|
||||
await tester.pump(const Duration(seconds: 3));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('On mobile data · 3 changes waiting'), findsOneWidget);
|
||||
expect(syncs, 0);
|
||||
unawaited(sync.dispose());
|
||||
});
|
||||
|
||||
testWidgets('Sync now syncs and the banner goes', (tester) async {
|
||||
final (sync, _) = await showPhone(tester);
|
||||
expect(find.text('On mobile data · sync paused'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Sync now').first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(syncs, 1);
|
||||
expect(find.byType(MobileDataBanner), findsNothing);
|
||||
unawaited(sync.dispose());
|
||||
});
|
||||
|
||||
testWidgets('Without files syncs, and the Sync card says the files wait', (
|
||||
tester,
|
||||
) async {
|
||||
final (sync, _) = await showPhone(tester);
|
||||
|
||||
await tester.tap(find.text('Without files'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(syncs, 1);
|
||||
expect(find.byType(MobileDataBanner), findsNothing);
|
||||
expect(find.text('Files wait for Wi-Fi'), findsOneWidget);
|
||||
unawaited(sync.dispose());
|
||||
});
|
||||
|
||||
testWidgets('the Sync card chooses what goes unasked; Sync everything '
|
||||
"syncs and doesn't ask", (tester) async {
|
||||
final (sync, gate) = await showPhone(tester);
|
||||
expect(find.text('Ask first'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Ask first'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Sync everything').last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(gate.mobileData, MobileData.everything);
|
||||
expect(syncs, 1);
|
||||
expect(find.byType(MobileDataBanner), findsNothing);
|
||||
unawaited(sync.dispose());
|
||||
});
|
||||
|
||||
testWidgets('a desktop shows no banner and no choice', (tester) async {
|
||||
network.state = FakeNetworkWatch.wifi;
|
||||
final (sync, _) = await showPhone(tester, offered: false);
|
||||
expect(find.byType(MobileDataBanner), findsNothing);
|
||||
expect(find.text('On mobile data'), findsNothing);
|
||||
expect(syncs, 1);
|
||||
unawaited(sync.dispose());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue