TF-4268 Cache Sentry config and user for FCM background re-initialization

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2026-04-09 16:22:37 +07:00
committed by Dat H. Pham
parent cb5d43630a
commit eeaab49e93
20 changed files with 755 additions and 183 deletions
@@ -6,6 +6,7 @@ import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart';
import 'package:core/utils/sentry/sentry_manager.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart';
@@ -13,6 +14,9 @@ import 'package:jmap_dart_client/jmap/push/state_change.dart';
import 'package:model/model.dart';
import 'package:rxdart/rxdart.dart';
import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart';
import 'package:tmail_ui_user/features/caching/entries/sentry_configuration_cache.dart';
import 'package:tmail_ui_user/features/caching/extensions/sentry_cache_extensions.dart';
import 'package:tmail_ui_user/features/caching/manager/sentry_configuration_cache_manager.dart';
import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart';
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';
@@ -48,10 +52,18 @@ class FcmMessageController extends PushBaseController {
@override
void initialize({AccountId? accountId, Session? session}) {
super.initialize(accountId: accountId, session: session);
try {
super.initialize(accountId: accountId, session: session);
_listenTokenStream();
_listenBackgroundMessageStream();
_listenTokenStream();
_listenBackgroundMessageStream();
} catch (e, st) {
logError(
'FcmMessageController::initialize: throw exception',
exception: e,
stackTrace: st,
);
}
}
void _listenBackgroundMessageStream() {
@@ -59,36 +71,104 @@ class FcmMessageController extends PushBaseController {
.debounceTime(const Duration(
milliseconds: FcmUtils.durationBackgroundMessageComing,
))
.listen(_handleBackgroundMessageAction);
.listen(
_handleBackgroundMessageAction,
onError: (e, st) {
logError(
'FcmMessageController::_listenBackgroundMessageStream',
exception: e,
stackTrace: st,
);
},
);
}
void _listenTokenStream() {
FcmService.instance.fcmTokenStreamController
?.stream
.debounceTime(const Duration(milliseconds: FcmUtils.durationRefreshToken))
.listen(FcmTokenController.instance.onFcmTokenChanged);
.listen(
FcmTokenController.instance.onFcmTokenChanged,
onError: (e, st) {
logError(
'FcmMessageController::_listenTokenStream',
exception: e,
stackTrace: st,
);
},
);
}
void _handleBackgroundMessageAction(Map<String, dynamic> payloadData) async {
log('FcmMessageController::_handleBackgroundMessageAction():payloadData: $payloadData');
logTrace('FcmMessageController::_handleBackgroundMessageAction():payloadData keys: ${payloadData.keys.toList()}');
final stateChange = FcmUtils.instance.convertFirebaseDataMessageToStateChange(payloadData);
await _initialAppConfig();
if (stateChange == null) {
logTrace('FcmMessageController::_handleBackgroundMessageAction(): stateChange is null');
return;
} else {
logTrace('FcmMessageController::_handleBackgroundMessageAction(): stateChange: ${stateChange.toString()}');
}
_getAuthenticatedAccount(stateChange: stateChange);
}
Future<void> _initialAppConfig() async {
await Future.wait([
MainBindings().dependencies(),
HiveCacheConfig.instance.setUp()
]);
Future<void> initialAppConfig() async {
try {
await MainBindings().dependencies();
await HiveCacheConfig.instance.setUp();
await Future.sync(() {
HomeBindings().dependencies();
MailboxDashBoardBindings().dependencies();
FcmInteractorBindings().dependencies();
});
_getInteractorBindings();
_getInteractorBindings();
} catch (e, st) {
logError(
'FcmMessageController::initialAppConfig: throw exception',
exception: e,
stackTrace: st,
);
rethrow;
}
}
Future<void> setUpSentryConfiguration() async {
try {
final cacheManager = getBinding<SentryConfigurationCacheManager>();
if (cacheManager == null) {
logWarning('FcmMessageController::setUpSentryConfiguration: SentryConfigurationCacheManager is null');
return;
}
final SentryConfigurationCache configCache;
try {
configCache = await cacheManager.getSentryConfiguration();
} catch (e) {
logWarning('FcmMessageController::setUpSentryConfiguration: SentryConfiguration not cached: $e');
return;
}
final sentryConfig = configCache.toSentryConfig();
if (!sentryConfig.isAvailable) {
logWarning('FcmMessageController::setUpSentryConfiguration: SentryConfiguration is not available');
return;
}
await SentryManager.instance.initializeWithSentryConfig(sentryConfig);
try {
final userCache = await cacheManager.getSentryUser();
SentryManager.instance.setUser(userCache.toSentryUser());
} catch (e) {
logTrace('FcmMessageController::setUpSentryConfiguration: Sentry user not cached: $e');
// Acceptable — Sentry initialized without user context
}
} catch (e, st) {
logError(
'FcmMessageController::setUpSentryConfiguration: throw exception',
exception: e,
stackTrace: st,
);
}
}
void _getInteractorBindings() {
@@ -100,9 +180,13 @@ class FcmMessageController extends PushBaseController {
FcmTokenController.instance.initialBindingInteractor();
}
void _getAuthenticatedAccount({StateChange? stateChange}) {
void _getAuthenticatedAccount({required StateChange stateChange}) {
if (_getAuthenticatedAccountInteractor != null) {
consumeState(_getAuthenticatedAccountInteractor!.execute(stateChange: stateChange));
} else {
logTrace(
'GetAuthenticatedAccountInteractor is null',
);
}
}
@@ -110,25 +194,18 @@ class FcmMessageController extends PushBaseController {
_dynamicUrlInterceptors?.setJmapUrl(storedTokenOidcSuccess.baseUrl.toString());
_authorizationInterceptors?.setTokenAndAuthorityOidc(
newToken: storedTokenOidcSuccess.tokenOidc,
newConfig: storedTokenOidcSuccess.oidcConfiguration
newConfig: storedTokenOidcSuccess.oidcConfiguration,
);
if (PlatformInfo.isAndroid) {
_dynamicUrlInterceptors?.changeBaseUrl(storedTokenOidcSuccess.baseUrl.toString());
_getSessionAction(stateChange: storedTokenOidcSuccess.stateChange);
} else {
_dynamicUrlInterceptors?.changeBaseUrl(storedTokenOidcSuccess.personalAccount.apiUrl);
final accountId = storedTokenOidcSuccess.personalAccount.accountId;
final username = storedTokenOidcSuccess.personalAccount.userName;
final stateChange = storedTokenOidcSuccess.stateChange;
if (accountId != null && username != null && stateChange != null) {
_pushActionFromRemoteMessageBackground(
accountId: accountId,
userName: username,
stateChange: stateChange);
}
_dispatchPushOnNonAndroid(
apiUrl: storedTokenOidcSuccess.personalAccount.apiUrl,
accountId: storedTokenOidcSuccess.personalAccount.accountId,
username: storedTokenOidcSuccess.personalAccount.userName,
stateChange: storedTokenOidcSuccess.stateChange,
);
}
}
@@ -142,24 +219,39 @@ class FcmMessageController extends PushBaseController {
_dynamicUrlInterceptors?.changeBaseUrl(credentialViewState.baseUrl.toString());
_getSessionAction(stateChange: credentialViewState.stateChange);
} else {
_dynamicUrlInterceptors?.changeBaseUrl(credentialViewState.personalAccount.apiUrl);
final accountId = credentialViewState.personalAccount.accountId;
final username = credentialViewState.personalAccount.userName;
final stateChange = credentialViewState.stateChange;
if (accountId != null && username != null && stateChange != null) {
_pushActionFromRemoteMessageBackground(
accountId: accountId,
userName: username,
stateChange: stateChange);
}
_dispatchPushOnNonAndroid(
apiUrl: credentialViewState.personalAccount.apiUrl,
accountId: credentialViewState.personalAccount.accountId,
username: credentialViewState.personalAccount.userName,
stateChange: credentialViewState.stateChange,
);
}
}
void _dispatchPushOnNonAndroid({
required String? apiUrl,
required AccountId? accountId,
required UserName? username,
required StateChange? stateChange,
}) {
_dynamicUrlInterceptors?.changeBaseUrl(apiUrl);
final canPush = accountId != null && username != null && stateChange != null;
if (!canPush) {
logTrace('FcmMessageController::_dispatchPushOnNonAndroid: accountId or username or stateChange is null');
return;
}
_pushActionFromRemoteMessageBackground(
accountId: accountId,
userName: username,
stateChange: stateChange,
);
}
void _getSessionAction({StateChange? stateChange}) {
if (_getSessionInteractor != null) {
consumeState(_getSessionInteractor!.execute(stateChange: stateChange));
} else {
logTrace('FcmMessageController::_getSessionAction: _getSessionInteractor is null');
}
}
@@ -176,9 +268,17 @@ class FcmMessageController extends PushBaseController {
userName: success.session.username,
stateChange: stateChange,
session: success.session);
} else {
logTrace(
'FcmMessageController::_handleGetSessionSuccess: Api url or state change is null',
);
}
} catch (e) {
logWarning('FcmMessageController::_handleGetSessionSuccess: Exception $e');
} catch (e, st) {
logError(
'FcmMessageController::_handleGetSessionSuccess:',
exception: e,
stackTrace: st,
);
}
}
@@ -189,7 +289,7 @@ class FcmMessageController extends PushBaseController {
Session? session
}) {
final mapTypeState = stateChange.getMapTypeState(accountId);
logTrace('FcmMessageController::_pushActionFromRemoteMessageBackground: Mapping type state to action ${mapTypeState.toString()}');
mappingTypeStateToAction(
mapTypeState,
accountId,
@@ -204,6 +304,11 @@ class FcmMessageController extends PushBaseController {
@override
void handleFailureViewState(Failure failure) {
log('FcmMessageController::_handleFailureViewState(): $failure');
if (failure is GetStoredTokenOidcFailure) {
logTrace('FcmMessageController::GetStoredTokenOidcFailure: Get stored token oidc is failed');
} else if (failure is GetSessionFailure) {
logTrace('FcmMessageController::GetSessionFailure: Get session is failed');
}
}
@override
@@ -91,19 +91,9 @@ class EmailChangeListener extends ChangeListener {
log('EmailChangeListener::dispatchActions():actions: $actions');
for (var action in actions) {
if (action is SynchronizeEmailOnForegroundAction) {
if (PlatformInfo.isAndroid) {
_handleRemoveNotificationWhenEmailMarkAsRead(action.newState, action.accountId, action.session);
}
_synchronizeEmailOnForegroundAction(action.newState);
if (PlatformInfo.isMobile) {
_getNewReceiveEmailFromNotificationAction(action.session, action.accountId, action.newState);
}
_onSynchronizeEmailOnForeground(action);
} else if (action is PushNotificationAction) {
_pushNotificationAction(action.newState, action.accountId, action.userName, action.session);
if (PlatformInfo.isAndroid) {
_getNewReceiveEmailFromNotificationAction(action.session, action.accountId, action.newState);
}
_onPushNotification(action);
} else if (action is StoreEmailStateToRefreshAction) {
if (PlatformInfo.isAndroid) {
_handleRemoveNotificationWhenEmailMarkAsRead(action.newState, action.accountId, action.session);
@@ -112,6 +102,23 @@ class EmailChangeListener extends ChangeListener {
}
}
void _onSynchronizeEmailOnForeground(SynchronizeEmailOnForegroundAction action) {
if (PlatformInfo.isAndroid) {
_handleRemoveNotificationWhenEmailMarkAsRead(action.newState, action.accountId, action.session);
}
_synchronizeEmailOnForegroundAction(action.newState);
if (PlatformInfo.isMobile) {
_getNewReceiveEmailFromNotificationAction(action.session, action.accountId, action.newState);
}
}
void _onPushNotification(PushNotificationAction action) {
_pushNotificationAction(action.newState, action.accountId, action.userName, action.session);
if (PlatformInfo.isAndroid) {
_getNewReceiveEmailFromNotificationAction(action.session, action.accountId, action.newState);
}
}
void _synchronizeEmailOnForegroundAction(jmap.State newState) {
log('EmailChangeListener::_synchronizeEmailAction():newState: $newState');
_dashBoardController?.dispatchEmailUIAction(RefreshChangeEmailAction(
@@ -142,27 +149,29 @@ class EmailChangeListener extends ChangeListener {
}
void _getStoredEmailState() {
if (_getStoredEmailStateInteractor != null && _session != null && _accountId != null) {
consumeState(_getStoredEmailStateInteractor!.execute(_session!, _accountId!));
}
final canExecute = _getStoredEmailStateInteractor != null
&& _session != null
&& _accountId != null;
if (!canExecute) return;
consumeState(_getStoredEmailStateInteractor!.execute(_session!, _accountId!));
}
void _getEmailChangesAction(jmap.State state) {
if (_getEmailChangesToPushNotificationInteractor != null &&
_accountId != null &&
_session != null &&
_userName != null) {
consumeState(_getEmailChangesToPushNotificationInteractor!.execute(
final canExecute = _getEmailChangesToPushNotificationInteractor != null
&& _accountId != null
&& _session != null
&& _userName != null;
if (!canExecute) return;
consumeState(_getEmailChangesToPushNotificationInteractor!.execute(
_session!,
_accountId!,
_userName!,
state,
propertiesCreated: EmailUtils.getPropertiesForEmailGetMethod(
_session!,
_accountId!,
_userName!,
state,
propertiesCreated: EmailUtils.getPropertiesForEmailGetMethod(
_session!,
_accountId!,
),
));
}
),
));
}
void _storeEmailDeliveryStateAction(AccountId accountId, UserName userName, jmap.State state) {
@@ -205,19 +214,12 @@ class EmailChangeListener extends ChangeListener {
@override
void handleSuccessViewState(Success success) {
log('EmailChangeListener::_handleSuccessViewState(): $success');
if (success is GetStoredEmailDeliveryStateSuccess && _newStateEmailDelivery != success.state) {
_getEmailChangesAction(success.state);
if (success is GetStoredEmailDeliveryStateSuccess) {
_handleGetStoredEmailDeliveryStateSuccess(success);
} else if (success is GetStoredEmailStateSuccess) {
_getEmailChangesAction(success.state);
} else if (success is GetEmailChangesToPushNotificationSuccess && _newStateEmailDelivery != null) {
_storeEmailDeliveryStateAction(success.accountId, success.userName, _newStateEmailDelivery!);
if (PlatformInfo.isAndroid) {
_handleListEmailToPushNotification(
userName: success.userName,
emailList: success.emailList
);
}
} else if (success is GetEmailChangesToPushNotificationSuccess) {
_handleGetEmailChangesToPushNotificationSuccess(success);
} else if (success is GetMailboxesNotPutNotificationsSuccess) {
final listEmails = _emailsAvailablePushNotification.toEmailsAvailablePushNotification(
mailboxIdsNotPutNotifications: success.mailboxes.mailboxIds);
@@ -240,18 +242,44 @@ class EmailChangeListener extends ChangeListener {
}
}
void _handleGetStoredEmailDeliveryStateSuccess(GetStoredEmailDeliveryStateSuccess success) {
if (_newStateEmailDelivery != success.state) {
_getEmailChangesAction(success.state);
}
}
void _handleGetEmailChangesToPushNotificationSuccess(GetEmailChangesToPushNotificationSuccess success) {
if (_newStateEmailDelivery == null) return;
_storeEmailDeliveryStateAction(
success.accountId,
success.userName,
_newStateEmailDelivery!,
);
if (PlatformInfo.isAndroid) {
_handleListEmailToPushNotification(
userName: success.userName,
emailList: success.emailList,
);
}
}
void _handleListEmailToPushNotification({
required UserName userName,
required List<PresentationEmail> emailList
}) {
_emailsAvailablePushNotification = emailList;
if (_getMailboxesNotPutNotificationsInteractor != null && _accountId != null && _session != null) {
final canFilterByMailbox = _getMailboxesNotPutNotificationsInteractor != null
&& _accountId != null
&& _session != null;
if (canFilterByMailbox) {
consumeState(_getMailboxesNotPutNotificationsInteractor!.execute(_session!, _accountId!));
} else {
final listEmails = _emailsAvailablePushNotification.toEmailsAvailablePushNotification();
_handleLocalPushNotification(
userName: userName,
emailList: listEmails
emailList: listEmails,
);
}
}
@@ -319,21 +347,21 @@ class EmailChangeListener extends ChangeListener {
void _getListDetailedEmailByIdAction(Session? session, AccountId accountId, List<EmailId> emailIds) {
log('EmailChangeListener::_getListDetailedEmailByIdAction():emailIds: $emailIds');
if (_getListDetailedEmailByIdInteractor != null &&
_dynamicUrlInterceptors != null &&
session != null) {
try {
final baseDownloadUrl = session.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors!.jmapUrl);
consumeState(_getListDetailedEmailByIdInteractor!.execute(
session,
accountId,
emailIds,
baseDownloadUrl
));
} catch (e) {
logWarning('EmailChangeListener::_getListDetailedEmailByIdAction(): $e');
consumeState(Stream.value(Left(GetDetailedEmailByIdFailure(e))));
}
final canExecute = _getListDetailedEmailByIdInteractor != null
&& _dynamicUrlInterceptors != null
&& session != null;
if (!canExecute) return;
try {
final baseDownloadUrl = session.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors!.jmapUrl);
consumeState(_getListDetailedEmailByIdInteractor!.execute(
session,
accountId,
emailIds,
baseDownloadUrl,
));
} catch (e) {
logWarning('EmailChangeListener::_getListDetailedEmailByIdAction(): $e');
consumeState(Stream.value(Left(GetDetailedEmailByIdFailure(e))));
}
}
@@ -3,11 +3,36 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/controller/fcm_message_controller.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_service.dart';
Future<void>? _backgroundInitFuture;
Future<void> _ensureBackgroundInitialized() {
return _backgroundInitFuture ??= (() async {
FcmService.instance.initialStreamController();
FcmMessageController.instance.initialize();
await FcmMessageController.instance
.initialAppConfig()
.timeout(const Duration(seconds: 10));
await FcmMessageController.instance
.setUpSentryConfiguration()
.timeout(const Duration(seconds: 10));
}()).catchError((Object error, StackTrace stackTrace) {
_backgroundInitFuture = null;
throw error;
});
}
@pragma('vm:entry-point')
Future<void> handleFirebaseBackgroundMessage(RemoteMessage message) async {
FcmService.instance.initialStreamController();
FcmMessageController.instance.initialize();
FcmService.instance.handleFirebaseBackgroundMessage(message);
try {
await _ensureBackgroundInitialized();
FcmService.instance.handleFirebaseBackgroundMessage(message);
} catch (e, st) {
logError(
'FcmReceiver::handleFirebaseBackgroundMessage: throw exception',
exception: e,
stackTrace: st,
);
}
}
class FcmReceiver {
@@ -34,21 +59,34 @@ class FcmReceiver {
final token = await FirebaseMessaging.instance.getToken();
log('FcmReceiver::_getInitialToken:token: $token');
return token;
} catch (e) {
logWarning('FcmReceiver::_getInitialToken: TYPE = ${e.runtimeType} | Exception = $e');
} catch (e, st) {
logError(
'FcmReceiver::_getInitialToken:',
exception: e,
stackTrace: st,
);
return null;
}
}
Future _onHandleFcmToken() async {
final token = await _getInitialToken();
FcmService.instance.handleToken(token);
var currentToken = await _getInitialToken();
FcmService.instance.handleToken(currentToken);
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
log('FcmReceiver::_onHandleFcmToken:onTokenRefresh: $newToken');
if (newToken != token) {
FcmService.instance.handleToken(newToken);
FirebaseMessaging.instance.onTokenRefresh.listen(
(newToken) {
if (newToken != currentToken) {
currentToken = newToken;
FcmService.instance.handleToken(newToken);
}
},
onError: (e, st) {
logError(
'FcmReceiver::_onHandleFcmToken:onTokenRefresh:',
exception: e,
stackTrace: st,
);
}
});
);
}
}
@@ -33,11 +33,24 @@ class FcmService {
fcmTokenStreamController = StreamController<String?>.broadcast();
}
void closeStream() {
backgroundMessageStreamController?.close();
fcmTokenStreamController?.close();
backgroundMessageStreamController = null;
fcmTokenStreamController = null;
Future<void> closeStream() async {
try {
await backgroundMessageStreamController?.close();
} catch (e) {
logWarning(
'FcmService::closeStream: backgroundMessageStreamController throw exception: $e',
);
} finally {
backgroundMessageStreamController = null;
}
try {
await fcmTokenStreamController?.close();
} catch (e) {
logWarning(
'FcmService::closeStream: fcmTokenStreamController throw exception: $e',
);
} finally {
fcmTokenStreamController = null;
}
}
}