Skip to content

Commit

Permalink
New: backgroundResultProvider checks bg activity
Browse files Browse the repository at this point in the history
This introduces a (currently still basic) BackgroundResult class and a
notifier that has checkForActivity() to check whether the background
task had been active since the last time we checked.
Currently this is done in ViewPage whenever the user opens some
worksheet. Shows a snackbar when activity was detected.
  • Loading branch information
holybiber committed Apr 10, 2024
1 parent b391a1f commit 704a5e1
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 59 deletions.
21 changes: 7 additions & 14 deletions integration_test/background_interaction_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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);
});
}

Expand Down
86 changes: 86 additions & 0 deletions lib/background/background_result.dart
Original file line number Diff line number Diff line change
@@ -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<BackgroundResult> {
@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<bool> 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<BackgroundResultNotifier, BackgroundResult>(() {
return BackgroundResultNotifier();
});
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
File renamed without changes.
3 changes: 2 additions & 1 deletion lib/l10n/locales/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
3 changes: 2 additions & 1 deletion lib/l10n/locales/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
5 changes: 5 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
58 changes: 18 additions & 40 deletions lib/routes/view_page.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -18,40 +18,22 @@ class ViewPage extends ConsumerWidget {
final String langCode;
const ViewPage(this.page, this.langCode, {super.key});

Future<String> 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<String> 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
Expand All @@ -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<dynamic> snapshot) {
debugPrint(snapshot.connectionState.toString());

Expand Down Expand Up @@ -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);
Expand Down
85 changes: 85 additions & 0 deletions test/background_result_test.dart
Original file line number Diff line number Diff line change
@@ -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));
});
}
2 changes: 1 addition & 1 deletion test/background_task_test.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 1 addition & 1 deletion test/languages_test.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down

0 comments on commit 704a5e1

Please sign in to comment.