diff --git a/core/lib/domain/exceptions/file_exception.dart b/core/lib/domain/exceptions/file_exception.dart new file mode 100644 index 0000000000..16e8980a86 --- /dev/null +++ b/core/lib/domain/exceptions/file_exception.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; + +abstract class FileException with EquatableMixin implements Exception { + final String message; + + FileException(this.message); + + @override + String toString() => message; + + @override + List get props => [message]; +} + +class NotFoundFileInFolderException extends FileException { + NotFoundFileInFolderException() : super('No files found in the folder'); +} + +class UserCancelShareFileException extends FileException { + UserCancelShareFileException() : super('User cancel share file'); +} \ No newline at end of file diff --git a/core/lib/presentation/views/loading/cupertino_loading_widget.dart b/core/lib/presentation/views/loading/cupertino_loading_widget.dart index 36544a955b..10ab0a7349 100644 --- a/core/lib/presentation/views/loading/cupertino_loading_widget.dart +++ b/core/lib/presentation/views/loading/cupertino_loading_widget.dart @@ -6,12 +6,14 @@ class CupertinoLoadingWidget extends StatelessWidget { final double? size; final EdgeInsetsGeometry? padding; final bool isCenter; + final Color? progressColor; const CupertinoLoadingWidget({ super.key, this.size, this.padding, this.isCenter = true, + this.progressColor, }); @override @@ -21,8 +23,8 @@ class CupertinoLoadingWidget extends StatelessWidget { child: SizedBox( width: size ?? CupertinoLoadingWidgetStyles.size, height: size ?? CupertinoLoadingWidgetStyles.size, - child: const CupertinoActivityIndicator( - color: CupertinoLoadingWidgetStyles.progressColor + child: CupertinoActivityIndicator( + color: progressColor ?? CupertinoLoadingWidgetStyles.progressColor ) ) ) @@ -31,8 +33,8 @@ class CupertinoLoadingWidget extends StatelessWidget { child: SizedBox( width: size ?? CupertinoLoadingWidgetStyles.size, height: size ?? CupertinoLoadingWidgetStyles.size, - child: const CupertinoActivityIndicator( - color: CupertinoLoadingWidgetStyles.progressColor + child: CupertinoActivityIndicator( + color: progressColor ?? CupertinoLoadingWidgetStyles.progressColor ) ), ); diff --git a/core/lib/utils/app_logger.dart b/core/lib/utils/app_logger.dart index 7664a65b18..35ca521c8d 100644 --- a/core/lib/utils/app_logger.dart +++ b/core/lib/utils/app_logger.dart @@ -1,13 +1,14 @@ import 'dart:async'; +import 'package:core/utils/log_tracking.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; final logHistory = _Dispatcher(''); -void log(String? value, {Level level = Level.info}) { - if (!kDebugMode) return; +Future log(String? value, {Level level = Level.info}) async { + if (!kDebugMode && !LogTracking().enableTraceLog) return; String logsStr = value ?? ''; logHistory.value = '$logsStr\n${logHistory.value}'; @@ -41,11 +42,16 @@ void log(String? value, {Level level = Level.info}) { break; } } + // ignore: avoid_print print('[TwakeMail] $logsStr'); + + if (LogTracking().enableTraceLog) { + await LogTracking().addLog(message: logsStr); + } } -void logError(String? value) => log(value, level: Level.error); +Future logError(String? value) => log(value, level: Level.error); // Take from: https://flutter.dev/docs/testing/errors void initLogger(VoidCallback runApp) { diff --git a/core/lib/utils/file_utils.dart b/core/lib/utils/file_utils.dart index f513fec900..0366fd6cf3 100644 --- a/core/lib/utils/file_utils.dart +++ b/core/lib/utils/file_utils.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:core/domain/exceptions/download_file_exception.dart'; +import 'package:core/domain/exceptions/file_exception.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/services.dart'; @@ -68,6 +69,18 @@ class FileUtils { } } + Future deleteFileByFolderName({required String nameFile, String? folderPath}) async { + final internalStorageDirPath = await _getInternalStorageDirPath( + nameFile: nameFile, + folderPath: folderPath); + + final file = File(internalStorageDirPath); + + if (await file.exists()) { + await file.delete(); + } + } + Future getContentFromFile({ required String nameFile, String? folderPath, @@ -121,4 +134,45 @@ class FileUtils { final base64Data = base64Encode(Uint8List.view(buffer)); return base64Data; } + + static Future getExternalDocumentPath({String? folderPath}) async { + Directory directory = Directory(''); + if (Platform.isAndroid) { + if (folderPath?.isNotEmpty == true) { + directory = Directory('/storage/emulated/0/Download/$folderPath'); + } else { + directory = Directory('/storage/emulated/0/Download'); + } + } else { + directory = await getApplicationDocumentsDirectory(); + if (folderPath?.isNotEmpty == true) { + directory = Directory('${directory.absolute.path}/$folderPath'); + } + } + + final exPath = directory.path; + log('FileUtils::getExternalDocumentPath:Saved Path: $exPath'); + await Directory(exPath).create(recursive: true); + return exPath; + } + + static Future copyInternalFilesToDownloadExternal(List listFilePaths) async { + final externalPath = await getExternalDocumentPath(); + + List externalListPaths = []; + for (var filePath in listFilePaths) { + final file = File(filePath); + final fileName = filePath.substring(filePath.lastIndexOf('/') + 1); + log('FileUtils::copyInternalFilesToDownloadExternal:filePath: $filePath | fileName: $fileName'); + final externalFile = File('$externalPath/$fileName'); + await externalFile.writeAsBytes(file.readAsBytesSync()); + externalListPaths.add(externalFile.path); + } + + if (externalListPaths.isNotEmpty) { + return externalPath; + } else { + throw NotFoundFileInFolderException(); + } + } } \ No newline at end of file diff --git a/core/lib/utils/html/html_utils.dart b/core/lib/utils/html/html_utils.dart index 828e3ac15c..63a212074f 100644 --- a/core/lib/utils/html/html_utils.dart +++ b/core/lib/utils/html/html_utils.dart @@ -30,7 +30,6 @@ class HtmlUtils { static const unregisterDropListener = ( script: ''' - console.log("unregisterDropListener"); const editor = document.querySelector(".note-editable"); const newEditor = editor.cloneNode(true); editor.parentNode.replaceChild(newEditor, editor);''', @@ -76,12 +75,12 @@ class HtmlUtils { required String base64Data, required String mimeType }) { + log('HtmlUtils::convertBase64ToImageResourceData:'); mimeType = validateHtmlImageResourceMimeType(mimeType); if (!base64Data.endsWith('==')) { base64Data.append('=='); } final imageResource = 'data:$mimeType;base64,$base64Data'; - log('HtmlUtils::convertBase64ToImageResourceData:imageResource: $imageResource'); return imageResource; } diff --git a/core/lib/utils/log_tracking.dart b/core/lib/utils/log_tracking.dart new file mode 100644 index 0000000000..ab03f0af37 --- /dev/null +++ b/core/lib/utils/log_tracking.dart @@ -0,0 +1,200 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:io'; + +import 'package:core/domain/exceptions/download_file_exception.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/file_utils.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:dartz/dartz.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:intl/intl.dart'; +import 'package:path_provider/path_provider.dart'; + +class LogTracking { + static const String logFolder = 'TraceLogs'; + static const String logFileNameDatePattern = 'yyyy-MM-dd'; + static const String logMessageDatePattern = 'yyyy-MM-dd, HH:mm:ss'; + + LogTracking._(); + + factory LogTracking() => _instance ??= LogTracking._(); + + static LogTracking? _instance; + + bool enableTraceLog = PlatformInfo.isMobile; + + final Queue _messagesQueue = Queue(); + bool _isScheduled = false; + + Future addLog({required String message}) async { + _messagesQueue.add(message); + + if (!_isScheduled) { + _isScheduled = true; + await _executeTraceLog(); + } + } + + Future _executeTraceLog() async { + while (true) { + try { + if (_messagesQueue.isEmpty) { + _isScheduled = false; + return; + } + + final message = _messagesQueue.removeFirst(); + await saveLog(message: message); + } catch (_) {} + } + } + + Future saveLog({required String message}) async { + try { + final currentDate = DateTime.timestamp(); + + final logFileName = generateLogFileName(currentDate: currentDate); + + final messageSanitized = sanitizeMessage( + message: message, + currentDate: currentDate); + + await saveToFile( + nameFile: logFileName, + folderPath: logFolder, + content: messageSanitized + ); + } catch (_) {} + } + + String sanitizeMessage({ + required String message, + required DateTime currentDate + }) { + final dateFormat = getDateFormatAsString( + pattern: logMessageDatePattern, + currentDate: currentDate); + + return '($dateFormat): $message \n'; + } + + String getDateFormatAsString({ + required String pattern, + required DateTime currentDate + }) { + final dateFormat = DateFormat(pattern); + return dateFormat.format(currentDate); + } + + String generateLogFileName({required DateTime currentDate}) { + final dateFormat = getDateFormatAsString( + pattern: logFileNameDatePattern, + currentDate: currentDate); + + return '${dateFormat}_log'; + } + + Future _getInternalStorageDirPath({ + String? nameFile, + String? folderPath, + String? extensionFile + }) async { + if (PlatformInfo.isMobile) { + String fileDirectory = (await getApplicationDocumentsDirectory()).absolute.path; + + if (folderPath != null) { + fileDirectory = '$fileDirectory/$folderPath'; + } + + Directory directory = Directory(fileDirectory); + + if (!await directory.exists()) { + await directory.create(recursive: true); + } + + if (nameFile != null) { + fileDirectory = '$fileDirectory/$nameFile'; + } + + if (extensionFile != null) { + fileDirectory = '$fileDirectory.$extensionFile'; + } + + return fileDirectory; + } else { + throw DeviceNotSupportedException(); + } + } + + Future saveToFile({ + required String nameFile, + required String content, + String? folderPath, + String? extensionFile, + FileMode fileMode = FileMode.append, + }) async { + final internalStorageDirPath = await _getInternalStorageDirPath( + nameFile: nameFile, + folderPath: folderPath, + extensionFile: extensionFile); + + final file = File(internalStorageDirPath); + + return await file.writeAsString(content, mode: fileMode); + } + + Future getTraceLog() async { + final folderPath = await _getInternalStorageDirPath(folderPath: logFolder); + log('LogTracking::getTraceLog:folderPath = $folderPath'); + final directory = Directory(folderPath); + if (directory.existsSync()) { + final directoryInfo = await getDirInfo(directory); + log('LogTracking::getTraceLog:directorySize = ${directoryInfo.value1}'); + log('LogTracking::getTraceLog:CountFile = ${directoryInfo.value2.length}'); + return TraceLog( + path: folderPath, + size: directoryInfo.value1, + listFilePaths: directoryInfo.value2); + } else { + throw Exception('Trace folder not exist'); + } + } + + Future>> getDirInfo(Directory dir) async { + var files = await dir.list(recursive: true).toList(); + var dirSize = files.fold(0, (int sum, file) => sum + file.statSync().size); + var listPath = files.map((file) => file.path).toList(); + return Tuple2(dirSize, listPath); + } + + Future exportTraceLog(TraceLog traceLog) async { + if (PlatformInfo.isIOS) { + final savePath = FileUtils.getExternalDocumentPath(folderPath: logFolder); + log('LogTracking::exportTraceLog:savePath = $savePath'); + return savePath; + } else { + final savePath = await compute( + FileUtils.copyInternalFilesToDownloadExternal, + traceLog.listFilePaths); + log('LogTracking::exportTraceLog:savePath = $savePath'); + return savePath; + } + } +} + +class TraceLog with EquatableMixin { + final String path; + final int size; + final List listFilePaths; + + TraceLog({ + required this.path, + required this.size, + required this.listFilePaths + }); + + @override + List get props => [path, size, listFilePaths]; +} \ No newline at end of file diff --git a/core/test/utils/log_tracking_test.dart b/core/test/utils/log_tracking_test.dart new file mode 100644 index 0000000000..1dc69b1791 --- /dev/null +++ b/core/test/utils/log_tracking_test.dart @@ -0,0 +1,86 @@ +@TestOn('vm') + +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/file_utils.dart'; +import 'package:core/utils/log_tracking.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; + +import 'file_utils_test.dart'; + +void main() { + final fileUtils = FileUtils(); + + group('log_tracking_test', () { + setUp(() async { + PathProviderPlatform.instance = FakePathProviderPlatform(); + }); + + test('app_logger::log:test', () async { + final fileName = LogTracking().generateLogFileName(currentDate: DateTime.timestamp()); + + await fileUtils.deleteFileByFolderName( + nameFile: fileName, + folderPath: LogTracking.logFolder); + + await Future.wait([ + log('hello 1'), + log('hello 2'), + log('hello 3'), + log('hello 4'), + ]); + + await log('hello 9'); + + await Future.wait([ + log('hello 5'), + log('hello 6'), + log('hello 7'), + log('hello 8'), + ]); + + final content = await fileUtils.getContentFromFile( + nameFile: fileName, + folderPath: LogTracking.logFolder); + + final listMessage= content + .split('\n') + .map((message) => message.trim()) + .where((message) => message.isNotEmpty); + + expect(listMessage.length, equals(9)); + expect(listMessage.first, contains('hello 1')); + expect(listMessage.last, contains('hello 8')); + }); + + test('app_logger::logError:test', () async { + final fileName = LogTracking().generateLogFileName(currentDate: DateTime.timestamp()); + + await fileUtils.deleteFileByFolderName( + nameFile: fileName, + folderPath: LogTracking.logFolder); + + await Future.wait([ + logError('Error 1'), + logError('Error 2'), + logError('Error 3'), + logError('Error 4'), + ]); + + await logError('Error 5'); + + final content = await fileUtils.getContentFromFile( + nameFile: fileName, + folderPath: LogTracking.logFolder); + + final listMessage= content + .split('\n') + .map((message) => message.trim()) + .where((message) => message.isNotEmpty); + + expect(listMessage.length, equals(5)); + expect(listMessage.first, contains('Error 1')); + expect(listMessage.last, contains('Error 5')); + }); + }); +} \ No newline at end of file diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index eacf5b8cb3..5954379146 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -153,7 +153,9 @@ abstract class BaseController extends GetxController return null; } - void handleErrorViewState(Object error, StackTrace stackTrace) {} + void handleErrorViewState(Object error, StackTrace stackTrace) { + logError('BaseController::handleErrorViewState: $error | $stackTrace'); + } void handleExceptionAction({Failure? failure, Exception? exception}) { logError('BaseController::handleExceptionAction():failure: $failure | exception: $exception'); @@ -167,9 +169,11 @@ abstract class BaseController extends GetxController } if (!authorizationInterceptors.isAppRunning) { + logError('BaseController::handleExceptionAction:isAppRunning = false'); return; } if (exception is BadCredentialsException || exception is ConnectionError) { + logError('BaseController::handleExceptionAction: exception is BadCredentialsException or ConnectionError'); clearDataAndGoToLoginPage(); } } @@ -203,24 +207,8 @@ abstract class BaseController extends GetxController } } - void startFpsMeter() { - FpsManager().start(); - fpsCallback = (fpsInfo) { - log('BaseController::startFpsMeter(): $fpsInfo'); - }; - if (fpsCallback != null) { - FpsManager().addFpsCallback(fpsCallback!); - } - } - - void stopFpsMeter() { - FpsManager().stop(); - if (fpsCallback != null) { - FpsManager().removeFpsCallback(fpsCallback!); - } - } - void injectAutoCompleteBindings(Session? session, AccountId? accountId) { + log('BaseController::injectAutoCompleteBindings:'); try { ContactAutoCompleteBindings().dependencies(); requireCapability(session!, accountId!, [tmailContactCapabilityIdentifier]); @@ -231,6 +219,7 @@ abstract class BaseController extends GetxController } void injectMdnBindings(Session? session, AccountId? accountId) { + log('BaseController::injectMdnBindings:'); try { requireCapability(session!, accountId!, [CapabilityIdentifier.jmapMdn]); MdnInteractorBindings().dependencies(); @@ -240,6 +229,7 @@ abstract class BaseController extends GetxController } void injectForwardBindings(Session? session, AccountId? accountId) { + log('BaseController::injectForwardBindings:'); try { requireCapability(session!, accountId!, [capabilityForward]); ForwardingInteractorsBindings().dependencies(); @@ -249,6 +239,7 @@ abstract class BaseController extends GetxController } void injectRuleFilterBindings(Session? session, AccountId? accountId) { + log('BaseController::injectRuleFilterBindings:'); try { requireCapability(session!, accountId!, [capabilityRuleFilter]); EmailRulesInteractorBindings().dependencies(); @@ -258,6 +249,7 @@ abstract class BaseController extends GetxController } Future injectFCMBindings(Session? session, AccountId? accountId) async { + log('BaseController::injectFCMBindings:'); try { requireCapability(session!, accountId!, [FirebaseCapability.fcmIdentifier]); log('BaseController::injectFCMBindings: fcmAvailable = ${AppConfig.fcmAvailable}'); @@ -289,6 +281,7 @@ abstract class BaseController extends GetxController FirebaseCapability.fcmIdentifier.isSupported(session, accountId) && AppConfig.fcmAvailable; void goToLogin() { + log('BaseController::goToLogin:currentRoute = ${Get.currentRoute}'); if (Get.currentRoute != AppRoutes.login) { pushAndPopAll( AppRoutes.login, @@ -300,6 +293,7 @@ abstract class BaseController extends GetxController } void logout(Session? session, AccountId? accountId) async { + log('BaseController::logout:accountId = $accountId'); if (session == null || accountId == null) { await clearDataAndGoToLoginPage(); return; @@ -317,6 +311,7 @@ abstract class BaseController extends GetxController } void _destroyFirebaseRegistration(FirebaseRegistrationId firebaseRegistrationId) async { + log('BaseController::_destroyFirebaseRegistration:firebaseRegistrationId = $firebaseRegistrationId'); _destroyFirebaseRegistrationInteractor = getBinding(); if (_destroyFirebaseRegistrationInteractor != null) { consumeState(_destroyFirebaseRegistrationInteractor!.execute(firebaseRegistrationId)); @@ -326,6 +321,7 @@ abstract class BaseController extends GetxController } void _getStoredFirebaseRegistrationFromCache() async { + log('BaseController::_getStoredFirebaseRegistrationFromCache:'); _getStoredFirebaseRegistrationInteractor = getBinding(); if (_getStoredFirebaseRegistrationInteractor != null) { consumeState(_getStoredFirebaseRegistrationInteractor!.execute()); @@ -341,6 +337,7 @@ abstract class BaseController extends GetxController } Future clearAllData() async { + log('BaseController::clearAllData:'); if (isAuthenticatedWithOidc) { await _clearOidcAuthData(); } else { @@ -349,6 +346,7 @@ abstract class BaseController extends GetxController } Future _clearBasicAuthData() async { + log('BaseController::_clearBasicAuthData:'); await Future.wait([ deleteCredentialInteractor.execute(), cachingManager.clearAll(), @@ -363,6 +361,7 @@ abstract class BaseController extends GetxController } Future _clearOidcAuthData() async { + log('BaseController::_clearOidcAuthData:'); await Future.wait([ deleteAuthorityOidcInteractor.execute(), cachingManager.clearAll(), diff --git a/lib/features/base/reloadable/reloadable_controller.dart b/lib/features/base/reloadable/reloadable_controller.dart index 71ecafff7d..ffa7c12cd7 100644 --- a/lib/features/base/reloadable/reloadable_controller.dart +++ b/lib/features/base/reloadable/reloadable_controller.dart @@ -30,10 +30,10 @@ abstract class ReloadableController extends BaseController { @override void handleFailureViewState(Failure failure) { + logError('ReloadableController::handleFailureViewState(): failure: $failure'); if (failure is GetCredentialFailure || failure is GetStoredTokenOidcFailure || failure is GetAuthenticatedAccountFailure) { - log('ReloadableController::handleFailureViewState(): failure: $failure'); goToLogin(); } else if (failure is GetSessionFailure) { _handleGetSessionFailure(failure.exception); @@ -44,6 +44,7 @@ abstract class ReloadableController extends BaseController { @override void handleSuccessViewState(Success success) { + log('ReloadableController::handleSuccessViewState: ${success.runtimeType}'); if (success is GetCredentialViewState) { _handleGetCredentialSuccess(success); } else if (success is GetSessionSuccess) { @@ -59,14 +60,17 @@ abstract class ReloadableController extends BaseController { * trigger reload by getting Credential again then setting up Interceptor and retrieving session * */ void reload() { + log('ReloadableController::reload:'); getAuthenticatedAccountAction(); } void getAuthenticatedAccountAction() { + log('ReloadableController::getAuthenticatedAccountAction:'); consumeState(_getAuthenticatedAccountInteractor.execute()); } void _setUpInterceptors(GetCredentialViewState credentialViewState) { + log('ReloadableController::_setUpInterceptors:'); dynamicUrlInterceptors.setJmapUrl(credentialViewState.baseUrl.origin); dynamicUrlInterceptors.changeBaseUrl(credentialViewState.baseUrl.origin); authorizationInterceptors.setBasicAuthorization( @@ -80,15 +84,18 @@ abstract class ReloadableController extends BaseController { } void _handleGetCredentialSuccess(GetCredentialViewState credentialViewState) { + log('ReloadableController::_handleGetCredentialSuccess:'); _setUpInterceptors(credentialViewState); getSessionAction(); } void getSessionAction() { + log('ReloadableController::getSessionAction:'); consumeState(_getSessionInteractor.execute()); } void _handleGetSessionFailure(dynamic exception) { + logError('ReloadableController::_handleGetSessionFailure:exception = $exception'); if (currentContext != null && currentOverlayContext != null && exception !is BadCredentialsException) { appToast.showToastErrorMessage( currentOverlayContext!, @@ -99,6 +106,7 @@ abstract class ReloadableController extends BaseController { } void _handleGetSessionSuccess(GetSessionSuccess success) { + log('ReloadableController::_handleGetSessionSuccess:'); final session = success.session; final personalAccount = session.personalAccount; final apiUrl = session.getQualifiedApiUrl(baseUrl: dynamicUrlInterceptors.jmapUrl); @@ -114,11 +122,13 @@ abstract class ReloadableController extends BaseController { void handleReloaded(Session session) {} void _handleGetStoredTokenOIDCSuccess(GetStoredTokenOidcSuccess tokenOidcSuccess) { + log('ReloadableController::_handleGetStoredTokenOIDCSuccess:'); _setUpInterceptorsOidc(tokenOidcSuccess); getSessionAction(); } void _setUpInterceptorsOidc(GetStoredTokenOidcSuccess tokenOidcSuccess) { + log('ReloadableController::_setUpInterceptorsOidc:'); dynamicUrlInterceptors.setJmapUrl(tokenOidcSuccess.baseUrl.toString()); dynamicUrlInterceptors.changeBaseUrl(tokenOidcSuccess.baseUrl.toString()); authorizationInterceptors.setTokenAndAuthorityOidc( @@ -139,6 +149,7 @@ abstract class ReloadableController extends BaseController { } void updateAuthenticationAccount(Session session, AccountId accountId, UserName userName) { + log('ReloadableController::updateAuthenticationAccount:'); final apiUrl = session.getQualifiedApiUrl(baseUrl: dynamicUrlInterceptors.jmapUrl); if (apiUrl.isNotEmpty) { consumeState(_updateAuthenticationAccountInteractor.execute(accountId, apiUrl, userName)); diff --git a/lib/features/caching/caching_manager.dart b/lib/features/caching/caching_manager.dart index 0fb76098c3..c102cc93f6 100644 --- a/lib/features/caching/caching_manager.dart +++ b/lib/features/caching/caching_manager.dart @@ -63,6 +63,7 @@ class CachingManager { ); Future clearAll() async { + log('CachingManager::clearAll:'); await Future.wait([ _stateCacheClient.clearAllData(), _mailboxCacheClient.clearAllData(), @@ -85,6 +86,7 @@ class CachingManager { } Future clearData() async { + log('CachingManager::clearData:'); await Future.wait([ _stateCacheClient.clearAllData(), _mailboxCacheClient.clearAllData(), @@ -103,6 +105,7 @@ class CachingManager { } Future clearEmailCacheAndStateCacheByTupleKey(AccountId accountId, Session session) { + log('CachingManager::clearEmailCacheAndStateCacheByTupleKey:'); return Future.wait([ _stateCacheClient.deleteItem(StateType.email.getTupleKeyStored(accountId, session.username)), _emailCacheClient.clearAllData(), @@ -110,6 +113,7 @@ class CachingManager { } Future clearEmailCacheAndAllStateCache() { + log('CachingManager::clearEmailCacheAndAllStateCache:'); return Future.wait([ _stateCacheClient.clearAllData(), _emailCacheClient.clearAllData(), @@ -122,14 +126,17 @@ class CachingManager { } Future getLatestVersion() { + log('CachingManager::getLatestVersion:'); return _hiveCacheVersionClient.getLatestVersion(); } Future closeHive() async { + log('CachingManager::closeHive:'); return await HiveCacheConfig.instance.closeHive(); } Future clearAllFileInStorage() async { + log('CachingManager::clearAllFileInStorage:'); await Future.wait([ _fileUtils.removeFolder(CachingConstants.newEmailsContentFolderName), _fileUtils.removeFolder(CachingConstants.openedEmailContentFolderName), @@ -137,6 +144,7 @@ class CachingManager { } Future clearLoginRecentData() async { + log('CachingManager::clearLoginRecentData:'); await Future.wait([ _recentLoginUrlCacheClient.clearAllData(), _recentLoginUsernameCacheClient.clearAllData(), diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 1ae8c76274..c8183f1cf5 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1108,7 +1108,6 @@ class ComposerController extends BaseController with DragDropFileMixin { }) async { final newEmailBody = await _getContentInEditor(); final oldEmailBody = _initTextEditor ?? ''; - log('ComposerController::_validateEmailChange: newEmailBody = $newEmailBody | oldEmailBody = $oldEmailBody'); final isEmailBodyChanged = !oldEmailBody.trim().isSame(newEmailBody.trim()); final newEmailSubject = subjectEmail.value ?? ''; diff --git a/lib/features/email/data/local/html_analyzer.dart b/lib/features/email/data/local/html_analyzer.dart index 904c320a05..ba34a6cf25 100644 --- a/lib/features/email/data/local/html_analyzer.dart +++ b/lib/features/email/data/local/html_analyzer.dart @@ -139,13 +139,12 @@ class HtmlAnalyzer { } Future removeCollapsedExpandedSignatureEffect({required String emailContent}) async { - log('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect: BEFORE = $emailContent'); + log('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect'); final document = parse(emailContent); final signatureElements = document.querySelectorAll('div.tmail-signature'); await Future.wait(signatureElements.map((signatureTag) async { final signatureChildren = signatureTag.children; for (var child in signatureChildren) { - log('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect: CHILD = ${child.outerHtml}'); if (child.attributes['class']?.contains('tmail-signature-button') == true) { child.remove(); } else if (child.attributes['class']?.contains('tmail-signature-content') == true) { @@ -154,7 +153,6 @@ class HtmlAnalyzer { } })); final newContent = document.body?.innerHtml ?? emailContent; - log('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect: AFTER = $newContent'); return newContent; } } \ No newline at end of file diff --git a/lib/features/login/data/extensions/list_account_cache_extensions.dart b/lib/features/login/data/extensions/list_account_cache_extensions.dart index fa31cc2f4f..771b5396c1 100644 --- a/lib/features/login/data/extensions/list_account_cache_extensions.dart +++ b/lib/features/login/data/extensions/list_account_cache_extensions.dart @@ -7,10 +7,10 @@ extension ListAccountCacheExtension on List { List unselected() => map((account) => account.unselected()).toList(); List removeDuplicated() { + log('ListAccountCacheExtension::removeDuplicated:BEFORE: $this'); final listAccountId = map((account) => account.accountId).whereNotNull().toSet(); - log('ListAccountCacheExtension::removeDuplicated:listAccountId: $listAccountId'); retainWhere((account) => listAccountId.remove(account.accountId)); - log('ListAccountCacheExtension::removeDuplicated:listAccount: $this'); + log('ListAccountCacheExtension::removeDuplicated:AFTER: $this'); return this; } diff --git a/lib/features/login/data/local/account_cache_manager.dart b/lib/features/login/data/local/account_cache_manager.dart index 3d3d6a43e4..cf58cfd73b 100644 --- a/lib/features/login/data/local/account_cache_manager.dart +++ b/lib/features/login/data/local/account_cache_manager.dart @@ -35,6 +35,7 @@ class AccountCacheManager { .removeDuplicated() .whereNot((account) => account.accountId == newAccountCache.accountId) .toList(); + log('AccountCacheManager::setCurrentAccount:newAllAccounts = $newAllAccounts'); if (newAllAccounts.isNotEmpty) { await _accountCacheClient.clearAllData(); await _accountCacheClient.updateMultipleItem(newAllAccounts.toMap()); @@ -47,4 +48,6 @@ class AccountCacheManager { log('AccountCacheManager::deleteCurrentAccount(): $hashId'); return _accountCacheClient.deleteItem(hashId); } + + Future closeAccountHiveCacheBox() => _accountCacheClient.closeBox(); } \ No newline at end of file diff --git a/lib/features/login/data/local/authentication_info_cache_manager.dart b/lib/features/login/data/local/authentication_info_cache_manager.dart index a9089267f9..6b11d8451b 100644 --- a/lib/features/login/data/local/authentication_info_cache_manager.dart +++ b/lib/features/login/data/local/authentication_info_cache_manager.dart @@ -25,4 +25,6 @@ class AuthenticationInfoCacheManager { Future removeAuthenticationInfo() { return _authenticationInfoCacheClient.deleteItem(AuthenticationInfoCache.keyCacheValue); } + + Future closeAuthenticationInfoHiveCacheBox() => _authenticationInfoCacheClient.closeBox(); } \ No newline at end of file diff --git a/lib/features/login/data/local/token_oidc_cache_manager.dart b/lib/features/login/data/local/token_oidc_cache_manager.dart index b65b1523ea..0597cc6d65 100644 --- a/lib/features/login/data/local/token_oidc_cache_manager.dart +++ b/lib/features/login/data/local/token_oidc_cache_manager.dart @@ -21,15 +21,19 @@ class TokenOidcCacheManager { } Future persistOneTokenOidc(TokenOIDC tokenOIDC) async { - log('TokenOidcCacheManager::persistOneTokenOidc(): $tokenOIDC'); + log('TokenOidcCacheManager::persistOneTokenOidc(): TOKEN_ID_HASH = ${tokenOIDC.tokenIdHash}'); + log('TokenOidcCacheManager::persistOneTokenOidc(): EXPIRED_TIME = ${tokenOIDC.expiredTime}'); await _tokenOidcCacheClient.clearAllData(); - log('TokenOidcCacheManager::persistOneTokenOidc(): key: ${tokenOIDC.tokenId.uuid}'); - log('TokenOidcCacheManager::persistOneTokenOidc(): key\'s hash: ${tokenOIDC.tokenIdHash}'); - log('TokenOidcCacheManager::persistOneTokenOidc(): token: ${tokenOIDC.token}'); - await _tokenOidcCacheClient.insertItem(tokenOIDC.tokenIdHash, tokenOIDC.toTokenOidcCache()); + await _tokenOidcCacheClient.insertItem( + tokenOIDC.tokenIdHash, + tokenOIDC.toTokenOidcCache()); + log('TokenOidcCacheManager::persistOneTokenOidc: SUCCESS'); } Future deleteTokenOidc() async { + log('TokenOidcCacheManager::deleteTokenOidc:'); await _tokenOidcCacheClient.clearAllData(); } + + Future closeTokenOIDCHiveCacheBox() => _tokenOidcCacheClient.closeBox(); } \ No newline at end of file diff --git a/lib/features/login/data/network/interceptors/authorization_interceptors.dart b/lib/features/login/data/network/interceptors/authorization_interceptors.dart index 34d21b5db6..4d2f51f777 100644 --- a/lib/features/login/data/network/interceptors/authorization_interceptors.dart +++ b/lib/features/login/data/network/interceptors/authorization_interceptors.dart @@ -76,13 +76,15 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { case AuthenticationType.none: break; } - log('AuthorizationInterceptors::onRequest(): URL = ${options.uri} | HEADER = ${options.headers} | DATA = ${options.data} | METHOD = ${options.method}'); + log('AuthorizationInterceptors::onRequest(): URL = ${options.uri} | HEADER = ${options.headers}'); super.onRequest(options, handler); } @override void onError(DioError err, ErrorInterceptorHandler handler) async { - logError('AuthorizationInterceptors::onError(): TOKEN = ${_token?.expiredTime} | DIO_ERROR = $err | METHOD = ${err.requestOptions.method}'); + logError('AuthorizationInterceptors::onError(): DIO_ERROR = $err'); + log('AuthorizationInterceptors::onError(): TOKEN_ID_HASH = ${_token?.tokenIdHash}'); + log('AuthorizationInterceptors::onError(): EXPIRED_TIME = ${_token?.expiredTime}'); try { final requestOptions = err.requestOptions; final extraInRequest = requestOptions.extra; @@ -92,7 +94,7 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { responseStatusCode: err.response?.statusCode, tokenOIDC: _token )) { - log('AuthorizationInterceptors::onError:_validateToRefreshToken'); + log('AuthorizationInterceptors::onError: Expired tokens begin to get new tokens'); final newTokenOidc = PlatformInfo.isIOS ? await _handleRefreshTokenOnIOSPlatform() : await _handleRefreshTokenOnOtherPlatform(); @@ -177,6 +179,7 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { required int? responseStatusCode, required TokenOIDC? tokenOIDC }) { + log('AuthorizationInterceptors::validateToRefreshToken'); return responseStatusCode == 401 && _isAuthenticationOidcValid() && _isTokenNotEmpty(tokenOIDC) @@ -188,6 +191,7 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { required String? authHeader, required TokenOIDC? tokenOIDC }) { + log('AuthorizationInterceptors::validateToRetryTheRequestWithNewToken:authHeader = $authHeader'); return authHeader != null && _isTokenNotEmpty(tokenOIDC) && !_isTokenExpired(tokenOIDC) @@ -210,8 +214,9 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { } Future _updateCurrentAccount({required TokenOIDC tokenOIDC}) async { + log('AuthorizationInterceptors::_updateCurrentAccount:'); final currentAccount = await _accountCacheManager.getCurrentAccount(); - + log('AuthorizationInterceptors::_updateCurrentAccount:currentAccount = $currentAccount'); await _accountCacheManager.deleteCurrentAccount(currentAccount.id); await _tokenOidcCacheManager.persistOneTokenOidc(tokenOIDC); @@ -224,6 +229,7 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { apiUrl: currentAccount.apiUrl, userName: currentAccount.userName ); + log('AuthorizationInterceptors::_updateCurrentAccount:personalAccount = $personalAccount'); await _accountCacheManager.setCurrentAccount(personalAccount); return personalAccount; @@ -251,6 +257,7 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { } Future _invokeRefreshTokenFromServer() async { + log('AuthorizationInterceptors::_invokeRefreshTokenFromServer:'); final newToken = await _authenticationClient.refreshingTokensOIDC( _configOIDC!.clientId, _configOIDC!.redirectUrl, @@ -258,13 +265,17 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { _configOIDC!.scopes, _token!.refreshToken ); - log('AuthorizationInterceptors::_invokeRefreshTokenFromServer:newToken: $newToken'); + log('AuthorizationInterceptors::_invokeRefreshTokenFromServer: NEW_TOKEN_ID_HASH = ${newToken.tokenIdHash}'); + log('AuthorizationInterceptors::_invokeRefreshTokenFromServer: NEW_TOKEN_EXPIRED_TIME = ${newToken.expiredTime}'); + log('AuthorizationInterceptors::_invokeRefreshTokenFromServer: NEW_TOKEN_REFRESH_TOKEN = ${newToken.refreshToken}'); + log('AuthorizationInterceptors::_invokeRefreshTokenFromServer: NEW_TOKEN_ACCESS_TOKEN = ${newToken.token}'); return newToken; } Future _handleRefreshTokenOnIOSPlatform() async { + log('AuthorizationInterceptors::_handleRefreshTokenOnIOSPlatform:'); final keychainToken = await _getTokenInKeychain(_token!); - + log('AuthorizationInterceptors::_handleRefreshTokenOnIOSPlatform:keychainToken = $keychainToken'); if (keychainToken == null) { final newToken = await _invokeRefreshTokenFromServer(); final newAccount = await _updateCurrentAccount(tokenOIDC: newToken); @@ -277,12 +288,14 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { } Future _handleRefreshTokenOnOtherPlatform() async { + log('AuthorizationInterceptors::_handleRefreshTokenOnOtherPlatform:'); final newToken = await _invokeRefreshTokenFromServer(); await _updateCurrentAccount(tokenOIDC: newToken); return newToken; } void clear() { + log('AuthorizationInterceptors::clear:'); _authorization = null; _token = null; _configOIDC = null; diff --git a/lib/features/login/presentation/login_controller.dart b/lib/features/login/presentation/login_controller.dart index 561ad3448e..68ba1f4ed3 100644 --- a/lib/features/login/presentation/login_controller.dart +++ b/lib/features/login/presentation/login_controller.dart @@ -173,7 +173,7 @@ class LoginController extends ReloadableController { @override void handleExceptionAction({Failure? failure, Exception? exception}) { - logError('LoginController::handleExceptionAction:exception: $exception | failure: ${failure.runtimeType}'); + logError('LoginController::handleExceptionAction:exception: $exception | failure: $failure'); if (failure is CheckOIDCIsAvailableFailure || failure is GetStoredOidcConfigurationFailure || failure is GetOIDCConfigurationFailure || diff --git a/lib/features/mailbox/data/local/state_cache_manager.dart b/lib/features/mailbox/data/local/state_cache_manager.dart index f1dd4c356b..31409a174d 100644 --- a/lib/features/mailbox/data/local/state_cache_manager.dart +++ b/lib/features/mailbox/data/local/state_cache_manager.dart @@ -24,4 +24,6 @@ class StateCacheManager { final stateKey = TupleKey(stateCache.type.name, accountId.asString, userName.value).encodeKey; return await _stateCacheClient.insertItem(stateKey, stateCache); } + + Future closeStateHiveCacheBox() => _stateCacheClient.closeBox(); } \ No newline at end of file diff --git a/lib/features/manage_account/data/datasource/trace_log_datasource.dart b/lib/features/manage_account/data/datasource/trace_log_datasource.dart new file mode 100644 index 0000000000..595bd48c4d --- /dev/null +++ b/lib/features/manage_account/data/datasource/trace_log_datasource.dart @@ -0,0 +1,7 @@ +import 'package:core/utils/log_tracking.dart'; + +abstract class TraceLogDataSource { + Future getTraceLog(); + + Future exportTraceLog(TraceLog traceLog); +} \ No newline at end of file diff --git a/lib/features/manage_account/data/datasource_impl/trace_log_data_source_impl.dart b/lib/features/manage_account/data/datasource_impl/trace_log_data_source_impl.dart new file mode 100644 index 0000000000..e923539ba5 --- /dev/null +++ b/lib/features/manage_account/data/datasource_impl/trace_log_data_source_impl.dart @@ -0,0 +1,63 @@ +import 'package:core/data/utils/device_manager.dart'; +import 'package:core/domain/exceptions/file_exception.dart'; +import 'package:core/utils/log_tracking.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:tmail_ui_user/features/manage_account/data/datasource/trace_log_datasource.dart'; +import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; +import 'package:tmail_ui_user/main/exceptions/permission_exception.dart'; +import 'package:tmail_ui_user/main/permissions/permission_service.dart'; + +class TraceLogDataSourceImpl extends TraceLogDataSource { + + final LogTracking _logTracking; + final DeviceManager _deviceManager; + final PermissionService _permissionService; + final ExceptionThrower _exceptionThrower; + + TraceLogDataSourceImpl( + this._logTracking, + this._deviceManager, + this._permissionService, + this._exceptionThrower + ); + + @override + Future getTraceLog() { + return Future.sync(() async { + return await _logTracking.getTraceLog(); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future exportTraceLog(TraceLog traceLog) { + return Future.sync(() async { + if (PlatformInfo.isAndroid) { + final permissionGranted = await _validateStoragePermissionOnAndroid(); + if (permissionGranted) { + return await _logTracking.exportTraceLog(traceLog); + } else { + throw const NotGrantedPermissionStorageException(); + } + } else { + final savePath = await _logTracking.exportTraceLog(traceLog); + final result = await Share.shareXFiles([XFile(savePath)]); + if (result.status == ShareResultStatus.success) { + return savePath; + } + throw UserCancelShareFileException(); + } + }).catchError(_exceptionThrower.throwException); + } + + Future _validateStoragePermissionOnAndroid() async { + final needRequestPermission = await _deviceManager.isNeedRequestStoragePermissionOnAndroid(); + if (needRequestPermission) { + final isGranted = await _permissionService.isGranted(Permission.storage); + return isGranted; + } else { + return true; + } + } +} \ No newline at end of file diff --git a/lib/features/manage_account/data/repository/trace_log_repository_impl.dart b/lib/features/manage_account/data/repository/trace_log_repository_impl.dart new file mode 100644 index 0000000000..a7ae5b1068 --- /dev/null +++ b/lib/features/manage_account/data/repository/trace_log_repository_impl.dart @@ -0,0 +1,19 @@ +import 'package:core/utils/log_tracking.dart'; +import 'package:tmail_ui_user/features/manage_account/data/datasource/trace_log_datasource.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/repository/trace_log_repository.dart'; + +class TraceLogRepositoryImpl extends TraceLogRepository { + final TraceLogDataSource _traceLogDataSource; + + TraceLogRepositoryImpl(this._traceLogDataSource); + + @override + Future getTraceLog() { + return _traceLogDataSource.getTraceLog(); + } + + @override + Future exportTraceLog(TraceLog traceLog) { + return _traceLogDataSource.exportTraceLog(traceLog); + } +} \ No newline at end of file diff --git a/lib/features/manage_account/domain/repository/trace_log_repository.dart b/lib/features/manage_account/domain/repository/trace_log_repository.dart new file mode 100644 index 0000000000..261effe780 --- /dev/null +++ b/lib/features/manage_account/domain/repository/trace_log_repository.dart @@ -0,0 +1,7 @@ +import 'package:core/utils/log_tracking.dart'; + +abstract class TraceLogRepository { + Future getTraceLog(); + + Future exportTraceLog(TraceLog traceLog); +} \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/export_trace_log_state.dart b/lib/features/manage_account/domain/state/export_trace_log_state.dart new file mode 100644 index 0000000000..41f79aa4e3 --- /dev/null +++ b/lib/features/manage_account/domain/state/export_trace_log_state.dart @@ -0,0 +1,19 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class ExportTraceLogLoading extends LoadingState {} + +class ExportTraceLogSuccess extends UIState { + + String savePath; + + ExportTraceLogSuccess(this.savePath); + + @override + List get props => [savePath]; +} + +class ExportTraceLogFailure extends FeatureFailure { + + ExportTraceLogFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/get_trace_log_state.dart b/lib/features/manage_account/domain/state/get_trace_log_state.dart new file mode 100644 index 0000000000..86941f730c --- /dev/null +++ b/lib/features/manage_account/domain/state/get_trace_log_state.dart @@ -0,0 +1,19 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/log_tracking.dart'; + +class GetTraceLogLoading extends LoadingState {} + +class GetTraceLogSuccess extends UIState { + final TraceLog traceLog; + + GetTraceLogSuccess(this.traceLog); + + @override + List get props => [traceLog]; +} + +class GetTraceLogFailure extends FeatureFailure { + + GetTraceLogFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/manage_account/domain/usecases/export_trace_log_interactor.dart b/lib/features/manage_account/domain/usecases/export_trace_log_interactor.dart new file mode 100644 index 0000000000..6d4ca5623d --- /dev/null +++ b/lib/features/manage_account/domain/usecases/export_trace_log_interactor.dart @@ -0,0 +1,22 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/log_tracking.dart'; +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/repository/trace_log_repository.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/export_trace_log_state.dart'; + +class ExportTraceLogInteractor { + final TraceLogRepository _traceLogRepository; + + ExportTraceLogInteractor(this._traceLogRepository); + + Stream> execute(TraceLog traceLog) async* { + try { + yield Right(ExportTraceLogLoading()); + final savePath = await _traceLogRepository.exportTraceLog(traceLog); + yield Right(ExportTraceLogSuccess(savePath)); + } catch (exception) { + yield Left(ExportTraceLogFailure(exception)); + } + } +} \ No newline at end of file diff --git a/lib/features/manage_account/domain/usecases/get_trace_log_interactor.dart b/lib/features/manage_account/domain/usecases/get_trace_log_interactor.dart new file mode 100644 index 0000000000..8058fc449e --- /dev/null +++ b/lib/features/manage_account/domain/usecases/get_trace_log_interactor.dart @@ -0,0 +1,21 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/repository/trace_log_repository.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/get_trace_log_state.dart'; + +class GetTraceLogInteractor { + final TraceLogRepository _traceLogRepository; + + GetTraceLogInteractor(this._traceLogRepository); + + Stream> execute() async* { + try { + yield Right(GetTraceLogLoading()); + final response = await _traceLogRepository.getTraceLog(); + yield Right(GetTraceLogSuccess(response)); + } catch (exception) { + yield Left(GetTraceLogFailure(exception)); + } + } +} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart index acff137cbe..711e61c20f 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart @@ -31,6 +31,7 @@ import 'package:tmail_ui_user/features/manage_account/presentation/model/manage_ import 'package:tmail_ui_user/features/manage_account/presentation/model/settings_page_level.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/notification/bindings/notification_binding.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/profiles_bindings.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/trace_log/trace_log_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/vacation_controller_bindings.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -196,6 +197,9 @@ class ManageAccountDashBoardController extends ReloadableController { case AccountMenuItem.vacation: case AccountMenuItem.none: break; + case AccountMenuItem.traceLog: + TraceLogBindings().dependencies(); + break; } } diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart index 8d3a0096db..6bf2eefd9f 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart @@ -4,6 +4,7 @@ import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/views/image/avatar_builder.dart'; import 'package:core/presentation/views/responsive/responsive_widget.dart'; import 'package:core/presentation/views/text/slogan_builder.dart'; +import 'package:core/utils/log_tracking.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -24,6 +25,7 @@ import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings_utils.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/account_menu_item.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/profiles_view.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/trace_log/trace_log_view.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/vacation_view.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -191,6 +193,12 @@ class ManageAccountDashBoardView extends GetWidget { indent: SettingsUtils.getHorizontalPadding(context, controller.responsiveUtils), endIndent: SettingsUtils.getHorizontalPadding(context, controller.responsiveUtils) ), + if (PlatformInfo.isMobile) + Column( + children: [ + SettingFirstLevelTileBuilder( + AccountMenuItem.traceLog.getName(context), + AccountMenuItem.traceLog.getIcon(controller.imagePaths), + subtitle: AppLocalizations.of(context).traceLogSettingExplanation, + () => controller.selectSettings(AccountMenuItem.traceLog) + ), + Divider( + color: AppColor.colorDividerHorizontal, + height: 1, + indent: SettingsUtils.getHorizontalPadding(context, controller.responsiveUtils), + endIndent: SettingsUtils.getHorizontalPadding(context, controller.responsiveUtils) + ), + ] + ), SettingFirstLevelTileBuilder( AppLocalizations.of(context).sign_out, controller.imagePaths.icSignOut, diff --git a/lib/features/manage_account/presentation/menu/settings/settings_view.dart b/lib/features/manage_account/presentation/menu/settings/settings_view.dart index 81b433ac05..5331a0a20b 100644 --- a/lib/features/manage_account/presentation/menu/settings/settings_view.dart +++ b/lib/features/manage_account/presentation/menu/settings/settings_view.dart @@ -2,6 +2,7 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/utils/style_utils.dart'; import 'package:core/presentation/views/button/icon_button_web.dart'; import 'package:core/utils/direction_utils.dart'; +import 'package:core/utils/log_tracking.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -21,6 +22,7 @@ import 'package:tmail_ui_user/features/manage_account/presentation/model/account import 'package:tmail_ui_user/features/manage_account/presentation/model/settings_page_level.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/notification/notification_view.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/profiles_view.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/trace_log/trace_log_view.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/vacation_view.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -231,6 +233,12 @@ class SettingsView extends GetWidget { return MailboxVisibilityView(); case AccountMenuItem.notification: return const NotificationView(); + case AccountMenuItem.traceLog: + if (LogTracking().enableTraceLog) { + return const TraceLogView(); + } else { + return const SizedBox.shrink(); + } default: return const SizedBox.shrink(); } diff --git a/lib/features/manage_account/presentation/model/account_menu_item.dart b/lib/features/manage_account/presentation/model/account_menu_item.dart index e0915ee4f4..c97d9eea3a 100644 --- a/lib/features/manage_account/presentation/model/account_menu_item.dart +++ b/lib/features/manage_account/presentation/model/account_menu_item.dart @@ -1,5 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/resources/image_paths.dart'; import 'package:flutter/cupertino.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -12,6 +12,7 @@ enum AccountMenuItem { vacation, mailboxVisibility, notification, + traceLog, none; String getIcon(ImagePaths imagePaths) { @@ -34,6 +35,8 @@ enum AccountMenuItem { return imagePaths.icNotification; case AccountMenuItem.none: return imagePaths.icProfiles; + case AccountMenuItem.traceLog: + return imagePaths.icSetting; } } @@ -57,6 +60,8 @@ enum AccountMenuItem { return AppLocalizations.of(context).notification; case AccountMenuItem.none: return AppLocalizations.of(context).profiles; + case AccountMenuItem.traceLog: + return AppLocalizations.of(context).traceLog; } } @@ -80,6 +85,8 @@ enum AccountMenuItem { return 'notification'; case AccountMenuItem.none: return 'profiles'; + case AccountMenuItem.traceLog: + return 'trace-log'; } } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/trace_log/trace_log_bindings.dart b/lib/features/manage_account/presentation/trace_log/trace_log_bindings.dart new file mode 100644 index 0000000000..3af62ed6c3 --- /dev/null +++ b/lib/features/manage_account/presentation/trace_log/trace_log_bindings.dart @@ -0,0 +1,53 @@ +import 'package:core/core.dart'; +import 'package:core/data/utils/device_manager.dart'; +import 'package:core/utils/log_tracking.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/base_bindings.dart'; +import 'package:tmail_ui_user/features/manage_account/data/datasource/trace_log_datasource.dart'; +import 'package:tmail_ui_user/features/manage_account/data/datasource_impl/trace_log_data_source_impl.dart'; +import 'package:tmail_ui_user/features/manage_account/data/repository/trace_log_repository_impl.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/repository/trace_log_repository.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/export_trace_log_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_trace_log_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/trace_log/trace_log_controller.dart'; +import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; +import 'package:tmail_ui_user/main/permissions/permission_service.dart'; + +class TraceLogBindings extends BaseBindings { + @override + void bindingsController() { + Get.lazyPut(() => TraceLogController( + Get.find(), + Get.find())); + } + + @override + void bindingsDataSource() { + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsDataSourceImpl() { + Get.lazyPut(() => TraceLogDataSourceImpl( + LogTracking(), + Get.find(), + Get.find(), + Get.find())); + } + + @override + void bindingsInteractor() { + Get.lazyPut(() => GetTraceLogInteractor(Get.find())); + Get.lazyPut(() => ExportTraceLogInteractor(Get.find())); + } + + @override + void bindingsRepository() { + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsRepositoryImpl() { + Get.lazyPut(() => TraceLogRepositoryImpl(Get.find())); + } +} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/trace_log/trace_log_controller.dart b/lib/features/manage_account/presentation/trace_log/trace_log_controller.dart new file mode 100644 index 0000000000..56e437c9d7 --- /dev/null +++ b/lib/features/manage_account/presentation/trace_log/trace_log_controller.dart @@ -0,0 +1,92 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/log_tracking.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/base_controller.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/export_trace_log_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/get_trace_log_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/export_trace_log_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_trace_log_interactor.dart'; +import 'package:tmail_ui_user/main/exceptions/permission_exception.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +class TraceLogController extends BaseController { + + final GetTraceLogInteractor _getTraceLogInteractor; + final ExportTraceLogInteractor _exportTraceLogInteractor; + + TraceLogController( + this._getTraceLogInteractor, + this._exportTraceLogInteractor, + ); + + final tracLog = Rxn(); + + @override + void onInit() { + super.onInit(); + _getTraceLog(); + } + + @override + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is GetTraceLogSuccess) { + tracLog.value = success.traceLog; + } else if (success is ExportTraceLogSuccess) { + _handleExportTraceLogSuccess(success); + } + } + + @override + void handleFailureViewState(Failure failure) { + super.handleFailureViewState(failure); + if (failure is GetTraceLogFailure) { + if (currentContext != null && currentOverlayContext != null) { + appToast.showToastErrorMessage( + currentOverlayContext!, + failure.exception.toString()); + } + } else if (failure is ExportTraceLogFailure) { + _handleExportTraceLogFailure(failure); + } + } + + void _getTraceLog() { + consumeState(_getTraceLogInteractor.execute()); + } + + void exportTraceLogFile(TraceLog traceLog) { + consumeState(_exportTraceLogInteractor.execute(traceLog)); + } + + void _handleExportTraceLogSuccess(ExportTraceLogSuccess success) { + if (currentContext != null && currentOverlayContext != null) { + appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).exportTraceLogSuccess(success.savePath)); + } + } + + void _handleExportTraceLogFailure(ExportTraceLogFailure failure) { + if (failure.exception is NotGrantedPermissionStorageException) { + if (currentContext != null && currentOverlayContext != null) { + appToast.showToastWarningMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).youNeedToGrantFilesPermissionToExportFile); + } + } else { + if (currentContext != null && currentOverlayContext != null) { + appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).exportTraceLogFailed); + } + } + } + + bool get isExporting => viewState.value.fold( + (failure) => false, + (success) => success is ExportTraceLogLoading); + +} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/trace_log/trace_log_view.dart b/lib/features/manage_account/presentation/trace_log/trace_log_view.dart new file mode 100644 index 0000000000..ff07dda0df --- /dev/null +++ b/lib/features/manage_account/presentation/trace_log/trace_log_view.dart @@ -0,0 +1,99 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:core/presentation/views/container/tmail_container_widget.dart'; +import 'package:core/presentation/views/loading/cupertino_loading_widget.dart'; +import 'package:filesize/filesize.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings_utils.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/trace_log/trace_log_controller.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class TraceLogView extends GetWidget with AppLoaderMixin { + const TraceLogView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: SettingsUtils.getBackgroundColor(context, controller.responsiveUtils), + body: Container( + width: double.infinity, + height: double.infinity, + color: SettingsUtils.getContentBackgroundColor(context, controller.responsiveUtils), + decoration: SettingsUtils.getBoxDecorationForContent(context, controller.responsiveUtils), + margin: SettingsUtils.getMarginViewForSettingDetails(context, controller.responsiveUtils), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + AppLocalizations.of(context).traceLogSettingExplanation, + style: const TextStyle( + fontSize: 16, + height: 20 / 16, + color: AppColor.colorTextSettingDescriptions)), + const SizedBox(height: 24), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Text( + AppLocalizations.of(context).totalSize, + style: const TextStyle( + fontSize: 17, + height: 24 / 17, + fontWeight: FontWeight.w500, + color: Colors.black + ) + ), + const SizedBox(width: 16), + Expanded( + child: Obx(() => Text( + filesize(controller.tracLog.value?.size ?? 0), + style: const TextStyle( + fontSize: 17, + height: 24 / 17, + color: Colors.black + ) + )) + ) + ] + ), + const SizedBox(height: 16), + Obx(() { + if (controller.tracLog.value != null) { + if (controller.isExporting) { + return const TMailContainerWidget( + borderRadius: 10, + width: 60, + backgroundColor: AppColor.primaryColor, + child: CupertinoLoadingWidget(size: 16)); + } else { + return TMailButtonWidget( + text: AppLocalizations.of(context).exportFile, + backgroundColor: AppColor.primaryColor, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + textStyle: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + fontSize: 17 + ), + borderRadius: 10, + onTapActionCallback: () => controller.exportTraceLogFile(controller.tracLog.value!), + ); + } + } else { + return const SizedBox.shrink(); + } + }) + ], + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/push_notification/presentation/controller/fcm_message_controller.dart b/lib/features/push_notification/presentation/controller/fcm_message_controller.dart index 9c69a7153d..8030c04626 100644 --- a/lib/features/push_notification/presentation/controller/fcm_message_controller.dart +++ b/lib/features/push_notification/presentation/controller/fcm_message_controller.dart @@ -21,10 +21,14 @@ import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions import 'package:tmail_ui_user/features/home/domain/state/get_session_state.dart'; import 'package:tmail_ui_user/features/home/domain/usecases/get_session_interactor.dart'; import 'package:tmail_ui_user/features/home/presentation/home_bindings.dart'; +import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; +import 'package:tmail_ui_user/features/login/data/local/authentication_info_cache_manager.dart'; +import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_credential_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_stored_token_oidc_state.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/data/local/state_cache_manager.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/action/fcm_action.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart'; @@ -48,6 +52,10 @@ class FcmMessageController extends FcmBaseController { DynamicUrlInterceptors? _dynamicUrlInterceptors; AuthorizationInterceptors? _authorizationInterceptors; GetSessionInteractor? _getSessionInteractor; + AccountCacheManager? _accountCacheManager; + TokenOidcCacheManager? _tokenOidcCacheManager; + StateCacheManager? _stateCacheManager; + AuthenticationInfoCacheManager? _authenticationInfoCacheManager; FcmMessageController._internal(); @@ -99,7 +107,7 @@ class FcmMessageController extends FcmBaseController { } } - void _handleBackgroundMessageAction(Map payloadData) async { + Future _handleBackgroundMessageAction(Map payloadData) async { log('FcmMessageController::_handleBackgroundMessageAction():payloadData: $payloadData'); final stateChange = FcmUtils.instance.convertFirebaseDataMessageToStateChange(payloadData); await _initialAppConfig(); @@ -187,6 +195,17 @@ class FcmMessageController extends FcmBaseController { }); _getInteractorBindings(); + + await Future.wait([ + if (_accountCacheManager != null) + _accountCacheManager!.closeAccountHiveCacheBox(), + if (_tokenOidcCacheManager != null) + _tokenOidcCacheManager!.closeTokenOIDCHiveCacheBox(), + if (_stateCacheManager != null) + _stateCacheManager!.closeStateHiveCacheBox(), + if (_authenticationInfoCacheManager != null) + _authenticationInfoCacheManager!.closeAuthenticationInfoHiveCacheBox(), + ]); } void _getInteractorBindings() { @@ -194,6 +213,10 @@ class FcmMessageController extends FcmBaseController { _dynamicUrlInterceptors = getBinding(); _authorizationInterceptors = getBinding(); _getSessionInteractor = getBinding(); + _accountCacheManager = getBinding(); + _tokenOidcCacheManager = getBinding(); + _stateCacheManager = getBinding(); + _authenticationInfoCacheManager = getBinding(); FcmTokenController.instance.initialBindingInteractor(); } diff --git a/lib/main/bindings/core/core_bindings.dart b/lib/main/bindings/core/core_bindings.dart index fea26aee3c..33dbeda811 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -13,6 +13,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/utils/sending_queue_isolate_manager.dart'; +import 'package:tmail_ui_user/main/permissions/permission_service.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; import 'package:uuid/uuid.dart'; @@ -58,6 +59,7 @@ class CoreBindings extends Bindings { } void _bindingUtils() { + Get.put(PermissionService()); Get.put(const Uuid()); Get.put(CompressFileUtils()); Get.put(AppConfigLoader()); diff --git a/lib/main/exceptions/permission_exception.dart b/lib/main/exceptions/permission_exception.dart new file mode 100644 index 0000000000..c0b168e453 --- /dev/null +++ b/lib/main/exceptions/permission_exception.dart @@ -0,0 +1,16 @@ + +import 'package:equatable/equatable.dart'; + +abstract class PermissionException with EquatableMixin implements Exception { + + final String? message; + + const PermissionException({this.message}); +} + +class NotGrantedPermissionStorageException extends PermissionException { + const NotGrantedPermissionStorageException() : super(message: 'Permission Storage has not been granted access'); + + @override + List get props => [super.message]; +} \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 2f1733d719..108e4c5330 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4085,4 +4085,48 @@ class AppLocalizations { name: 'showNotifications', ); } + + String get traceLog { + return Intl.message( + 'Trace Log', + name: 'traceLog'); + } + + String get traceLogSettingExplanation { + return Intl.message( + 'Trace log to easily monitor log on mobile devices', + name: 'traceLogSettingExplanation'); + } + + String get totalSize { + return Intl.message( + 'Total Size', + name: 'totalSize'); + } + + String get exportFile { + return Intl.message( + 'Export file', + name: 'exportFile'); + } + + String get youNeedToGrantFilesPermissionToExportFile { + return Intl.message( + 'You need to grant files permission to export files', + name: 'youNeedToGrantFilesPermissionToExportFile' + ); + } + + String exportTraceLogSuccess(String path) { + return Intl.message( + 'Export successful tracking logs at "$path"', + name: 'exportTraceLogSuccess', + args: [path]); + } + + String get exportTraceLogFailed { + return Intl.message( + 'Export trace log failed', + name: 'exportTraceLogFailed'); + } } \ No newline at end of file diff --git a/lib/main/utils/ios_sharing_manager.dart b/lib/main/utils/ios_sharing_manager.dart index bb261bea1f..ee2a92df90 100644 --- a/lib/main/utils/ios_sharing_manager.dart +++ b/lib/main/utils/ios_sharing_manager.dart @@ -45,6 +45,7 @@ class IOSSharingManager { } Future saveKeyChainSharingSession(PersonalAccount personalAccount) async { + log('IOSSharingManager::saveKeyChainSharingSession:personalAccount = $personalAccount'); try { if (!_validateToSaveKeychain(personalAccount)) { logError('IOSSharingManager::saveKeyChainSharingSession: account is null');