Skip to content

Commit

Permalink
refactor: background tasks with classes (openfoodfacts#2994)
Browse files Browse the repository at this point in the history
New files:
* `abstract_background_task.dart`: Abstract background task.
* `background_task_details.dart`: Background task that changes product details (data, but no image upload).
* `background_task_image.dart`: Background task about product image upload.

Impacted files:
* `add_basic_details_page.dart`: refactored the call to background task; minor refactoring
* `background_task_helper.dart`: moved most of the code to new classes `AbstractBackgroundTask` and offsprings.
* `edit_ingredients_page.dart`: refactored the call to background task
* `nutrition_page_loaded.dart`: refactored the call to background task
* `picture_capture_helper.dart`: refactored the call to background task
* `simple_input_page.dart`: refactored the call to background task
* `simple_input_page_helpers.dart`: added and implemented method `getTask`
  • Loading branch information
monsieurtanuki authored Sep 10, 2022
1 parent d2f8077 commit 68b6939
Show file tree
Hide file tree
Showing 10 changed files with 455 additions and 392 deletions.
84 changes: 84 additions & 0 deletions packages/smooth_app/lib/background/abstract_background_task.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:openfoodfacts/utils/CountryHelper.dart';
import 'package:smooth_app/background/background_task_details.dart';
import 'package:smooth_app/background/background_task_image.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/query/product_query.dart';
import 'package:task_manager/task_manager.dart';

/// Abstract background task.
abstract class AbstractBackgroundTask {
const AbstractBackgroundTask({
required this.processName,
required this.uniqueId,
required this.barcode,
required this.languageCode,
required this.user,
required this.country,
});

/// Typically, similar to the name of the class that extends this one.
///
/// To be used when deserializing, in order to check who is who.
final String processName;

/// Unique task identifier, needed e.g. for task overwriting.
final String uniqueId;

final String barcode;
final String languageCode;
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);

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

/// Executes the background task.
Future<TaskResult> execute(final LocalDatabase localDatabase);

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

@protected
OpenFoodFactsCountry? getCountry() => CountryHelper.fromJson(country);

/// 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();
}
}
171 changes: 171 additions & 0 deletions packages/smooth_app/lib/background/background_task_details.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import 'dart:convert';

import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:openfoodfacts/utils/CountryHelper.dart';
import 'package:smooth_app/background/abstract_background_task.dart';
import 'package:smooth_app/database/dao_product.dart';
import 'package:smooth_app/database/local_database.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 {
const BackgroundTaskDetails._({
required super.processName,
required super.uniqueId,
required super.barcode,
required super.languageCode,
required super.user,
required super.country,
required this.inputMap,
});

BackgroundTaskDetails._fromJson(Map<String, dynamic> json)
: this._(
processName: json['processName'] as String,
uniqueId: json['uniqueId'] as String,
barcode: json['barcode'] as String,
languageCode: json['languageCode'] as String,
user: json['user'] as String,
country: json['country'] as String,
inputMap: json['inputMap'] as String,
);

/// Task ID.
static const String _PROCESS_NAME = 'PRODUCT_EDIT';

/// Serialized product.
final String inputMap;

@override
Map<String, dynamic> toJson() => <String, dynamic>{
'processName': processName,
'uniqueId': uniqueId,
'barcode': barcode,
'languageCode': languageCode,
'user': user,
'country': country,
'inputMap': inputMap,
};

/// Returns the deserialized background task if possible, or null.
static AbstractBackgroundTask? fromTask(final Task task) {
try {
final AbstractBackgroundTask result =
BackgroundTaskDetails._fromJson(task.data!);
if (result.processName == _PROCESS_NAME) {
return result;
}
} catch (e) {
//
}
return null;
}

/// Adds the background task about changing a product.
///
/// Either [productEditTask] or [productEditTasks] must be populated;
/// we need that for classification purpose (and unique id computation).
static Future<void> addTask(
final Product minimalistProduct, {
final List<ProductEditTask>? productEditTasks,
final ProductEditTask? productEditTask,
}) async {
final String code;
if (productEditTask != null) {
if (productEditTasks != null) {
throw Exception();
}
code = productEditTask.code;
} else {
if (productEditTasks == null || productEditTasks.isEmpty) {
throw Exception();
}
final StringBuffer buffer = StringBuffer();
for (final ProductEditTask task in productEditTasks) {
buffer.write(task.code);
}
code = buffer.toString();
}
final String uniqueId = AbstractBackgroundTask.generateUniqueId(
minimalistProduct.barcode!,
code,
);
final BackgroundTaskDetails backgroundTask = BackgroundTaskDetails._(
uniqueId: uniqueId,
processName: _PROCESS_NAME,
barcode: minimalistProduct.barcode!,
languageCode: ProductQuery.getLanguage().code,
inputMap: jsonEncode(minimalistProduct.toJson()),
user: jsonEncode(ProductQuery.getUser().toJson()),
country: ProductQuery.getCountry()!.iso2Code,
);
await TaskManager().addTask(
Task(
data: backgroundTask.toJson(),
uniqueId: uniqueId,
),
);
}

/// Uploads the product changes, downloads the whole product, updates locally.
@override
Future<TaskResult> execute(final LocalDatabase localDatabase) async {
final Map<String, dynamic> productMap =
json.decode(inputMap) as Map<String, dynamic>;
final User user =
User.fromJson(jsonDecode(this.user) as Map<String, dynamic>);

await OpenFoodAPIClient.saveProduct(
user,
Product.fromJson(productMap),
language: getLanguage(),
country: getCountry(),
);

final DaoProduct daoProduct = DaoProduct(localDatabase);
final ProductQueryConfiguration configuration = ProductQueryConfiguration(
barcode,
fields: ProductQuery.fields,
language: getLanguage(),
country: getCountry(),
);

final ProductResult queryResult =
await OpenFoodAPIClient.getProduct(configuration);

if (queryResult.status == AbstractBackgroundTask.SUCCESS_CODE) {
final Product? product = queryResult.product;
if (product != null) {
await daoProduct.put(product);
localDatabase.notifyListeners();
}
}

// Returns true to let platform know that the task is completed
return TaskResult.success;
}
}

/// Product edit single tasks.
///
/// Used for classification (and unique id computation).
enum ProductEditTask {
nutrition('N'),
packaging('P'),
ingredient('I'),
basic('B'),
store('S'),
origin('O'),
emb('E'),
label('L'),
category('K'),
country('C');

const ProductEditTask(this.code);

/// Code used to distinguish the tasks.
///
/// Of course there shouldn't be duplicates.
final String code;
}
137 changes: 137 additions & 0 deletions packages/smooth_app/lib/background/background_task_image.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import 'dart:convert';
import 'dart:io';

import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:openfoodfacts/utils/CountryHelper.dart';
import 'package:smooth_app/background/abstract_background_task.dart';
import 'package:smooth_app/database/dao_product.dart';
import 'package:smooth_app/database/local_database.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 {
const BackgroundTaskImage._({
required super.processName,
required super.uniqueId,
required super.barcode,
required super.languageCode,
required super.user,
required super.country,
required this.imageField,
required this.imagePath,
});

BackgroundTaskImage._fromJson(Map<String, dynamic> json)
: this._(
processName: json['processName'] as String,
uniqueId: json['uniqueId'] as String,
barcode: json['barcode'] as String,
languageCode: json['languageCode'] as String,
user: json['user'] as String,
country: json['country'] as String,
imageField: json['imageField'] as String,
imagePath: json['imagePath'] as String,
);

/// Task ID.
static const String _PROCESS_NAME = 'IMAGE_UPLOAD';

final String imageField;
final String imagePath;

@override
Map<String, dynamic> toJson() => <String, dynamic>{
'processName': processName,
'uniqueId': uniqueId,
'barcode': barcode,
'languageCode': languageCode,
'user': user,
'country': country,
'imageField': imageField,
'imagePath': imagePath,
};

/// Returns the deserialized background task if possible, or null.
static AbstractBackgroundTask? fromTask(final Task task) {
try {
final AbstractBackgroundTask result =
BackgroundTaskImage._fromJson(task.data!);
if (result.processName == _PROCESS_NAME) {
return result;
}
} catch (e) {
//
}
return null;
}

/// Adds the background task about uploading a product image.
static Future<void> addTask(
final String barcode, {
required final ImageField imageField,
required final File imageFile,
}) async {
// For "OTHER" images we randomize the id with timestamp
// so that it runs separately.
final String uniqueId = AbstractBackgroundTask.generateUniqueId(
barcode,
imageField.value,
appendTimestamp: imageField == ImageField.OTHER,
);
final BackgroundTaskImage backgroundImageInputData = 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,
);
await TaskManager().addTask(
Task(
data: backgroundImageInputData.toJson(),
uniqueId: uniqueId,
),
);
}

/// Uploads the product image, downloads the whole product, updates locally.
@override
Future<TaskResult> execute(final LocalDatabase localDatabase) async {
final User user =
User.fromJson(jsonDecode(this.user) as Map<String, dynamic>);

final SendImage image = SendImage(
lang: getLanguage(),
barcode: barcode,
imageField: ImageFieldExtension.getType(imageField),
imageUri: Uri.parse(imagePath),
);

await OpenFoodAPIClient.addProductImage(user, image);

// Go to the file system and delete the file that was uploaded
File(imagePath).deleteSync();
final DaoProduct daoProduct = DaoProduct(localDatabase);
final ProductQueryConfiguration configuration = ProductQueryConfiguration(
barcode,
fields: ProductQuery.fields,
language: getLanguage(),
country: getCountry(),
);

final ProductResult queryResult =
await OpenFoodAPIClient.getProduct(configuration);
if (queryResult.status == AbstractBackgroundTask.SUCCESS_CODE) {
final Product? product = queryResult.product;
if (product != null) {
await daoProduct.put(product);
localDatabase.notifyListeners();
}
}

return TaskResult.success;
}
}
Loading

0 comments on commit 68b6939

Please sign in to comment.