Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 3263 - new BackgroundTaskManager that always works #3339

Merged
merged 4 commits into from
Nov 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 42 additions & 38 deletions packages/smooth_app/lib/background/abstract_background_task.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
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/local_database.dart';
import 'package:smooth_app/generic_lib/duration_constants.dart';
import 'package:smooth_app/pages/product/common/product_refresher.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 +36,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 All @@ -72,34 +106,4 @@ abstract class AbstractBackgroundTask {
barcode: barcode,
localDatabase: localDatabase,
);

/// 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();
}
}
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