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: