mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-06-13 13:02:13 +00:00
Fix: Issue with background notifications on Android
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
b7b2ad701f
commit
053fbeb66e
8 changed files with 290 additions and 231 deletions
|
|
@ -1,5 +1,9 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.2.31
|
||||||
|
|
||||||
|
- Fix: Issue with background notifications on Android
|
||||||
|
|
||||||
## 0.2.30
|
## 0.2.30
|
||||||
|
|
||||||
- Fix: Changed minimum threshold for the user discovery to 3
|
- Fix: Changed minimum threshold for the user discovery to 3
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ void main() async {
|
||||||
unawaited(StartupGuard.markAppStartup());
|
unawaited(StartupGuard.markAppStartup());
|
||||||
|
|
||||||
var storageError = await twonlyMinimumInitialization();
|
var storageError = await twonlyMinimumInitialization();
|
||||||
await initFCMService();
|
await FcmNotificationService.initStartup();
|
||||||
|
|
||||||
var userExists = false;
|
var userExists = false;
|
||||||
|
|
||||||
|
|
@ -109,6 +109,8 @@ void main() async {
|
||||||
unawaited(initFileDownloader());
|
unawaited(initFileDownloader());
|
||||||
|
|
||||||
if (userExists) {
|
if (userExists) {
|
||||||
|
unawaited(FcmNotificationService.initAfterUserLoaded());
|
||||||
|
|
||||||
if (userService.currentUser.allowErrorTrackingViaSentry) {
|
if (userService.currentUser.allowErrorTrackingViaSentry) {
|
||||||
AppState.allowErrorTrackingViaSentry = true;
|
AppState.allowErrorTrackingViaSentry = true;
|
||||||
await SentryFlutter.init(
|
await SentryFlutter.init(
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server;
|
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
|
||||||
|
as server;
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart';
|
||||||
import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart';
|
import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart';
|
||||||
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
||||||
|
|
@ -65,13 +66,15 @@ class ApiService {
|
||||||
Stream<SubscriptionPlan> get onPlanUpdated => _planUpdateController.stream;
|
Stream<SubscriptionPlan> get onPlanUpdated => _planUpdateController.stream;
|
||||||
|
|
||||||
final _connectionStateController = StreamController<bool>.broadcast();
|
final _connectionStateController = StreamController<bool>.broadcast();
|
||||||
Stream<bool> get onConnectionStateUpdated => _connectionStateController.stream;
|
Stream<bool> get onConnectionStateUpdated =>
|
||||||
|
_connectionStateController.stream;
|
||||||
|
|
||||||
final _appOutdatedController = StreamController<void>.broadcast();
|
final _appOutdatedController = StreamController<void>.broadcast();
|
||||||
Stream<void> get onAppOutdated => _appOutdatedController.stream;
|
Stream<void> get onAppOutdated => _appOutdatedController.stream;
|
||||||
|
|
||||||
final _newDeviceRegisteredController = StreamController<void>.broadcast();
|
final _newDeviceRegisteredController = StreamController<void>.broadcast();
|
||||||
Stream<void> get onNewDeviceRegistered => _newDeviceRegisteredController.stream;
|
Stream<void> get onNewDeviceRegistered =>
|
||||||
|
_newDeviceRegisteredController.stream;
|
||||||
|
|
||||||
bool appIsOutdated = false;
|
bool appIsOutdated = false;
|
||||||
bool isAuthenticated = false;
|
bool isAuthenticated = false;
|
||||||
|
|
@ -80,7 +83,8 @@ class ApiService {
|
||||||
Timer? reconnectionTimer;
|
Timer? reconnectionTimer;
|
||||||
int _reconnectionDelay = 5;
|
int _reconnectionDelay = 5;
|
||||||
|
|
||||||
final HashMap<Int64, Completer<server.ServerToClient?>> _pendingRequests = HashMap();
|
final HashMap<Int64, Completer<server.ServerToClient?>> _pendingRequests =
|
||||||
|
HashMap();
|
||||||
IOWebSocketChannel? _channel;
|
IOWebSocketChannel? _channel;
|
||||||
// ignore: cancel_subscriptions
|
// ignore: cancel_subscriptions
|
||||||
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
|
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
|
||||||
|
|
@ -112,7 +116,7 @@ class ApiService {
|
||||||
|
|
||||||
// Function is called after the user is authenticated at the server
|
// Function is called after the user is authenticated at the server
|
||||||
Future<void> onAuthenticated() async {
|
Future<void> onAuthenticated() async {
|
||||||
await initFCMAfterAuthenticated();
|
await FcmNotificationService.initFCMAfterAuthenticated();
|
||||||
_connectionStateController.add(true);
|
_connectionStateController.add(true);
|
||||||
|
|
||||||
if (AppState.isInBackgroundTask) {
|
if (AppState.isInBackgroundTask) {
|
||||||
|
|
@ -418,7 +422,9 @@ class ApiService {
|
||||||
}
|
}
|
||||||
if (res.error == ErrorCode.UserIdNotFound && contactId != null) {
|
if (res.error == ErrorCode.UserIdNotFound && contactId != null) {
|
||||||
Log.warn('Contact deleted their account $contactId.');
|
Log.warn('Contact deleted their account $contactId.');
|
||||||
final contact = await twonlyDB.contactsDao.getContactByUserId(contactId).getSingleOrNull();
|
final contact = await twonlyDB.contactsDao
|
||||||
|
.getContactByUserId(contactId)
|
||||||
|
.getSingleOrNull();
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
await twonlyDB.contactsDao.updateContact(
|
await twonlyDB.contactsDao.updateContact(
|
||||||
contactId,
|
contactId,
|
||||||
|
|
@ -483,7 +489,8 @@ class ApiService {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (result.isError) {
|
if (result.isError) {
|
||||||
if (result.error != ErrorCode.AuthTokenNotValid && result.error != ErrorCode.ForegroundSessionConnected) {
|
if (result.error != ErrorCode.AuthTokenNotValid &&
|
||||||
|
result.error != ErrorCode.ForegroundSessionConnected) {
|
||||||
Log.error(
|
Log.error(
|
||||||
'got error while authenticating to the server: ${result.error}',
|
'got error while authenticating to the server: ${result.error}',
|
||||||
);
|
);
|
||||||
|
|
@ -521,7 +528,8 @@ class ApiService {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (result.isError) {
|
if (result.isError) {
|
||||||
if (result.error != ErrorCode.AuthTokenNotValid && result.error != ErrorCode.ForegroundSessionConnected) {
|
if (result.error != ErrorCode.AuthTokenNotValid &&
|
||||||
|
result.error != ErrorCode.ForegroundSessionConnected) {
|
||||||
Log.error(
|
Log.error(
|
||||||
'got error while authenticating to the server: ${result.error}',
|
'got error while authenticating to the server: ${result.error}',
|
||||||
);
|
);
|
||||||
|
|
@ -553,7 +561,8 @@ class ApiService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final handshake = Handshake()..getAuthChallenge = Handshake_GetAuthChallenge();
|
final handshake = Handshake()
|
||||||
|
..getAuthChallenge = Handshake_GetAuthChallenge();
|
||||||
final req = createClientToServerFromHandshake(handshake);
|
final req = createClientToServerFromHandshake(handshake);
|
||||||
|
|
||||||
final result = await sendRequestSync(req, authenticated: false);
|
final result = await sendRequestSync(req, authenticated: false);
|
||||||
|
|
@ -618,7 +627,9 @@ class ApiService {
|
||||||
|
|
||||||
final register = Handshake_Register()
|
final register = Handshake_Register()
|
||||||
..username = username
|
..username = username
|
||||||
..publicIdentityKey = (await signalStore.getIdentityKeyPair()).getPublicKey().serialize()
|
..publicIdentityKey = (await signalStore.getIdentityKeyPair())
|
||||||
|
.getPublicKey()
|
||||||
|
.serialize()
|
||||||
..registrationId = Int64(signalIdentity.registrationId)
|
..registrationId = Int64(signalIdentity.registrationId)
|
||||||
..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize()
|
..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize()
|
||||||
..signedPrekeySignature = signedPreKey.signature
|
..signedPrekeySignature = signedPreKey.signature
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:clock/clock.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:hashlib/random.dart';
|
import 'package:hashlib/random.dart';
|
||||||
import 'package:mutex/mutex.dart';
|
import 'package:mutex/mutex.dart';
|
||||||
|
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
|
|
@ -281,7 +282,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
|
||||||
Log.info('[$receiptId] Finished handleEncryptedMessage');
|
Log.info('[$receiptId] Finished handleEncryptedMessage');
|
||||||
|
|
||||||
if (a == null && b == null) {
|
if (a == null && b == null) {
|
||||||
unawaited(updateLastServerMessageTimestamp());
|
unawaited(FcmNotificationService.updateLastServerMessageTimestamp());
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
// Message was handled without any error. Show push notification to the user for Android.
|
// Message was handled without any error. Show push notification to the user for Android.
|
||||||
await showPushNotificationFromServerMessages(
|
await showPushNotificationFromServerMessages(
|
||||||
|
|
|
||||||
29
lib/src/services/notifications/fcm.background.dart
Normal file
29
lib/src/services/notifications/fcm.background.dart
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
|
||||||
|
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
||||||
|
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||||
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
|
@pragma('vm:entry-point')
|
||||||
|
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
|
SentryWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await AppEnvironment.init();
|
||||||
|
final isInitialized = await initBackgroundExecution();
|
||||||
|
await setupPushNotification();
|
||||||
|
Log.info('Handling a background message: ${message.messageId}');
|
||||||
|
await FcmNotificationService.handleRemoteMessage(message);
|
||||||
|
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
if (isInitialized) {
|
||||||
|
await handlePeriodicTask(lastExecutionInSecondsLimit: 10);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// make sure every thing run...
|
||||||
|
await Future.delayed(const Duration(milliseconds: 2000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// ignore_for_file: unreachable_from_main
|
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io' show Platform;
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
|
|
@ -7,13 +5,11 @@ import 'package:firebase_app_installations/firebase_app_installations.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||||
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
|
|
||||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||||
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
import 'package:twonly/src/services/notifications/fcm.background.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
|
|
@ -21,240 +17,253 @@ import '../../../firebase_options.dart';
|
||||||
|
|
||||||
// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de
|
// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de
|
||||||
|
|
||||||
Future<void> checkForTokenUpdates() async {
|
class FcmNotificationService {
|
||||||
try {
|
static Future<void> initStartup() async {
|
||||||
if (!userService.isUserCreated) return;
|
await Firebase.initializeApp(
|
||||||
if (Platform.isIOS) {
|
options: DefaultFirebaseOptions.currentPlatform,
|
||||||
var apnsToken = await FirebaseMessaging.instance.getAPNSToken();
|
);
|
||||||
for (var i = 0; i < 20; i++) {
|
|
||||||
if (apnsToken != null) break;
|
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
|
||||||
await Future<void>.delayed(const Duration(seconds: 1));
|
|
||||||
apnsToken = await FirebaseMessaging.instance.getAPNSToken();
|
FirebaseMessaging.onMessage.listen(handleRemoteMessage);
|
||||||
}
|
}
|
||||||
if (apnsToken == null) {
|
|
||||||
Log.error('Could not get APNS token even after 20s...');
|
static Future<void> initAfterUserLoaded() async {
|
||||||
|
unawaited(_checkForTokenUpdates());
|
||||||
|
unawaited(_checkFcmHealthAndResetIfNeeded());
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> initFCMAfterAuthenticated({bool force = false}) async {
|
||||||
|
final fcmToken = userService.currentUser.fcmToken;
|
||||||
|
if (userService.currentUser.updateFCMToken || force) {
|
||||||
|
if (fcmToken == null) {
|
||||||
|
Log.error('FCM token could not be updated as it is empty');
|
||||||
|
await _checkForTokenUpdates();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
final res = await apiService.updateFCMToken(
|
||||||
|
fcmToken,
|
||||||
final fcmToken = await FirebaseMessaging.instance.getToken();
|
);
|
||||||
if (fcmToken == null) {
|
if (res.isSuccess) {
|
||||||
Log.error('Could not get fcm token');
|
Log.info('Uploaded new FCM token!');
|
||||||
return;
|
await UserService.update((u) {
|
||||||
}
|
u.updateFCMToken = false;
|
||||||
|
|
||||||
Log.info('Loaded FCM token.');
|
|
||||||
|
|
||||||
if (userService.currentUser.fcmToken == null ||
|
|
||||||
fcmToken != userService.currentUser.fcmToken) {
|
|
||||||
Log.info('Got new FCM token.');
|
|
||||||
await UserService.update((u) {
|
|
||||||
u
|
|
||||||
..updateFCMToken = true
|
|
||||||
..fcmToken = fcmToken;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
FirebaseMessaging.instance.onTokenRefresh
|
|
||||||
.listen((fcmToken) async {
|
|
||||||
await UserService.update((u) {
|
|
||||||
u
|
|
||||||
..updateFCMToken = true
|
|
||||||
..fcmToken = fcmToken;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.onError((err) {
|
|
||||||
Log.error('could not listen on token refresh');
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} else {
|
||||||
Log.error('could not load fcm token: $e');
|
Log.error('Could not update FCM token!');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> initFCMAfterAuthenticated({bool force = false}) async {
|
static Future<void> resetFCMTokens() async {
|
||||||
final fcmToken = userService.currentUser.fcmToken;
|
await FirebaseInstallations.instance.delete();
|
||||||
if (userService.currentUser.updateFCMToken || force) {
|
Log.info('Firebase Installation successfully deleted.');
|
||||||
if (fcmToken == null) {
|
await FirebaseMessaging.instance.deleteToken();
|
||||||
Log.error('FCM token could not be updated as it is empty');
|
Log.info('Old FCM deleted.');
|
||||||
await checkForTokenUpdates();
|
await UserService.update((u) => u.fcmToken = null);
|
||||||
|
await _checkForTokenUpdates();
|
||||||
|
await initFCMAfterAuthenticated(force: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _checkForTokenUpdates() async {
|
||||||
|
try {
|
||||||
|
if (!userService.isUserCreated) {
|
||||||
|
Log.info(
|
||||||
|
'Checking for FCM token updates skipped: user is not yet created.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
var apnsToken = await FirebaseMessaging.instance.getAPNSToken();
|
||||||
|
for (var i = 0; i < 20; i++) {
|
||||||
|
if (apnsToken != null) break;
|
||||||
|
await Future<void>.delayed(const Duration(seconds: 1));
|
||||||
|
apnsToken = await FirebaseMessaging.instance.getAPNSToken();
|
||||||
|
}
|
||||||
|
if (apnsToken == null) {
|
||||||
|
Log.error('Could not get APNS token even after 20s...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final fcmToken = await FirebaseMessaging.instance.getToken();
|
||||||
|
if (fcmToken == null) {
|
||||||
|
Log.error('Could not get fcm token');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.info('Loaded FCM token.');
|
||||||
|
|
||||||
|
if (userService.currentUser.fcmToken == null ||
|
||||||
|
fcmToken != userService.currentUser.fcmToken) {
|
||||||
|
Log.info('Got new FCM token.');
|
||||||
|
await UserService.update((u) {
|
||||||
|
u
|
||||||
|
..updateFCMToken = true
|
||||||
|
..fcmToken = fcmToken;
|
||||||
|
});
|
||||||
|
if (apiService.isAuthenticated) {
|
||||||
|
final res = await apiService.updateFCMToken(fcmToken);
|
||||||
|
if (res.isSuccess) {
|
||||||
|
Log.info('Uploaded new FCM token!');
|
||||||
|
await UserService.update((u) {
|
||||||
|
u.updateFCMToken = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Log.error('Could not update FCM token!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FirebaseMessaging.instance.onTokenRefresh
|
||||||
|
// ignore: avoid_types_on_closure_parameters
|
||||||
|
.listen((String fcmToken) async {
|
||||||
|
await UserService.update((u) {
|
||||||
|
u
|
||||||
|
..updateFCMToken = true
|
||||||
|
..fcmToken = fcmToken;
|
||||||
|
});
|
||||||
|
if (apiService.isAuthenticated) {
|
||||||
|
final res = await apiService.updateFCMToken(fcmToken);
|
||||||
|
if (res.isSuccess) {
|
||||||
|
Log.info('Uploaded new FCM token!');
|
||||||
|
await UserService.update((u) {
|
||||||
|
u.updateFCMToken = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Log.error('Could not update FCM token!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onError((err) {
|
||||||
|
Log.error('could not listen on token refresh');
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('could not load fcm token: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> handleRemoteMessage(RemoteMessage message) async {
|
||||||
|
await _updateLastFcmMessageTimestamp();
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
Log.error('Got message in Dart while on iOS');
|
||||||
|
}
|
||||||
|
if (message.notification != null && AppState.isAppInBackground) {
|
||||||
|
Log.error(
|
||||||
|
'Got notification but app is in background, so the SDK already have shown the message.',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final res = await apiService.updateFCMToken(
|
|
||||||
fcmToken,
|
if (message.notification != null || message.data['title'] != null) {
|
||||||
);
|
final title =
|
||||||
if (res.isSuccess) {
|
message.notification?.title ?? message.data['title'] as String? ?? '';
|
||||||
Log.info('Uploaded new FCM token!');
|
final body =
|
||||||
await UserService.update((u) {
|
message.notification?.body ?? message.data['body'] as String? ?? '';
|
||||||
u.updateFCMToken = false;
|
await customLocalPushNotification(title, body);
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Log.error('Could not update FCM token!');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> resetFCMTokens() async {
|
static Future<void> _updateLastFcmMessageTimestamp() async {
|
||||||
await FirebaseInstallations.instance.delete();
|
const storage = FlutterSecureStorage();
|
||||||
Log.info('Firebase Installation successfully deleted.');
|
final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
await FirebaseMessaging.instance.deleteToken();
|
try {
|
||||||
Log.info('Old FCM deleted.');
|
await storage.write(
|
||||||
await UserService.update((u) => u.fcmToken = null);
|
key: SecureStorageKeys.lastFcmMessageTimestamp,
|
||||||
await checkForTokenUpdates();
|
value: nowMs,
|
||||||
await initFCMAfterAuthenticated(force: true);
|
iOptions: const IOSOptions(
|
||||||
}
|
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
||||||
|
accessibility: KeychainAccessibility.first_unlock,
|
||||||
Future<void> initFCMService() async {
|
),
|
||||||
await Firebase.initializeApp(
|
);
|
||||||
options: DefaultFirebaseOptions.currentPlatform,
|
Log.info('Updated last FCM message timestamp to $nowMs');
|
||||||
);
|
} catch (e) {
|
||||||
|
Log.error('Could not write last FCM message timestamp: $e');
|
||||||
unawaited(checkForTokenUpdates());
|
|
||||||
unawaited(checkFcmHealthAndResetIfNeeded());
|
|
||||||
|
|
||||||
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
|
||||||
|
|
||||||
FirebaseMessaging.onMessage.listen(handleRemoteMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
|
||||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
|
||||||
SentryWidgetsFlutterBinding.ensureInitialized();
|
|
||||||
await AppEnvironment.init();
|
|
||||||
final isInitialized = await initBackgroundExecution();
|
|
||||||
await setupPushNotification();
|
|
||||||
Log.info('Handling a background message: ${message.messageId}');
|
|
||||||
await handleRemoteMessage(message);
|
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
if (isInitialized) {
|
|
||||||
await handlePeriodicTask(lastExecutionInSecondsLimit: 10);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// make sure every thing run...
|
|
||||||
await Future.delayed(const Duration(milliseconds: 2000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> handleRemoteMessage(RemoteMessage message) async {
|
|
||||||
await updateLastFcmMessageTimestamp();
|
|
||||||
if (!Platform.isAndroid) {
|
|
||||||
Log.error('Got message in Dart while on iOS');
|
|
||||||
}
|
|
||||||
if (message.notification != null && AppState.isAppInBackground) {
|
|
||||||
Log.error(
|
|
||||||
'Got notification but app is in background, so the SDK already have shown the message.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.notification != null || message.data['title'] != null) {
|
static Future<void> updateLastServerMessageTimestamp() async {
|
||||||
final title =
|
const storage = FlutterSecureStorage();
|
||||||
message.notification?.title ?? message.data['title'] as String? ?? '';
|
final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
final body =
|
try {
|
||||||
message.notification?.body ?? message.data['body'] as String? ?? '';
|
await storage.write(
|
||||||
await customLocalPushNotification(title, body);
|
key: SecureStorageKeys.lastServerMessageTimestamp,
|
||||||
|
value: nowMs,
|
||||||
|
iOptions: const IOSOptions(
|
||||||
|
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
||||||
|
accessibility: KeychainAccessibility.first_unlock,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Log.info('Updated last server message timestamp to $nowMs');
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Could not write last server message timestamp: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// On Android the push notification is now shown in the server_message.dart. This ensures
|
static Future<void> _checkFcmHealthAndResetIfNeeded() async {
|
||||||
// that the messages was successfully decrypted before showing the push notification
|
if (!userService.isUserCreated) {
|
||||||
// else if (message.data['push_data'] != null) {
|
Log.info('FCM health check skipped: user is not yet created.');
|
||||||
// await handlePushData(message.data['push_data'] as String);
|
return;
|
||||||
// }
|
}
|
||||||
}
|
const storage = FlutterSecureStorage();
|
||||||
|
try {
|
||||||
|
final lastFcmStr = await storage.read(
|
||||||
|
key: SecureStorageKeys.lastFcmMessageTimestamp,
|
||||||
|
iOptions: const IOSOptions(
|
||||||
|
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
||||||
|
accessibility: KeychainAccessibility.first_unlock,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final lastServerStr = await storage.read(
|
||||||
|
key: SecureStorageKeys.lastServerMessageTimestamp,
|
||||||
|
iOptions: const IOSOptions(
|
||||||
|
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
||||||
|
accessibility: KeychainAccessibility.first_unlock,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
Future<void> updateLastFcmMessageTimestamp() async {
|
final now = DateTime.now();
|
||||||
const storage = FlutterSecureStorage();
|
final threeDaysAgo = now.subtract(const Duration(days: 3));
|
||||||
final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
|
|
||||||
try {
|
|
||||||
await storage.write(
|
|
||||||
key: SecureStorageKeys.lastFcmMessageTimestamp,
|
|
||||||
value: nowMs,
|
|
||||||
iOptions: const IOSOptions(
|
|
||||||
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
|
||||||
accessibility: KeychainAccessibility.first_unlock,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Log.info('Updated last FCM message timestamp to $nowMs');
|
|
||||||
} catch (e) {
|
|
||||||
Log.error('Could not write last FCM message timestamp: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateLastServerMessageTimestamp() async {
|
DateTime? lastFcmTime;
|
||||||
const storage = FlutterSecureStorage();
|
if (lastFcmStr != null) {
|
||||||
final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
|
final ms = int.tryParse(lastFcmStr);
|
||||||
try {
|
if (ms != null) {
|
||||||
await storage.write(
|
lastFcmTime = DateTime.fromMillisecondsSinceEpoch(ms);
|
||||||
key: SecureStorageKeys.lastServerMessageTimestamp,
|
}
|
||||||
value: nowMs,
|
|
||||||
iOptions: const IOSOptions(
|
|
||||||
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
|
||||||
accessibility: KeychainAccessibility.first_unlock,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Log.info('Updated last server message timestamp to $nowMs');
|
|
||||||
} catch (e) {
|
|
||||||
Log.error('Could not write last server message timestamp: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> checkFcmHealthAndResetIfNeeded() async {
|
|
||||||
if (!userService.isUserCreated) return;
|
|
||||||
const storage = FlutterSecureStorage();
|
|
||||||
try {
|
|
||||||
final lastFcmStr = await storage.read(
|
|
||||||
key: SecureStorageKeys.lastFcmMessageTimestamp,
|
|
||||||
iOptions: const IOSOptions(
|
|
||||||
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
|
||||||
accessibility: KeychainAccessibility.first_unlock,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final lastServerStr = await storage.read(
|
|
||||||
key: SecureStorageKeys.lastServerMessageTimestamp,
|
|
||||||
iOptions: const IOSOptions(
|
|
||||||
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
|
||||||
accessibility: KeychainAccessibility.first_unlock,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final now = DateTime.now();
|
|
||||||
final threeDaysAgo = now.subtract(const Duration(days: 3));
|
|
||||||
|
|
||||||
DateTime? lastFcmTime;
|
|
||||||
if (lastFcmStr != null) {
|
|
||||||
final ms = int.tryParse(lastFcmStr);
|
|
||||||
if (ms != null) {
|
|
||||||
lastFcmTime = DateTime.fromMillisecondsSinceEpoch(ms);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (lastFcmTime != null) {
|
if (lastFcmTime != null) {
|
||||||
Log.info('Last message received via FCM messaging system: $lastFcmTime');
|
Log.info(
|
||||||
} else {
|
'Last message received via FCM messaging system: $lastFcmTime',
|
||||||
Log.info('No record of a message received via FCM messaging system.');
|
);
|
||||||
}
|
} else {
|
||||||
|
Log.info('No record of a message received via FCM messaging system.');
|
||||||
DateTime? lastServerTime;
|
|
||||||
if (lastServerStr != null) {
|
|
||||||
final ms = int.tryParse(lastServerStr);
|
|
||||||
if (ms != null) {
|
|
||||||
lastServerTime = DateTime.fromMillisecondsSinceEpoch(ms);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check conditions:
|
DateTime? lastServerTime;
|
||||||
// 1. No messages received via FCM in the last 3 days (either null or older than 3 days)
|
if (lastServerStr != null) {
|
||||||
final fcmInactive = lastFcmTime == null || lastFcmTime.isBefore(threeDaysAgo);
|
final ms = int.tryParse(lastServerStr);
|
||||||
// 2. Server message received within the last 3 days
|
if (ms != null) {
|
||||||
final serverActive = lastServerTime != null && lastServerTime.isAfter(threeDaysAgo);
|
lastServerTime = DateTime.fromMillisecondsSinceEpoch(ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (fcmInactive && serverActive) {
|
final fcmInactive =
|
||||||
Log.warn('FCM has been inactive for >3 days, but server messages have been active. Resetting FCM tokens...');
|
lastFcmTime == null || lastFcmTime.isBefore(threeDaysAgo);
|
||||||
await resetFCMTokens();
|
final serverActive =
|
||||||
} else {
|
lastServerTime != null && lastServerTime.isAfter(threeDaysAgo);
|
||||||
Log.info('FCM check passed. No reset needed.');
|
|
||||||
|
if (fcmInactive && serverActive) {
|
||||||
|
Log.warn(
|
||||||
|
'FCM has been inactive for >3 days, but server messages have been active. Resetting FCM tokens...',
|
||||||
|
);
|
||||||
|
await resetFCMTokens();
|
||||||
|
} else {
|
||||||
|
Log.info('FCM check passed. No reset needed.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Error during FCM health check: $e');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
Log.error('Error during FCM health check: $e');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/model/json/userdata.model.dart';
|
import 'package:twonly/src/model/json/userdata.model.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||||
|
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
||||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
@ -163,6 +164,8 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
|
|
||||||
await UserService.save(userData);
|
await UserService.save(userData);
|
||||||
|
|
||||||
|
unawaited(FcmNotificationService.initAfterUserLoaded());
|
||||||
|
|
||||||
await apiService.authenticate();
|
await apiService.authenticate();
|
||||||
widget.callbackOnSuccess();
|
widget.callbackOnSuccess();
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ class _NotificationViewState extends State<NotificationView> {
|
||||||
_isLoadingTroubleshooting = true;
|
_isLoadingTroubleshooting = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
await initFCMAfterAuthenticated(force: true);
|
await FcmNotificationService.initFCMAfterAuthenticated(force: true);
|
||||||
|
|
||||||
await setupNotificationWithUsers(force: true);
|
await setupNotificationWithUsers(force: true);
|
||||||
|
|
||||||
|
|
@ -90,7 +90,7 @@ class _NotificationViewState extends State<NotificationView> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoadingReset = true;
|
_isLoadingReset = true;
|
||||||
});
|
});
|
||||||
await resetFCMTokens();
|
await FcmNotificationService.resetFCMTokens();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await showAlertDialog(
|
await showAlertDialog(
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue