diff --git a/packages/smooth_app/android/app/src/main/AndroidManifest.xml b/packages/smooth_app/android/app/src/main/AndroidManifest.xml
index ba479267764..dee6e00c40f 100644
--- a/packages/smooth_app/android/app/src/main/AndroidManifest.xml
+++ b/packages/smooth_app/android/app/src/main/AndroidManifest.xml
@@ -23,6 +23,10 @@
+
+
+
+
debugPrint(
+ void notFound() => Logs.d(
'please download $url and put it in asset somewhere like $cachedFilenames');
Exception loadException() =>
diff --git a/packages/smooth_app/lib/data_models/continuous_scan_model.dart b/packages/smooth_app/lib/data_models/continuous_scan_model.dart
index 5f935c00b60..0cc1503e1bc 100644
--- a/packages/smooth_app/lib/data_models/continuous_scan_model.dart
+++ b/packages/smooth_app/lib/data_models/continuous_scan_model.dart
@@ -8,6 +8,7 @@ import 'package:smooth_app/database/dao_product.dart';
import 'package:smooth_app/database/dao_product_list.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/helpers/analytics_helper.dart';
+import 'package:smooth_app/services/smooth_services.dart';
enum ScannedProductState {
FOUND,
@@ -57,7 +58,7 @@ class ContinuousScanModel with ChangeNotifier {
}
return this;
} catch (e) {
- debugPrint('exception: $e');
+ Logs.e('Load database error', ex: e);
}
return null;
}
@@ -78,7 +79,7 @@ class ContinuousScanModel with ChangeNotifier {
}
return true;
} catch (e) {
- debugPrint('exception: $e');
+ Logs.e('Refresh database error', ex: e);
}
return false;
}
diff --git a/packages/smooth_app/lib/data_models/user_management_provider.dart b/packages/smooth_app/lib/data_models/user_management_provider.dart
index 5c8e48f9606..d807b767a70 100644
--- a/packages/smooth_app/lib/data_models/user_management_provider.dart
+++ b/packages/smooth_app/lib/data_models/user_management_provider.dart
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:openfoodfacts/utils/OpenFoodAPIConfiguration.dart';
import 'package:smooth_app/database/dao_secured_string.dart';
+import 'package:smooth_app/services/smooth_services.dart';
class UserManagementProvider with ChangeNotifier {
static const String _USER_ID = 'user_id';
@@ -48,7 +49,7 @@ class UserManagementProvider with ChangeNotifier {
/// manually overwritten from an external apk.
DaoSecuredString.remove(key: _USER_ID);
DaoSecuredString.remove(key: _PASSWORD);
- debugPrint('Credentials query failed, you have been logged out');
+ Logs.e('Credentials query failed, you have been logged out');
}
if (userId == null || password == null) {
diff --git a/packages/smooth_app/lib/database/dao_product_list.dart b/packages/smooth_app/lib/database/dao_product_list.dart
index 9ac44620218..a7fbec0448b 100644
--- a/packages/smooth_app/lib/database/dao_product_list.dart
+++ b/packages/smooth_app/lib/database/dao_product_list.dart
@@ -1,13 +1,13 @@
import 'dart:async';
import 'dart:convert';
-import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:openfoodfacts/model/Product.dart';
import 'package:smooth_app/data_models/product_list.dart';
import 'package:smooth_app/database/abstract_dao.dart';
import 'package:smooth_app/database/dao_product.dart';
import 'package:smooth_app/database/local_database.dart';
+import 'package:smooth_app/services/smooth_services.dart';
/// "Total size" fake value for lists that are not partial/paged.
const int _uselessTotalSizeValue = 0;
@@ -160,10 +160,10 @@ class DaoProductList extends AbstractDao {
barcodes.add(barcode);
products[barcode] = product;
} else {
- debugPrint('unexpected: unknown product for $barcode');
+ Logs.e('unexpected: unknown product for $barcode');
}
} catch (e) {
- debugPrint('unexpected: exception for product $barcode');
+ Logs.e('unexpected: unknown product for $barcode', ex: e);
}
}
productList.set(barcodes, products);
diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb
index 705d5659c29..f6c0dd6a43d 100644
--- a/packages/smooth_app/lib/l10n/app_en.arb
+++ b/packages/smooth_app/lib/l10n/app_en.arb
@@ -274,6 +274,8 @@
},
"support_join_slack": "Ask for help in our Slack channel",
"support_via_email": "Send us an e-mail",
+ "support_via_email_include_logs_dialog_title": "Send app logs?",
+ "support_via_email_include_logs_dialog_body": "Do you wish to include application logs in attachment to your email?",
"termsOfUse": "Terms of use",
"@termsOfUse": {},
"about_this_app": "About this app",
diff --git a/packages/smooth_app/lib/main.dart b/packages/smooth_app/lib/main.dart
index 34694409d50..2dee55b0cc5 100644
--- a/packages/smooth_app/lib/main.dart
+++ b/packages/smooth_app/lib/main.dart
@@ -23,6 +23,7 @@ import 'package:smooth_app/helpers/camera_helper.dart';
import 'package:smooth_app/helpers/data_importer/smooth_app_data_importer.dart';
import 'package:smooth_app/helpers/network_config.dart';
import 'package:smooth_app/pages/onboarding/onboarding_flow_navigator.dart';
+import 'package:smooth_app/services/smooth_services.dart';
import 'package:smooth_app/themes/smooth_theme.dart';
import 'package:smooth_app/themes/theme_provider.dart';
@@ -89,6 +90,7 @@ Future _init1() async {
return false;
}
+ await SmoothServices().init();
await setupAppNetworkConfig();
await UserManagementProvider.mountCredentials();
_userPreferences = await UserPreferences.getUserPreferences();
diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart
index 30f073b67ed..28d90828469 100644
--- a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart
+++ b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
+import 'package:flutter_email_sender/flutter_email_sender.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:mailto/mailto.dart';
import 'package:openfoodfacts/utils/OpenFoodAPIConfiguration.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/data_models/user_management_provider.dart';
@@ -20,7 +20,6 @@ import 'package:smooth_app/pages/preferences/user_preferences_page.dart';
import 'package:smooth_app/pages/preferences/user_preferences_widgets.dart';
import 'package:smooth_app/pages/product/common/product_query_page_helper.dart';
import 'package:smooth_app/pages/user_management/login_page.dart';
-import 'package:url_launcher/url_launcher.dart';
class UserPreferencesAccount extends AbstractUserPreferences {
UserPreferencesAccount({
@@ -295,12 +294,13 @@ class _UserPreferencesPageState extends State {
const UserPreferencesListItemDivider(),
ListTile(
onTap: () async {
- final Mailto mailtoLink = Mailto(
- to: ['contact@openfoodfacts.org'],
- subject: appLocalizations.email_subject_account_deletion,
+ final Email email = Email(
body: appLocalizations.email_body_account_deletion(userId),
+ subject: appLocalizations.email_subject_account_deletion,
+ recipients: ['contact@openfoodfacts.org'],
);
- await launchUrl(Uri.parse('$mailtoLink'));
+
+ await FlutterEmailSender.send(email);
},
title: Text(appLocalizations.account_delete),
leading: const Icon(Icons.delete),
diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_connect.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_connect.dart
index a06e9fcf6ef..c6b389db088 100644
--- a/packages/smooth_app/lib/pages/preferences/user_preferences_connect.dart
+++ b/packages/smooth_app/lib/pages/preferences/user_preferences_connect.dart
@@ -1,16 +1,17 @@
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_email_sender/flutter_email_sender.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_svg/flutter_svg.dart';
-import 'package:mailto/mailto.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:smooth_app/data_models/user_preferences.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
+import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart';
import 'package:smooth_app/helpers/launch_url_helper.dart';
import 'package:smooth_app/pages/preferences/abstract_user_preferences.dart';
import 'package:smooth_app/pages/preferences/user_preferences_list_tile.dart';
import 'package:smooth_app/pages/preferences/user_preferences_page.dart';
-import 'package:url_launcher/url_launcher.dart';
+import 'package:smooth_app/services/smooth_services.dart';
/// Display of "Connect" for the preferences page.
class UserPreferencesConnect extends AbstractUserPreferences {
@@ -79,13 +80,34 @@ class UserPreferencesConnect extends AbstractUserPreferences {
title: appLocalizations.support_via_email,
leading: UserPreferencesListTile.getTintedIcon(Icons.drafts, context),
onTap: () async {
- final Mailto mailtoLink = Mailto(
- to: ['contact@openfoodfacts.org'],
-// This shouldn't be translated as its a debug message to OpenFoodFacts
- subject: 'Smoothie help',
+ final bool? includeLogs = await showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return SmoothAlertDialog(
+ title: appLocalizations
+ .support_via_email_include_logs_dialog_title,
+ body: Text(
+ appLocalizations
+ .support_via_email_include_logs_dialog_body,
+ ),
+ close: true,
+ positiveAction: SmoothActionButton(
+ text: appLocalizations.yes,
+ onPressed: () => Navigator.of(context).pop(true)),
+ negativeAction: SmoothActionButton(
+ text: appLocalizations.no,
+ onPressed: () => Navigator.of(context).pop(false)),
+ );
+ });
+
+ final Email email = Email(
body: await _emailBody,
+ subject: 'Smoothie help',
+ recipients: ['contact@openfoodfacts.org'],
+ attachmentPaths: includeLogs == true ? Logs.logFilesPaths : null,
);
- await launchUrl(Uri.parse('$mailtoLink'));
+
+ await FlutterEmailSender.send(email);
},
),
];
diff --git a/packages/smooth_app/lib/pages/scan/camera_controller.dart b/packages/smooth_app/lib/pages/scan/camera_controller.dart
index cde71229a6f..ddc5e8b6c59 100644
--- a/packages/smooth_app/lib/pages/scan/camera_controller.dart
+++ b/packages/smooth_app/lib/pages/scan/camera_controller.dart
@@ -6,6 +6,7 @@ import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:smooth_app/data_models/user_preferences.dart';
+import 'package:smooth_app/services/smooth_services.dart';
/// A lifecycle-aware [CameraController]
/// On Android it supports pause/resume feed
@@ -68,9 +69,12 @@ class SmoothCameraController extends CameraController {
.onCameraClosing(cameraId)
.listen((CameraClosingEvent event) async {
value = value.markAsClosed();
+ Logs.d('Camera closed!');
});
_updateState(_CameraState.resumed);
+ } else {
+ Logs.w('Controller already initialized!');
}
}
@@ -124,7 +128,7 @@ class SmoothCameraController extends CameraController {
// The pause process can sometimes be too long, in that case, we just for
// it to be finished
_hasAPendingResume = true;
- debugPrint('Preview not paused, will be restarted later…');
+ Logs.d('Preview not paused, will be restarted later…');
return;
} else if (_state == _CameraState.paused) {
return resumePreview();
@@ -244,7 +248,7 @@ class SmoothCameraController extends CameraController {
void _updateState(_CameraState newState) {
if (newState != _state) {
_state = newState;
- debugPrint('New camera state = $_state');
+ Logs.d('New camera state = $_state');
// Notify the UI to ensure a setState is called
if (_state == _CameraState.resumed) {
diff --git a/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart b/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart
index 0ff1673f06d..ff9218fafe9 100644
--- a/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart
+++ b/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart
@@ -3,7 +3,6 @@ import 'dart:math' as math;
import 'package:audioplayers/audioplayers.dart';
import 'package:camera/camera.dart';
-import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
@@ -20,6 +19,7 @@ import 'package:smooth_app/pages/scan/camera_controller.dart';
import 'package:smooth_app/pages/scan/lifecycle_manager.dart';
import 'package:smooth_app/pages/scan/mkit_scan_helper.dart';
import 'package:smooth_app/pages/scan/scan_visor.dart';
+import 'package:smooth_app/services/smooth_services.dart';
import 'package:smooth_app/widgets/lifecycle_aware_widget.dart';
import 'package:smooth_app/widgets/screen_visibility.dart';
@@ -298,14 +298,10 @@ class MLKitScannerPageState extends LifecycleAwareState
});
}
} on CameraException catch (e) {
- if (kDebugMode) {
- // TODO(M123): Show error message
- debugPrint(e.toString());
- }
+ // TODO(M123): Show error message
+ Logs.d('On camera error', ex: e);
} on FlutterError catch (e) {
- if (kDebugMode) {
- debugPrint(e.toString());
- }
+ Logs.d('On camera (Flutter part) error', ex: e);
}
_redrawScreen();
@@ -338,7 +334,9 @@ class MLKitScannerPageState extends LifecycleAwareState
_stopImageStream();
} else {
// TODO(M123): Handle errors better
- debugPrint(_controller!.value.errorDescription);
+ Logs.e(
+ 'On camera controller error : ${_controller!.value.errorDescription}',
+ );
}
}
}
diff --git a/packages/smooth_app/lib/services/logs/fimber/fimber_helper.dart b/packages/smooth_app/lib/services/logs/fimber/fimber_helper.dart
new file mode 100644
index 00000000000..5f95b2c5dfe
--- /dev/null
+++ b/packages/smooth_app/lib/services/logs/fimber/fimber_helper.dart
@@ -0,0 +1,23 @@
+import 'package:smooth_app/services/logs/smooth_log_levels.dart';
+
+extension LogLevelExtension on LogLevel {
+ String get fimberLevel {
+ switch (this) {
+ case LogLevel.verbose:
+ return 'V';
+ case LogLevel.debug:
+ return 'D';
+ case LogLevel.info:
+ return 'I';
+ case LogLevel.warning:
+ return 'W';
+ case LogLevel.error:
+ return 'E';
+ }
+ }
+}
+
+extension LogLevelsExtension on Iterable {
+ List get fimberLevels =>
+ map((LogLevel level) => level.fimberLevel).toList(growable: false);
+}
diff --git a/packages/smooth_app/lib/services/logs/fimber/trees/base_fimber_tree.dart b/packages/smooth_app/lib/services/logs/fimber/trees/base_fimber_tree.dart
new file mode 100644
index 00000000000..710efd9b1f3
--- /dev/null
+++ b/packages/smooth_app/lib/services/logs/fimber/trees/base_fimber_tree.dart
@@ -0,0 +1,17 @@
+import 'package:fimber/fimber.dart';
+import 'package:smooth_app/services/logs/fimber/fimber_helper.dart';
+import 'package:smooth_app/services/logs/smooth_log_levels.dart';
+
+abstract class BaseFimberTree extends LogTree {
+ BaseFimberTree({required List logLevels})
+ : assert(logLevels.isNotEmpty),
+ _logLevels = logLevels
+ .map((LogLevel level) => level.fimberLevel)
+ .toList(growable: false),
+ super();
+
+ final List _logLevels;
+
+ @override
+ List getLevels() => _logLevels;
+}
diff --git a/packages/smooth_app/lib/services/logs/fimber/trees/debug_fimber_tree.dart b/packages/smooth_app/lib/services/logs/fimber/trees/debug_fimber_tree.dart
new file mode 100644
index 00000000000..ea81dfdea54
--- /dev/null
+++ b/packages/smooth_app/lib/services/logs/fimber/trees/debug_fimber_tree.dart
@@ -0,0 +1,13 @@
+import 'package:fimber/fimber.dart';
+import 'package:smooth_app/services/logs/fimber/fimber_helper.dart';
+import 'package:smooth_app/services/logs/smooth_log_levels.dart';
+
+class DebugFimberTree extends DebugTree {
+ DebugFimberTree({required List logLevels})
+ : assert(logLevels.isNotEmpty),
+ super(
+ logLevels: logLevels
+ .map((LogLevel level) => level.fimberLevel)
+ .toList(growable: false),
+ );
+}
diff --git a/packages/smooth_app/lib/services/logs/fimber/trees/file_fimber_tree.dart b/packages/smooth_app/lib/services/logs/fimber/trees/file_fimber_tree.dart
new file mode 100644
index 00000000000..adf99af1292
--- /dev/null
+++ b/packages/smooth_app/lib/services/logs/fimber/trees/file_fimber_tree.dart
@@ -0,0 +1,68 @@
+import 'dart:io';
+
+import 'package:fimber/fimber.dart';
+import 'package:smooth_app/services/logs/fimber/trees/base_fimber_tree.dart';
+import 'package:smooth_app/services/logs/smooth_log_levels.dart';
+
+/// Single file fimber implementation
+/// When the maxDataSize is reached, half of the content is removed
+class FileFimberTree extends BaseFimberTree {
+ FileFimberTree({
+ required List logLevels,
+ required this.outputFile,
+ }) : super(logLevels: logLevels) {
+ outputFile.createSync();
+ }
+
+ static final int _maxFileSize = DataSize(megabytes: 5).realSize;
+ final File outputFile;
+
+ /// Generates the following String:
+ /// [level] [tag]: [message]
+ /// [ex]
+ /// [stacktrace]
+ @override
+ void log(
+ String level,
+ String message, {
+ String? tag,
+ dynamic ex,
+ StackTrace? stacktrace,
+ }) {
+ final StringBuffer buffer = StringBuffer(level);
+
+ if (tag != null) {
+ buffer.write(' $tag');
+ }
+
+ buffer.writeln(':$message');
+ if (ex != null) {
+ buffer.writeln(ex);
+ }
+ if (stacktrace != null) {
+ buffer.writeln(stacktrace.toString());
+ }
+
+ _appendToFile(buffer.toString());
+ buffer.clear();
+ }
+
+ void _appendToFile(String content) {
+ // If adding the new line exceeds the max length, we remove half of
+ // the content
+ if (outputFile.lengthSync() + content.length > _maxFileSize) {
+ final List lines = outputFile.readAsLinesSync();
+ lines.add(content);
+
+ outputFile.writeAsStringSync(
+ lines.sublist((lines.length / 2).round()).join('\n'),
+ mode: FileMode.writeOnly,
+ );
+ } else {
+ outputFile.writeAsStringSync(
+ content,
+ mode: FileMode.writeOnlyAppend,
+ );
+ }
+ }
+}
diff --git a/packages/smooth_app/lib/services/logs/fimber/trees/sentry_fimber_tree.dart b/packages/smooth_app/lib/services/logs/fimber/trees/sentry_fimber_tree.dart
new file mode 100644
index 00000000000..f99785e1b51
--- /dev/null
+++ b/packages/smooth_app/lib/services/logs/fimber/trees/sentry_fimber_tree.dart
@@ -0,0 +1,49 @@
+import 'package:fimber/fimber.dart';
+import 'package:sentry_flutter/sentry_flutter.dart';
+import 'package:smooth_app/services/logs/fimber/trees/base_fimber_tree.dart';
+import 'package:smooth_app/services/logs/smooth_log_levels.dart';
+
+/// Custom Fimber [LogTree] that send logs to Sentry
+class SentryFimberTree extends BaseFimberTree {
+ SentryFimberTree({
+ required List logLevels,
+ }) : super(logLevels: logLevels);
+
+ @override
+ void log(
+ String level,
+ String message, {
+ dynamic ex,
+ StackTrace? stacktrace,
+ String? tag,
+ }) {
+ if (ex != null) {
+ Sentry.captureException(
+ ex,
+ stackTrace: stacktrace,
+ hint: tag,
+ );
+ } else {
+ Sentry.captureMessage(
+ message,
+ level: _convertLevel(level),
+ hint: tag,
+ );
+ }
+ }
+
+ SentryLevel _convertLevel(String fimberLevel) {
+ switch (fimberLevel) {
+ case 'D':
+ return SentryLevel.debug;
+ case 'W':
+ return SentryLevel.warning;
+ case 'E':
+ return SentryLevel.error;
+ case 'V':
+ case 'I':
+ default:
+ return SentryLevel.info;
+ }
+ }
+}
diff --git a/packages/smooth_app/lib/services/logs/logs_fimber_impl.dart b/packages/smooth_app/lib/services/logs/logs_fimber_impl.dart
new file mode 100644
index 00000000000..05ee4b48070
--- /dev/null
+++ b/packages/smooth_app/lib/services/logs/logs_fimber_impl.dart
@@ -0,0 +1,118 @@
+import 'dart:io';
+
+import 'package:fimber/fimber.dart';
+import 'package:flutter/foundation.dart';
+import 'package:path/path.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:smooth_app/services/logs/fimber/fimber_helper.dart';
+import 'package:smooth_app/services/logs/fimber/trees/debug_fimber_tree.dart';
+import 'package:smooth_app/services/logs/fimber/trees/file_fimber_tree.dart';
+import 'package:smooth_app/services/logs/fimber/trees/sentry_fimber_tree.dart';
+import 'package:smooth_app/services/logs/smooth_log_levels.dart';
+import 'package:smooth_app/services/logs/smooth_logs_service.dart';
+
+/// On debug builds: device logs + file (all levels)
+/// On release builds : device logs (only errors), sentry (all levels) and
+/// file (only errors & info)
+class FimberLogImpl implements AppLogService {
+ // Link to a file tree impl (required for exporting logs)
+ late FileFimberTree _fileTree;
+
+ @override
+ Future init() async {
+ if (kReleaseMode) {
+ Fimber.plantTree(
+ DebugTree(
+ logLevels: ['E'],
+ ),
+ );
+ _fileTree = FileFimberTree(
+ outputFile: await _fileName,
+ logLevels: LogLevels.prodLogLevels,
+ );
+ Fimber.plantTree(
+ SentryFimberTree(logLevels: LogLevels.allLogLevels),
+ );
+ } else {
+ Fimber.plantTree(
+ DebugFimberTree(
+ logLevels: LogLevels.allLogLevels,
+ ),
+ );
+
+ _fileTree = FileFimberTree(
+ outputFile: await _fileName,
+ logLevels: LogLevels.allLogLevels,
+ );
+ }
+
+ Fimber.plantTree(_fileTree);
+ }
+
+ Future get _fileName => _filesDirectory
+ .then((Directory dir) => File(join(dir.absolute.path, 'app_logs.log')));
+
+ Future get _filesDirectory => getApplicationSupportDirectory()
+ .then((Directory dir) => Directory(join(dir.absolute.path, 'logs')))
+ .then((Directory dir) => dir.create(recursive: true));
+
+ @override
+ void log(
+ LogLevel level,
+ String message, {
+ String? tag,
+ dynamic ex,
+ StackTrace? stacktrace,
+ }) {
+ Fimber.log(
+ _getFimberLogLevel(level),
+ message,
+ tag: tag ?? _generateTag(),
+ ex: ex,
+ stacktrace: stacktrace,
+ );
+ }
+
+ @override
+ void d(String message, {String? tag, dynamic ex, StackTrace? stacktrace}) {
+ Fimber.log('D', message,
+ tag: tag ?? _generateTag(), ex: ex, stacktrace: stacktrace);
+ }
+
+ @override
+ void e(String message, {String? tag, dynamic ex, StackTrace? stacktrace}) {
+ Fimber.log('E', message,
+ tag: tag ?? _generateTag(), ex: ex, stacktrace: stacktrace);
+ }
+
+ @override
+ void i(String message, {String? tag, dynamic ex, StackTrace? stacktrace}) {
+ Fimber.log('I', message,
+ tag: tag ?? _generateTag(), ex: ex, stacktrace: stacktrace);
+ }
+
+ @override
+ void v(String message, {String? tag, dynamic ex, StackTrace? stacktrace}) {
+ Fimber.log('V', message,
+ tag: tag ?? _generateTag(), ex: ex, stacktrace: stacktrace);
+ }
+
+ @override
+ void w(String message, {String? tag, dynamic ex, StackTrace? stacktrace}) {
+ Fimber.log('W', message,
+ tag: tag ?? _generateTag(), ex: ex, stacktrace: stacktrace);
+ }
+
+ String _getFimberLogLevel(LogLevel level) => level.fimberLevel;
+
+ String _generateTag() {
+ return StackTrace.current.toString().split('\n')[4].split('.')[0];
+ }
+
+ @override
+ List get logFilesPaths {
+ return [
+ _fileTree.outputFile.absolute.path,
+ ];
+ }
+}
diff --git a/packages/smooth_app/lib/services/logs/smooth_log_levels.dart b/packages/smooth_app/lib/services/logs/smooth_log_levels.dart
new file mode 100644
index 00000000000..6c64a4bfa41
--- /dev/null
+++ b/packages/smooth_app/lib/services/logs/smooth_log_levels.dart
@@ -0,0 +1,21 @@
+enum LogLevel {
+ debug,
+ error,
+ info,
+ verbose,
+ warning,
+}
+
+class LogLevels {
+ static const List allLogLevels = [
+ LogLevel.error,
+ LogLevel.info,
+ LogLevel.debug,
+ LogLevel.verbose,
+ LogLevel.warning
+ ];
+ static const List prodLogLevels = [
+ LogLevel.error,
+ LogLevel.info
+ ];
+}
diff --git a/packages/smooth_app/lib/services/logs/smooth_logs_service.dart b/packages/smooth_app/lib/services/logs/smooth_logs_service.dart
new file mode 100644
index 00000000000..904e44cb7c3
--- /dev/null
+++ b/packages/smooth_app/lib/services/logs/smooth_logs_service.dart
@@ -0,0 +1,135 @@
+/// Please use this file for import statements, as it allows to easily change
+/// the logging solution
+import 'package:smooth_app/services/logs/smooth_log_levels.dart';
+import 'package:smooth_app/services/smooth_service.dart';
+
+class LogsService extends SmoothService {
+ LogsService() : super();
+
+ void log(
+ LogLevel level,
+ String message, {
+ String? tag,
+ dynamic ex,
+ StackTrace? stacktrace,
+ }) {
+ for (final AppLogService logService in impls) {
+ logService.log(level, message, tag: tag, ex: ex, stacktrace: stacktrace);
+ }
+ }
+
+ void d(
+ String message, {
+ String? tag,
+ dynamic ex,
+ StackTrace? stacktrace,
+ }) =>
+ log(
+ LogLevel.debug,
+ message,
+ tag: tag,
+ ex: ex,
+ stacktrace: stacktrace,
+ );
+
+ /// Write an error log
+ void e(
+ String message, {
+ String? tag,
+ dynamic ex,
+ StackTrace? stacktrace,
+ }) =>
+ log(
+ LogLevel.error,
+ message,
+ tag: tag,
+ ex: ex,
+ stacktrace: stacktrace,
+ );
+
+ /// Write an info log
+ void i(
+ String message, {
+ String? tag,
+ dynamic ex,
+ StackTrace? stacktrace,
+ }) =>
+ log(
+ LogLevel.info,
+ message,
+ tag: tag,
+ ex: ex,
+ stacktrace: stacktrace,
+ );
+
+ /// Write a verbose log
+ void v(
+ String message, {
+ String? tag,
+ dynamic ex,
+ StackTrace? stacktrace,
+ }) =>
+ log(
+ LogLevel.info,
+ message,
+ tag: tag,
+ ex: ex,
+ stacktrace: stacktrace,
+ );
+
+ /// Write a warning log
+ void w(
+ String message, {
+ String? tag,
+ dynamic ex,
+ StackTrace? stacktrace,
+ }) =>
+ log(
+ LogLevel.info,
+ message,
+ tag: tag,
+ ex: ex,
+ stacktrace: stacktrace,
+ );
+
+ List get logFilesPaths {
+ final List files = [];
+
+ for (final AppLogService logService in impls) {
+ files.addAll(logService.logFilesPaths);
+ }
+
+ return files;
+ }
+}
+
+abstract class AppLogService implements SmoothServiceImpl {
+ @override
+ Future init();
+
+ /// Write a debug log
+ void d(String message, {String? tag, dynamic ex, StackTrace? stacktrace});
+
+ /// Write an error log
+ void e(String message, {String? tag, dynamic ex, StackTrace? stacktrace});
+
+ /// Write an info log
+ void i(String message, {String? tag, dynamic ex, StackTrace? stacktrace});
+
+ /// Write a verbose log
+ void v(String message, {String? tag, dynamic ex, StackTrace? stacktrace});
+
+ /// Write a warning log
+ void w(String message, {String? tag, dynamic ex, StackTrace? stacktrace});
+
+ /// Write a debug log
+ void log(
+ LogLevel level,
+ String message, {
+ String? tag,
+ dynamic ex,
+ StackTrace? stacktrace,
+ });
+
+ List get logFilesPaths;
+}
diff --git a/packages/smooth_app/lib/services/smooth_service.dart b/packages/smooth_app/lib/services/smooth_service.dart
new file mode 100644
index 00000000000..5baa5d0e64e
--- /dev/null
+++ b/packages/smooth_app/lib/services/smooth_service.dart
@@ -0,0 +1,28 @@
+/// Generic interface for a service (eg: logger, analytics…) containing
+/// one or multiple implementations
+abstract class SmoothService {
+ SmoothService() : _impls = {};
+
+ final Set _impls;
+
+ Future attach(T impl) async {
+ if (!_impls.contains(impl)) {
+ _impls.add(impl);
+ await impl.init();
+ return true;
+ }
+
+ return false;
+ }
+
+ bool detach(T impl) {
+ return _impls.remove(impl);
+ }
+
+ Set get impls => _impls;
+}
+
+/// Generic interface for a service implementation
+abstract class SmoothServiceImpl {
+ Future init();
+}
diff --git a/packages/smooth_app/lib/services/smooth_services.dart b/packages/smooth_app/lib/services/smooth_services.dart
new file mode 100644
index 00000000000..bd9a8d31d67
--- /dev/null
+++ b/packages/smooth_app/lib/services/smooth_services.dart
@@ -0,0 +1,26 @@
+// ignore_for_file: non_constant_identifier_names
+
+import 'package:smooth_app/services/logs/logs_fimber_impl.dart';
+import 'package:smooth_app/services/logs/smooth_logs_service.dart';
+
+/// List of services (logs, analytics…) available in the app
+class SmoothServices {
+ factory SmoothServices() {
+ return _singleton;
+ }
+
+ SmoothServices._internal() {
+ _logsService = LogsService();
+ }
+
+ static final SmoothServices _singleton = SmoothServices._internal();
+ late LogsService _logsService;
+
+ Future init() {
+ return Future.wait(>[
+ _logsService.attach(FimberLogImpl()),
+ ]);
+ }
+}
+
+LogsService get Logs => SmoothServices()._logsService;
diff --git a/packages/smooth_app/pubspec.lock b/packages/smooth_app/pubspec.lock
index 127b5af1cae..f31a8d95615 100644
--- a/packages/smooth_app/pubspec.lock
+++ b/packages/smooth_app/pubspec.lock
@@ -322,6 +322,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.2"
+ fimber:
+ dependency: "direct main"
+ description:
+ name: fimber
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.6.6"
fixnum:
dependency: transitive
description:
@@ -339,6 +346,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ flutter_email_sender:
+ dependency: "direct main"
+ description:
+ name: flutter_email_sender
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "5.1.0"
flutter_isolate:
dependency: "direct main"
description:
@@ -651,13 +665,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
- mailto:
- dependency: "direct main"
- description:
- name: mailto
- url: "https://pub.dartlang.org"
- source: hosted
- version: "2.0.0"
matcher:
dependency: transitive
description:
diff --git a/packages/smooth_app/pubspec.yaml b/packages/smooth_app/pubspec.yaml
index e2c2c579045..5ead418e31f 100644
--- a/packages/smooth_app/pubspec.yaml
+++ b/packages/smooth_app/pubspec.yaml
@@ -61,7 +61,7 @@ dependencies:
audioplayers: ^1.0.0
percent_indicator: ^4.2.2
- mailto: ^2.0.0
+ flutter_email_sender: ^5.1.0
flutter_native_splash: ^2.2.3+1
google_mlkit_barcode_scanning: ^0.3.0
image_cropper: ^2.0.3
@@ -79,6 +79,7 @@ dependencies:
data_importer:
path: ../data_importer
share_plus: ^4.0.8
+ fimber: ^0.6.6
dev_dependencies:
integration_test: