diff --git a/integration_test/background_interaction_test.dart b/integration_test/background_interaction_test.dart index 82177c6..80a1553 100644 --- a/integration_test/background_interaction_test.dart +++ b/integration_test/background_interaction_test.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'dart:isolate'; import 'dart:ui'; -import 'package:app4training/background_task.dart'; -import 'package:app4training/background_test.dart'; +import 'package:app4training/background/background_task.dart'; +import 'package:app4training/background/background_test.dart'; import 'package:app4training/data/globals.dart'; import 'package:app4training/data/languages.dart'; import 'package:app4training/main.dart'; @@ -15,6 +15,7 @@ import 'package:integration_test/integration_test.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:workmanager/workmanager.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations_de.dart'; void main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -76,6 +77,7 @@ void main() async { // The languageStatusProvider haven't been loaded into memory yet - // Let's open the settings and close them again to make sure we have them // so that we can detect background activity later + await tester.ensureVisible(find.text('Einstellungen')); await tester.tap(find.text('Einstellungen')); await tester.pumpAndSettle(); Navigator.of(tester.element(find.byType(Scaffold))).pop(); @@ -89,7 +91,7 @@ void main() async { final msg = await completer.future.timeout(const Duration(seconds: 10)); expect(msg, equals('success')); - // Now open a worksheet to trigger ViewPage.checkForBackgroundActivity() + // Now open a worksheet to trigger the check for background activity await tester.tap(find.text('Grundlagen')); await tester.pumpAndSettle(); expect(find.text('Schritte der Vergebung'), findsOneWidget); @@ -98,17 +100,8 @@ void main() async { await tester.pumpAndSettle(); expect(find.byType(ViewPage), findsOneWidget); - /* TODO - checkForBackgroundActivity correctly detects activity (see debug prints), - but how do we make a test out of this? - ViewPage viewPage = - find.byType(ViewPage).evaluate().single.widget as ViewPage; - // Cast the widget to the appropriate type to access the WidgetRef - final ref = ProviderContainer( - overrides: [sharedPrefsProvider.overrideWithValue(prefs)]); - expect(viewPage.checkForBackgroundActivity(), true);*/ - - await Future.delayed(const Duration(seconds: 10)); + // Check whether the snack bar is visible + expect(find.text(AppLocalizationsDe().foundBgActivity), findsOneWidget); }); } diff --git a/lib/background/background_result.dart b/lib/background/background_result.dart new file mode 100644 index 0000000..d123738 --- /dev/null +++ b/lib/background/background_result.dart @@ -0,0 +1,86 @@ +import 'package:app4training/data/globals.dart'; +import 'package:app4training/data/languages.dart'; +import 'package:app4training/data/updates.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// The results of the background task (if it ran and did something) +/// Currently it holds just a bool but this will be extended +/// to hold detailled information in case there was some activity +@immutable +class BackgroundResult { + final bool foundActivity; + + const BackgroundResult(this.foundActivity); + + @override + String toString() { + return 'Background result: activity = $foundActivity'; + } +} + +class BackgroundResultNotifier extends Notifier { + @override + BackgroundResult build() { + return const BackgroundResult(false); + } + + /// Check whether the background task found updates and if yes: read results + /// Returns whether we found activity of the background task + /// + /// Implementation: Are the lastChecked dates + /// in the SharedPreferences newer than what we have stored in languageStatus? + /// (TODO see overview over synchronization with background isolate) + /// + /// Remark: languageStatusProviders must have been initialized already before, + /// otherwise they're loading their lastChecked times from sharedPrefs now + /// and can't detect any background activity + Future checkForActivity() async { + debugPrint("Checking for background activity"); + bool foundBgActivity = false; + + // Reload SharedPreferences because they're cached + // May need to change when SharedPreferences gets an API to directly + // use it asynchronously: https://github.com/flutter/packages/pull/5210 + await ref.read(sharedPrefsProvider).reload(); + + for (String languageCode in ref.read(availableLanguagesProvider)) { + // We don't check languages that are not downloaded + if (!ref.read(languageProvider(languageCode)).downloaded) continue; + + DateTime lcTimestampOrig = + ref.read(languageStatusProvider(languageCode)).lastCheckedTimestamp; + DateTime? lcTimestamp; + String? lcRaw = + ref.read(sharedPrefsProvider).getString('lastChecked-$languageCode'); + if (lcRaw != null) { + try { + lcTimestamp = DateTime.parse(lcRaw).toUtc(); + } on FormatException { + debugPrint( + 'Error while trying to parse lastChecked timestamp: $lcRaw'); + lcTimestamp = null; + } + } + if ((lcTimestamp != null) && + (lcTimestamp.compareTo(DateTime.now()) <= 0) && + lcTimestamp.compareTo(lcTimestampOrig) > 0) { + // It looks like there has been background activity! + debugPrint( + 'Background activity detected: lastChecked was $lcTimestampOrig, sharedPrefs says $lcTimestamp'); + foundBgActivity = true; + } else { + debugPrint( + 'No background activity. lastChecked: $lcTimestampOrig, sharedPrefs says $lcRaw'); + } + } + debugPrint('Checking for background activity done'); + state = BackgroundResult(foundBgActivity); + return foundBgActivity; + } +} + +final backgroundResultProvider = + NotifierProvider(() { + return BackgroundResultNotifier(); +}); diff --git a/lib/background_task.dart b/lib/background/background_task.dart similarity index 98% rename from lib/background_task.dart rename to lib/background/background_task.dart index 267c686..6276856 100644 --- a/lib/background_task.dart +++ b/lib/background/background_task.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'dart:ui'; -import 'package:app4training/background_test.dart'; +import 'package:app4training/background/background_test.dart'; import 'package:app4training/data/globals.dart'; import 'package:app4training/data/languages.dart'; import 'package:app4training/data/updates.dart'; diff --git a/lib/background_test.dart b/lib/background/background_test.dart similarity index 100% rename from lib/background_test.dart rename to lib/background/background_test.dart diff --git a/lib/l10n/locales/app_de.arb b/lib/l10n/locales/app_de.arb index 307793c..bf60d79 100644 --- a/lib/l10n/locales/app_de.arb +++ b/lib/l10n/locales/app_de.arb @@ -332,5 +332,6 @@ "ignore": "Ignorieren", "updatesExplanation": "Ab und zu werden manche Materialien aktualisiert: Wir veröffentlichen eine neue Version eines Arbeitsblattes oder fügen eine neue Übersetzung hinzu.\nUnser Ziel ist, dass du dir darüber keine Gedanken machen brauchst, sondern immer die aktuellsten Versionen einsatzbereit dabei hast. Deshalb kann die App im Hintergrund nach Aktualisierungen suchen und sie automatisch herunterladen, wenn du das möchtest.", "letsGo": "Los geht's!", - "homeExplanation": "Gott baut sein Reich überall auf der Welt. Er möchte, dass wir dabei mitmachen und andere zu Jüngern machen!\nDiese App will dir diese Aufgabe erleichtern: Wir stellen dir gute Trainingsmaterialien zur Verfügung. Und das Beste ist: Du kannst dasselbe Arbeitsblatt in verschiedenen Sprachen anschauen, so dass du immer weißt, was es bedeutet, selbst wenn du eine Sprache nicht verstehst.\n\nAlle Inhalte sind nun offline verfügbar und jederzeit bereit auf deinem Handy:" + "homeExplanation": "Gott baut sein Reich überall auf der Welt. Er möchte, dass wir dabei mitmachen und andere zu Jüngern machen!\nDiese App will dir diese Aufgabe erleichtern: Wir stellen dir gute Trainingsmaterialien zur Verfügung. Und das Beste ist: Du kannst dasselbe Arbeitsblatt in verschiedenen Sprachen anschauen, so dass du immer weißt, was es bedeutet, selbst wenn du eine Sprache nicht verstehst.\n\nAlle Inhalte sind nun offline verfügbar und jederzeit bereit auf deinem Handy:", + "foundBgActivity": "Im Hintergrund wurde nach Updates gesucht" } diff --git a/lib/l10n/locales/app_en.arb b/lib/l10n/locales/app_en.arb index 4c649cd..7abba97 100644 --- a/lib/l10n/locales/app_en.arb +++ b/lib/l10n/locales/app_en.arb @@ -332,5 +332,6 @@ "ignore": "Ignore", "updatesExplanation": "From time to time the resources get updated: We release a new version of a worksheet or add a new translation.\nWe want you to not worry about that but be always ready to use the latest versions. Therefore the app can check for updates in the background and download them automatically if you want.", "letsGo": "Let's go!", - "homeExplanation": "God is building His kingdom all around the world. He wants us to join in His work and make disciples!\nThis app wants to serve you and make your job easier: We equip you with good training worksheets. The best thing is: You can access the same worksheet in different languages so that you always know what it means, even if you don't understand the language.\n\nAll this content is now available offline, always ready on your phone:" + "homeExplanation": "God is building His kingdom all around the world. He wants us to join in His work and make disciples!\nThis app wants to serve you and make your job easier: We equip you with good training worksheets. The best thing is: You can access the same worksheet in different languages so that you always know what it means, even if you don't understand the language.\n\nAll this content is now available offline, always ready on your phone:", + "foundBgActivity": "Searched for updates in the background" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 84a6325..da36baa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,6 +17,11 @@ void main() async { sharedPrefsProvider.overrideWithValue(prefs), packageInfoProvider.overrideWithValue(packageInfo) ], child: const App4Training())); + +/* + await Workmanager().initialize(backgroundTask, isInDebugMode: false); + await Workmanager().registerOneOffTask("task-identifier", "simpleTask", + initialDelay: const Duration(seconds: 10));*/ } class App4Training extends ConsumerWidget { diff --git a/lib/routes/view_page.dart b/lib/routes/view_page.dart index b0923ba..c540d6c 100644 --- a/lib/routes/view_page.dart +++ b/lib/routes/view_page.dart @@ -1,6 +1,6 @@ +import 'package:app4training/background/background_result.dart'; import 'package:app4training/data/exceptions.dart'; import 'package:app4training/data/globals.dart'; -import 'package:app4training/data/updates.dart'; import 'package:app4training/l10n/l10n.dart'; import 'package:app4training/widgets/error_message.dart'; import 'package:app4training/widgets/html_view.dart'; @@ -18,40 +18,22 @@ class ViewPage extends ConsumerWidget { final String langCode; const ViewPage(this.page, this.langCode, {super.key}); - Future checkForBackgroundActivity(WidgetRef ref) async { - debugPrint("Checking for background activity"); - await ref.read(sharedPrefsProvider).reload(); - for (String languageCode in ref.read(availableLanguagesProvider)) { - // We don't check languages that are not downloaded - if (!ref.read(languageProvider(languageCode)).downloaded) continue; - - DateTime lcTimestampOrig = - ref.read(languageStatusProvider(languageCode)).lastCheckedTimestamp; - DateTime? lcTimestamp; - String? lcRaw = - ref.read(sharedPrefsProvider).getString('lastChecked-$languageCode'); - if (lcRaw != null) { - try { - lcTimestamp = DateTime.parse(lcRaw).toUtc(); - } on FormatException { - debugPrint( - 'Error while trying to parse lastChecked timestamp: $lcRaw'); - lcTimestamp = null; - } - } - if ((lcTimestamp != null) && - (lcTimestamp.compareTo(DateTime.now()) <= 0) && - lcTimestamp.compareTo(lcTimestampOrig) > 0) { - // It looks like there has been background activity! - debugPrint( - 'Background activity detected: lastChecked was $lcTimestampOrig, sharedPrefs says $lcTimestamp'); - } else { - debugPrint( - 'No background activity. lastChecked: $lcTimestampOrig, sharedPrefs says $lcRaw'); - } + /// First check whether the background process did something since + /// the last time we checked. + /// Then load the pageContent + Future checkAndLoad(BuildContext context, WidgetRef ref) async { + // Get l10n now as we can't access context after async gap later + AppLocalizations l10n = context.l10n; + final foundActivity = + await ref.read(backgroundResultProvider.notifier).checkForActivity(); + debugPrint("backgroundActivity: $foundActivity"); + if (foundActivity) { + ref + .watch(scaffoldMessengerProvider) + .showSnackBar(SnackBar(content: Text(l10n.foundBgActivity))); } - debugPrint('Checking for background activity done'); - return "Test"; + return ref + .watch(pageContentProvider((name: page, langCode: langCode)).future); } @override @@ -63,11 +45,7 @@ class ViewPage extends ConsumerWidget { ), drawer: MainDrawer(page, langCode), body: FutureBuilder( - future: Future.wait([ - ref.watch( - pageContentProvider((name: page, langCode: langCode)).future), - checkForBackgroundActivity(ref) - ]), + future: checkAndLoad(context, ref), builder: (BuildContext context, AsyncSnapshot snapshot) { debugPrint(snapshot.connectionState.toString()); @@ -99,7 +77,7 @@ class ViewPage extends ConsumerWidget { return ErrorMessage(context.l10n.error, context.l10n.internalError(e.toString())); } else { - String content = snapshot.data[0]; + String content = snapshot.data; // Save the selected page to the SharedPreferences to continue here // in case the user closes the app ref.read(sharedPrefsProvider).setString('recentPage', page); diff --git a/test/background_result_test.dart b/test/background_result_test.dart new file mode 100644 index 0000000..dddc84c --- /dev/null +++ b/test/background_result_test.dart @@ -0,0 +1,85 @@ +import 'package:app4training/background/background_result.dart'; +import 'package:app4training/data/globals.dart'; +import 'package:app4training/data/languages.dart'; +import 'package:app4training/data/updates.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'languages_test.dart'; + +void main() { + test('No background activity', () async { + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + + final ref = ProviderContainer(overrides: [ + languageProvider.overrideWith(() => TestLanguageController()), + sharedPrefsProvider.overrideWith((ref) => prefs) + ]); + + expect(await ref.read(backgroundResultProvider.notifier).checkForActivity(), + false); + }); + + test('There was some background activity', () async { + final oldTime = DateTime(2023, 2, 2).toUtc(); + SharedPreferences.setMockInitialValues( + {'lastChecked-de': oldTime.toIso8601String()}); + final prefs = await SharedPreferences.getInstance(); + + final ref = ProviderContainer(overrides: [ + languageProvider.overrideWith(() => TestLanguageController()), + sharedPrefsProvider.overrideWith((ref) => prefs) + ]); + + expect(await ref.read(backgroundResultProvider.notifier).checkForActivity(), + false); + + // Now we change the lastChecked timestamp for German + final currentTime = DateTime.now().toUtc(); + await prefs.setString('lastChecked-de', currentTime.toIso8601String()); + expect(ref.read(languageStatusProvider('de')).lastCheckedTimestamp, + equals(oldTime)); + + expect(await ref.read(backgroundResultProvider.notifier).checkForActivity(), + true); + +// TODO: It would probably be good if checkForActivity also updates +// the language status providers if necessary +// expect(ref.read(languageStatusProvider('de')).lastCheckedTimestamp, +// equals(currentTime)); + }); + + test('Test edge case: invalid values', () async { + final oldTime = DateTime(2023, 2, 2).toUtc(); + SharedPreferences.setMockInitialValues( + {'lastChecked-de': oldTime.toIso8601String()}); + final prefs = await SharedPreferences.getInstance(); + + final ref = ProviderContainer(overrides: [ + languageProvider.overrideWith(() => TestLanguageController()), + sharedPrefsProvider.overrideWith((ref) => prefs) + ]); + + expect(await ref.read(backgroundResultProvider.notifier).checkForActivity(), + false); + + // Now we set the lastChecked timestamp to an invalid value + await prefs.setString('lastChecked-de', 'invalid'); + expect(await ref.read(backgroundResultProvider.notifier).checkForActivity(), + false); + + DateTime futureDate = DateTime.now().add(const Duration(days: 1)); + await prefs.setString('lastChecked-de', futureDate.toIso8601String()); + expect(await ref.read(backgroundResultProvider.notifier).checkForActivity(), + false); + + await prefs.setString('lastChecked-de', DateTime(2023).toIso8601String()); + expect(await ref.read(backgroundResultProvider.notifier).checkForActivity(), + true); + + expect(ref.read(languageStatusProvider('de')).lastCheckedTimestamp, + equals(oldTime)); + }); +} diff --git a/test/background_task_test.dart b/test/background_task_test.dart index 5da1ef6..6c67413 100644 --- a/test/background_task_test.dart +++ b/test/background_task_test.dart @@ -1,4 +1,4 @@ -import 'package:app4training/background_task.dart'; +import 'package:app4training/background/background_task.dart'; import 'package:app4training/data/globals.dart'; import 'package:app4training/data/languages.dart'; import 'package:app4training/data/updates.dart'; diff --git a/test/languages_test.dart b/test/languages_test.dart index 698f731..95bdf3b 100644 --- a/test/languages_test.dart +++ b/test/languages_test.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:app4training/background_test.dart'; +import 'package:app4training/background/background_test.dart'; import 'package:app4training/data/exceptions.dart'; import 'package:app4training/data/globals.dart'; import 'package:dio/dio.dart';