Skip to content

Commit

Permalink
feature: 3263 - new BackgroundTaskManager that always works
Browse files Browse the repository at this point in the history
Deleted file:
* `background_task_helper.dart`

New files:
* `background_task_manager.dart`: Management of background tasks: single thread, block, restart, display.
* `dao_instant_string.dart`: Where we store strings that need INSTANT access (= not lazy, no await).

Impacted fles:
* `abstract_background_task.dart`: refactored
* `background_task_details.dart`: refactored around the changes in `AbstractBackgroundTask`
* `background_task_image.dart`: refactored around the changes in `AbstractBackgroundTask`
* `dao_string_list.dart`: refactoring around now managing several lists; removed unnecessary `await` for a non-lazy dao
* `local_database.dart`: added the new class `DaoInstantString`; relaunch the background task manager at every refresh
* `main.dart`: minor refactoring
* `new_crop_page.dart`: unrelated bug fix
* `offline_tasks_page.dart`: refactored around the new `BackgroundTaskManager`
* `operation_type.dart`: added helper methods
* `product_image_gallery_view.dart`: minor refactoring
* `product_image_viewer.dart`: unrelated bug fix - the product was not refreshed, and so wasn't the image even after a successful download
* `pubspec.lock`: wtf
* `pubspec.yaml`: removed `flutter_task_manager`
* `search_history_view.dart`: minor refactoring now that we have several lists in `DaoStringList`
* `search_page.dart`: minor refactoring now that we have several lists in `DaoStringList`
* `up_to_date_changes.dart`: minor refactoring
* `up_to_date_product_provider.dart`: minor refactoring
  • Loading branch information
monsieurtanuki committed Nov 21, 2022
1 parent a141bdf commit 0cce8be
Show file tree
Hide file tree
Showing 20 changed files with 429 additions and 432 deletions.
81 changes: 44 additions & 37 deletions packages/smooth_app/lib/background/abstract_background_task.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:openfoodfacts/utils/CountryHelper.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/background/background_task_details.dart';
import 'package:smooth_app/background/background_task_image.dart';
import 'package:smooth_app/background/background_task_manager.dart';
import 'package:smooth_app/database/dao_product.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/generic_lib/duration_constants.dart';
import 'package:smooth_app/query/product_query.dart';
import 'package:task_manager/task_manager.dart';

/// Abstract background task.
abstract class AbstractBackgroundTask {
Expand All @@ -34,29 +37,61 @@ abstract class AbstractBackgroundTask {
final String user;
final String country;

@protected
Map<String, dynamic> toJson();

/// Returns the deserialized background task if possible, or null.
static AbstractBackgroundTask? fromTask(final Task task) =>
BackgroundTaskDetails.fromTask(task) ??
BackgroundTaskImage.fromTask(task);
static AbstractBackgroundTask? fromJson(final Map<String, dynamic> map) =>
BackgroundTaskDetails.fromJson(map) ?? BackgroundTaskImage.fromJson(map);

/// Response code sent by the server in case of a success.
@protected
static const int SUCCESS_CODE = 1;

/// Executes the background task: upload, download, update locally.
Future<TaskResult> execute(final LocalDatabase localDatabase) async {
Future<void> execute(final LocalDatabase localDatabase) async {
await upload();
await _downloadAndRefresh(localDatabase);
return TaskResult.success;
}

/// Runs _instantly_ temporary code in order to "fake" the background task.
///
/// For instance, here we can pretend that we've changed the product name
/// by doing it locally, but the background task that talks to the server
/// is not even started.
Future<void> preExecute(final LocalDatabase localDatabase);

/// Cleans the temporary data changes performed in [preExecute].
Future<void> postExecute(final LocalDatabase localDatabase);

/// Uploads data changes.
@protected
Future<void> upload();

/// SnackBar message when we add the task, like "Added to the task queue!"
@protected
String getSnackBarMessage(final AppLocalizations appLocalizations);

/// Adds this task to the [BackgroundTaskManager].
@protected
Future<void> addToManager(final State<StatefulWidget> widget) async {
if (!widget.mounted) {
return;
}
final LocalDatabase localDatabase = widget.context.read<LocalDatabase>();
await BackgroundTaskManager(localDatabase).add(this);
if (!widget.mounted) {
return;
}
ScaffoldMessenger.of(widget.context).showSnackBar(
SnackBar(
content: Text(
getSnackBarMessage(AppLocalizations.of(widget.context)),
),
duration: SnackBarDuration.medium,
),
);
}

@protected
OpenFoodFactsLanguage getLanguage() => LanguageHelper.fromJson(languageCode);

Expand Down Expand Up @@ -84,37 +119,9 @@ abstract class AbstractBackgroundTask {
await daoProduct.put(product);
localDatabase.upToDate.setLatestDownloadedProduct(product);
localDatabase.notifyListeners();
return;
}
}
}

/// Generates a unique id for the background task.
///
/// This ensures that the background task is unique and also
/// ensures that in case of conflicts, the background task is replaced.
/// Example: 8901072002478_B_en_in_username
@protected
static String generateUniqueId(
String barcode,
String processIdentifier, {
final bool appendTimestamp = false,
}) {
final StringBuffer stringBuffer = StringBuffer();
stringBuffer
..write(barcode)
..write('_')
..write(processIdentifier)
..write('_')
..write(ProductQuery.getLanguage().code)
..write('_')
..write(ProductQuery.getCountry()!.iso2Code)
..write('_')
..write(ProductQuery.getUser().userId);
if (appendTimestamp) {
stringBuffer
..write('_')
..write(DateTime.now().millisecondsSinceEpoch);
}
return stringBuffer.toString();
throw Exception('Could not download product!');
}
}
58 changes: 24 additions & 34 deletions packages/smooth_app/lib/background/background_task_details.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:openfoodfacts/utils/CountryHelper.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/background/abstract_background_task.dart';
import 'package:smooth_app/data_models/operation_type.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/generic_lib/duration_constants.dart';
import 'package:smooth_app/query/product_query.dart';
import 'package:task_manager/task_manager.dart';

/// Background task that changes product details (data, but no image upload).
class BackgroundTaskDetails extends AbstractBackgroundTask {
Expand Down Expand Up @@ -36,6 +35,8 @@ class BackgroundTaskDetails extends AbstractBackgroundTask {
/// Task ID.
static const String _PROCESS_NAME = 'PRODUCT_EDIT';

static const OperationType _operationType = OperationType.details;

/// Serialized product.
final String inputMap;

Expand All @@ -51,10 +52,9 @@ class BackgroundTaskDetails extends AbstractBackgroundTask {
};

/// Returns the deserialized background task if possible, or null.
static AbstractBackgroundTask? fromTask(final Task task) {
static BackgroundTaskDetails? fromJson(final Map<String, dynamic> map) {
try {
final AbstractBackgroundTask result =
BackgroundTaskDetails._fromJson(task.data!);
final BackgroundTaskDetails result = BackgroundTaskDetails._fromJson(map);
if (result.processName == _PROCESS_NAME) {
return result;
}
Expand All @@ -65,44 +65,34 @@ class BackgroundTaskDetails extends AbstractBackgroundTask {
}

@override
Future<TaskResult> execute(final LocalDatabase localDatabase) async {
try {
await super.execute(localDatabase);
} catch (e) {
//
} finally {
Future<void> preExecute(final LocalDatabase localDatabase) async =>
localDatabase.upToDate.addChange(uniqueId, _product);

@override
Future<void> postExecute(final LocalDatabase localDatabase) async =>
localDatabase.upToDate.terminate(uniqueId);
}
return TaskResult.success;
}

/// Adds the background task about changing a product.
static Future<void> addTask(
final Product minimalistProduct, {
required final State<StatefulWidget> widget,
}) async {
final LocalDatabase localDatabase = widget.context.read<LocalDatabase>();
final String uniqueId =
await localDatabase.upToDate.addChange(minimalistProduct);
final BackgroundTaskDetails backgroundTask = _getNewTask(
final String uniqueId = await _operationType.getNewKey(
localDatabase,
minimalistProduct.barcode!,
);
final AbstractBackgroundTask task = _getNewTask(
minimalistProduct,
uniqueId,
);
// TODO(monsieurtanuki): currently we run the task immediately and just once - if it fails we rollback the changes.
backgroundTask.execute(localDatabase); // async
if (!widget.mounted) {
return;
}
ScaffoldMessenger.of(widget.context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(widget.context).product_task_background_schedule,
),
duration: SnackBarDuration.medium,
),
);
await task.addToManager(widget);
}

@override
String getSnackBarMessage(final AppLocalizations appLocalizations) =>
appLocalizations.product_task_background_schedule;

/// Returns a new background task about changing a product.
static BackgroundTaskDetails _getNewTask(
final Product minimalistProduct,
Expand All @@ -118,16 +108,16 @@ class BackgroundTaskDetails extends AbstractBackgroundTask {
country: ProductQuery.getCountry()!.iso2Code,
);

Product get _product =>
Product.fromJson(json.decode(inputMap) as Map<String, dynamic>);

/// Uploads the product changes.
@override
Future<void> upload() async {
final Map<String, dynamic> productMap =
json.decode(inputMap) as Map<String, dynamic>;

// TODO(AshAman999): check returned Status
await OpenFoodAPIClient.saveProduct(
getUser(),
Product.fromJson(productMap),
_product,
language: getLanguage(),
country: getCountry(),
);
Expand Down
88 changes: 45 additions & 43 deletions packages/smooth_app/lib/background/background_task_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:openfoodfacts/utils/CountryHelper.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/background/abstract_background_task.dart';
import 'package:smooth_app/data_models/operation_type.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/database/transient_file.dart';
import 'package:smooth_app/generic_lib/duration_constants.dart';
import 'package:smooth_app/query/product_query.dart';
import 'package:task_manager/task_manager.dart';

/// Background task about product image upload.
class BackgroundTaskImage extends AbstractBackgroundTask {
Expand Down Expand Up @@ -41,6 +40,8 @@ class BackgroundTaskImage extends AbstractBackgroundTask {
/// Task ID.
static const String _PROCESS_NAME = 'IMAGE_UPLOAD';

static const OperationType _operationType = OperationType.image;

final String imageField;
final String imagePath;

Expand All @@ -57,10 +58,9 @@ class BackgroundTaskImage extends AbstractBackgroundTask {
};

/// Returns the deserialized background task if possible, or null.
static AbstractBackgroundTask? fromTask(final Task task) {
static AbstractBackgroundTask? fromJson(final Map<String, dynamic> map) {
try {
final AbstractBackgroundTask result =
BackgroundTaskImage._fromJson(task.data!);
final AbstractBackgroundTask result = BackgroundTaskImage._fromJson(map);
if (result.processName == _PROCESS_NAME) {
return result;
}
Expand All @@ -78,55 +78,57 @@ class BackgroundTaskImage extends AbstractBackgroundTask {
required final State<StatefulWidget> widget,
}) async {
final LocalDatabase localDatabase = widget.context.read<LocalDatabase>();
// For "OTHER" images we randomize the id with timestamp
// so that it runs separately.
final String uniqueId = AbstractBackgroundTask.generateUniqueId(
final String uniqueId = await _operationType.getNewKey(
localDatabase,
barcode,
imageField.value,
appendTimestamp: imageField == ImageField.OTHER,
);
TransientFile.putImage(imageField, barcode, localDatabase, imageFile);
final BackgroundTaskImage backgroundTask = BackgroundTaskImage._(
uniqueId: uniqueId,
barcode: barcode,
processName: _PROCESS_NAME,
imageField: imageField.value,
imagePath: imageFile.path,
languageCode: ProductQuery.getLanguage().code,
user: jsonEncode(ProductQuery.getUser().toJson()),
country: ProductQuery.getCountry()!.iso2Code,
);
// TODO(monsieurtanuki): currently we run the task immediately and just once - if it fails we rollback the changes.
backgroundTask.execute(localDatabase); // async
if (!widget.mounted) {
return;
}
ScaffoldMessenger.of(widget.context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(widget.context).image_upload_queued,
),
duration: SnackBarDuration.medium,
),
final AbstractBackgroundTask task = _getNewTask(
barcode,
imageField,
imageFile,
uniqueId,
);
await task.addToManager(widget);
}

@override
Future<TaskResult> execute(final LocalDatabase localDatabase) async {
try {
await super.execute(localDatabase);
} catch (e) {
//
} finally {
String getSnackBarMessage(final AppLocalizations appLocalizations) =>
appLocalizations.image_upload_queued;

/// Returns a new background task about changing a product.
static BackgroundTaskImage _getNewTask(
final String barcode,
final ImageField imageField,
final File imageFile,
final String uniqueId,
) =>
BackgroundTaskImage._(
uniqueId: uniqueId,
barcode: barcode,
processName: _PROCESS_NAME,
imageField: imageField.value,
imagePath: imageFile.path,
languageCode: ProductQuery.getLanguage().code,
user: jsonEncode(ProductQuery.getUser().toJson()),
country: ProductQuery.getCountry()!.iso2Code,
);

@override
Future<void> preExecute(final LocalDatabase localDatabase) async =>
TransientFile.putImage(
ImageFieldExtension.getType(imageField),
barcode,
localDatabase,
File(imagePath),
);

@override
Future<void> postExecute(final LocalDatabase localDatabase) async =>
TransientFile.removeImage(
ImageFieldExtension.getType(imageField),
barcode,
localDatabase,
);
localDatabase.notifyListeners();
}
return TaskResult.success;
}

/// Uploads the product image.
@override
Expand Down
Loading

0 comments on commit 0cce8be

Please sign in to comment.