From 05e2922da5a6b8af308d5c656a29ea1eeb7cc021 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Thu, 14 Apr 2022 07:34:55 +0200 Subject: [PATCH] refactor: New push --- lib/utils/background_push.dart | 279 ++----------------- lib/utils/client_manager.dart | 17 +- lib/utils/push_helper.dart | 146 ++++++++++ pubspec.lock | 8 +- pubspec.yaml | 7 +- scripts/enable-android-google-services.patch | 36 ++- 6 files changed, 206 insertions(+), 287 deletions(-) create mode 100644 lib/utils/push_helper.dart diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index f9a937e1ec..00ca2aaa19 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -25,7 +25,6 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:http/http.dart' as http; @@ -34,10 +33,10 @@ import 'package:unifiedpush/unifiedpush.dart'; import 'package:vrouter/vrouter.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions.dart/client_stories_extension.dart'; +import 'package:fluffychat/utils/push_helper.dart'; import '../config/app_config.dart'; import '../config/setting_keys.dart'; import 'famedlysdk_store.dart'; -import 'matrix_sdk_extensions.dart/matrix_locals.dart'; import 'platform_infos.dart'; //import 'package:fcm_shared_isolate/fcm_shared_isolate.dart'; @@ -66,6 +65,8 @@ class BackgroundPush { final pendingTests = >{}; + final dynamic firebase = null; //FcmSharedIsolate(); + DateTime? lastReceivedPush; bool upAction = false; @@ -76,8 +77,15 @@ class BackgroundPush { onRoomSync ??= client.onSync.stream .where((s) => s.hasRoomUpdate) .listen((s) => _onClearingPush(getFromServer: false)); - _fcmSharedIsolate?.setListeners( - onMessage: _onFcmMessage, + firebase?.setListeners( + onMessage: (message) => pushHelper( + PushNotification.fromJson( + Map.from(message['data'] ?? message)), + client: client, + l10n: l10n, + activeRoomId: router?.currentState?.pathParameters['roomid'], + onSelectNotification: goToRoom, + ), onNewToken: _newFcmToken, ); if (Platform.isAndroid) { @@ -112,16 +120,14 @@ class BackgroundPush { void handleLoginStateChanged(_) => setupPush(); + StreamSubscription? onLogin; + StreamSubscription? onRoomSync; + void _newFcmToken(String token) { _fcmToken = token; setupPush(); } - final dynamic _fcmSharedIsolate = null; //FcmSharedIsolate(); - - StreamSubscription? onLogin; - StreamSubscription? onRoomSync; - Future setupPusher({ String? gatewayUrl, String? token, @@ -129,7 +135,7 @@ class BackgroundPush { bool useDeviceSpecificAppId = false, }) async { if (PlatformInfos.isIOS) { - await _fcmSharedIsolate?.requestPermission(); + await firebase?.requestPermission(); } final clientName = PlatformInfos.clientName; oldTokens ??= {}; @@ -213,7 +219,6 @@ class BackgroundPush { Future setupPush() async { Logs().d("SetupPush"); - await setupLocalNotificationsPlugin(); if (client.loginState != LoginState.loggedIn || !PlatformInfos.isMobile || context == null) { @@ -272,7 +277,7 @@ class BackgroundPush { Logs().v('Setup firebase'); if (_fcmToken?.isEmpty ?? true) { try { - _fcmToken = await _fcmSharedIsolate?.getToken(); + _fcmToken = await firebase?.getToken(); if (_fcmToken == null) throw ('PushToken is null'); } catch (e, s) { Logs().w('[Push] cannot get token', e, e is String ? null : s); @@ -306,43 +311,10 @@ class BackgroundPush { } } - bool _notificationsPluginSetUp = false; - Future setupLocalNotificationsPlugin() async { - if (_notificationsPluginSetUp) { - return; - } - - // initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project - const initializationSettingsAndroid = - AndroidInitializationSettings('notifications_icon'); - final initializationSettingsIOS = IOSInitializationSettings( - onDidReceiveLocalNotification: (i, a, b, c) async => null, - ); - final initializationSettings = InitializationSettings( - android: initializationSettingsAndroid, - iOS: initializationSettingsIOS, - ); - await _flutterLocalNotificationsPlugin.initialize(initializationSettings, - onSelectNotification: goToRoom); - - _notificationsPluginSetUp = true; - } - Future setupUp() async { await UnifiedPush.registerAppWithDialog(context!); } - Future _onFcmMessage(Map message) async { - Logs().v('[Push] Foreground message received'); - final data = Map.from(message['data'] ?? message); - try { - await _onMessage(data); - } catch (e, s) { - Logs().e('[Push] Error while processing notification', e, s); - await _showDefaultNotification(data); - } - } - Future _newUpEndpoint(String newEndpoint, String i) async { upAction = true; if (newEndpoint.isEmpty) { @@ -374,7 +346,7 @@ class BackgroundPush { Logs().i('[Push] UnifiedPush using endpoint ' + endpoint); final oldTokens = {}; try { - final fcmToken = await _fcmSharedIsolate?.getToken(); + final fcmToken = await firebase?.getToken(); oldTokens.add(fcmToken); } catch (_) {} await setupPusher( @@ -405,80 +377,12 @@ class BackgroundPush { upAction = true; final data = Map.from( json.decode(utf8.decode(message))['notification']); - try { - await _onMessage(data); - } catch (e, s) { - Logs().e('[Push] Error while processing notification', e, s); - await _showDefaultNotification(data); - } - } - - Future _onMessage(Map data) async { - Logs().v('[Push] _onMessage'); - lastReceivedPush = DateTime.now(); - final roomId = data['room_id']; - final eventId = data['event_id']; - if (roomId == 'test') { - Logs().v('[Push] Test $eventId was successful!'); - pendingTests.remove(eventId)?.complete(); - return; - } - - // For legacy reasons the counts map could be a String encoded JSON: - final countsMap = - data.tryGetMap('counts', TryGet.silent) ?? - (jsonDecode(data.tryGet('counts') ?? '{}') - as Map); - final unread = countsMap.tryGet('unread'); - - if ((roomId?.isEmpty ?? true) || - (eventId?.isEmpty ?? true) || - unread == 0) { - await _onClearingPush(); - return; - } - var giveUp = false; - var loaded = false; - final stopwatch = Stopwatch(); - stopwatch.start(); - final syncSubscription = client.onSync.stream.listen((r) { - if (stopwatch.elapsed.inSeconds >= 30) { - giveUp = true; - } - }); - final eventSubscription = client.onEvent.stream.listen((e) { - if (e.content['event_id'] == eventId) { - loaded = true; - } - }); - try { - if (!(await eventExists(roomId, eventId)) && !loaded) { - do { - Logs().v('[Push] getting ' + roomId + ', event ' + eventId); - await client - .oneShotSync() - .catchError((e) => Logs().v('[Push] Error one-shot syncing', e)); - if (stopwatch.elapsed.inSeconds >= 60) { - giveUp = true; - } - } while (!loaded && !giveUp); - } - Logs().v('[Push] ' + - (giveUp ? 'gave up on ' : 'got ') + - roomId + - ', event ' + - eventId); - } finally { - await syncSubscription.cancel(); - await eventSubscription.cancel(); - } - await _showNotification(roomId, eventId); - } - - Future eventExists(String roomId, String? eventId) async { - final room = client.getRoomById(roomId); - if (room == null) return false; - return (await client.database!.getEventById(eventId!, room)) != null; + await pushHelper( + PushNotification.fromJson(data), + client: client, + l10n: l10n, + activeRoomId: router?.currentState?.pathParameters['roomid'], + ); } /// Workaround for the problem that local notification IDs must be int but we @@ -584,139 +488,4 @@ class BackgroundPush { _clearingPushLock = false; } } - - Future _showNotification(String roomId, String eventId) async { - await setupLocalNotificationsPlugin(); - final room = client.getRoomById(roomId); - if (room == null) { - throw 'Room not found'; - } - await room.postLoad(); - final event = await client.database!.getEventById(eventId, room); - - final activeRoomId = router!.currentState!.pathParameters['roomid']; - - if (((activeRoomId?.isNotEmpty ?? false) && - activeRoomId == room.id && - client.syncPresence == null) || - (event != null && (room.notificationCount == 0))) { - return; - } - - // load the locale - await loadLocale(); - - // Calculate title - final title = l10n!.unreadMessages(room.notificationCount); - - // Calculate the body - final body = event!.getLocalizedBody( - MatrixLocals(L10n.of(context!)!), - withSenderNamePrefix: !room.isDirectChat, - plaintextBody: true, - hideReply: true, - hideEdit: true, - ); - - // The person object for the android message style notification - final avatar = room.avatar == null - ? null - : await DefaultCacheManager().getSingleFile( - event.room.avatar! - .getThumbnail( - client, - width: 126, - height: 126, - ) - .toString(), - ); - final person = Person( - name: room.getLocalizedDisplayname(MatrixLocals(l10n!)), - icon: avatar == null ? null : BitmapFilePathAndroidIcon(avatar.path), - ); - - // Show notification - final androidPlatformChannelSpecifics = _getAndroidNotificationDetails( - styleInformation: MessagingStyleInformation( - person, - conversationTitle: title, - messages: [ - Message( - body, - event.originServerTs, - person, - ) - ], - ), - ticker: l10n!.newMessageInFluffyChat, - ); - const iOSPlatformChannelSpecifics = IOSNotificationDetails(); - final platformChannelSpecifics = NotificationDetails( - android: androidPlatformChannelSpecifics, - iOS: iOSPlatformChannelSpecifics, - ); - await _flutterLocalNotificationsPlugin.show( - await mapRoomIdToInt(room.id), - room.getLocalizedDisplayname(MatrixLocals(l10n!)), - body, - platformChannelSpecifics, - payload: roomId, - ); - } - - Future _showDefaultNotification(Map data) async { - try { - await setupLocalNotificationsPlugin(); - - await loadLocale(); - final String? eventId = data['event_id']; - final String? roomId = data['room_id']; - - // For legacy reasons the counts map could be a String encoded JSON: - final countsMap = data.tryGetMap('counts') ?? - (jsonDecode(data.tryGet('counts') ?? '{}') - as Map); - final unread = countsMap.tryGet('unread') ?? 1; - - if (unread == 0 || roomId == null || eventId == null) { - await _onClearingPush(); - return; - } - - // Display notification - final androidPlatformChannelSpecifics = _getAndroidNotificationDetails(); - const iOSPlatformChannelSpecifics = IOSNotificationDetails(); - final platformChannelSpecifics = NotificationDetails( - android: androidPlatformChannelSpecifics, - iOS: iOSPlatformChannelSpecifics, - ); - final title = l10n!.unreadChats(unread); - await _flutterLocalNotificationsPlugin.show( - await mapRoomIdToInt(roomId), - title, - l10n!.openAppToReadMessages, - platformChannelSpecifics, - payload: roomId, - ); - } catch (e, s) { - Logs().e('[Push] Error while processing background notification', e, s); - } - } - - AndroidNotificationDetails _getAndroidNotificationDetails( - {MessagingStyleInformation? styleInformation, String? ticker}) { - final color = (context != null ? Theme.of(context!).primaryColor : null) ?? - const Color(0xFF5625BA); - - return AndroidNotificationDetails( - AppConfig.pushNotificationsChannelId, - AppConfig.pushNotificationsChannelName, - AppConfig.pushNotificationsChannelDescription, - styleInformation: styleInformation, - importance: Importance.max, - priority: Priority.high, - ticker: ticker, - color: color, - ); - } } diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index 1e9e0cf959..efc9ed2790 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -14,7 +14,7 @@ import 'matrix_sdk_extensions.dart/flutter_matrix_hive_database.dart'; abstract class ClientManager { static const String clientNamespace = 'im.fluffychat.store.clients'; - static Future> getClients() async { + static Future> getClients({bool initialize = true}) async { if (PlatformInfos.isLinux) { Hive.init((await getApplicationSupportDirectory()).path); } else { @@ -37,12 +37,15 @@ abstract class ClientManager { await Store().setItem(clientNamespace, jsonEncode(clientNames.toList())); } final clients = clientNames.map(createClient).toList(); - await Future.wait(clients.map((client) => client - .init( - waitForFirstSync: false, - waitUntilLoadCompletedLoaded: false, - ) - .catchError((e, s) => Logs().e('Unable to initialize client', e, s)))); + if (initialize) { + await Future.wait(clients.map((client) => client + .init( + waitForFirstSync: false, + waitUntilLoadCompletedLoaded: false, + ) + .catchError( + (e, s) => Logs().e('Unable to initialize client', e, s)))); + } if (clients.length > 1 && clients.any((c) => !c.isLogged())) { final loggedOutClients = clients.where((c) => !c.isLogged()).toList(); for (final client in loggedOutClients) { diff --git a/lib/utils/push_helper.dart b/lib/utils/push_helper.dart new file mode 100644 index 0000000000..919ffe6de1 --- /dev/null +++ b/lib/utils/push_helper.dart @@ -0,0 +1,146 @@ +import 'dart:convert'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:matrix/matrix.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; +import 'package:fluffychat/utils/client_manager.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart'; + +Future pushHelper( + PushNotification notification, { + Client? client, + L10n? l10n, + String? activeRoomId, + Future Function(String?)? onSelectNotification, +}) async { + final isBackgroundMessage = client == null; + Logs().v( + 'Push helper has been started (background=$isBackgroundMessage).', + notification.toJson(), + ); + + if (!isBackgroundMessage && + activeRoomId == notification.roomId && + activeRoomId != null && + client?.syncPresence == null) { + Logs().v('Room is in foreground. Stop push helper here.'); + return; + } + + // Initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project + final _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + await _flutterLocalNotificationsPlugin.initialize( + InitializationSettings( + android: const AndroidInitializationSettings('notifications_icon'), + iOS: IOSInitializationSettings( + onDidReceiveLocalNotification: (i, a, b, c) async => null, + ), + ), + onSelectNotification: onSelectNotification, + ); + + client ??= (await ClientManager.getClients(initialize: false)).first; + final event = await client.getEventByPushNotification( + notification, + storeInDatabase: isBackgroundMessage, + ); + + if (event == null) { + Logs().v('Notification is a clearing indicator.'); + await _flutterLocalNotificationsPlugin.cancelAll(); + final store = await SharedPreferences.getInstance(); + await store.setString(SettingKeys.notificationCurrentIds, json.encode({})); + return; + } + Logs().v('Push helper got notification event.'); + + l10n ??= await L10n.delegate.load(window.locale); + final matrixLocals = MatrixLocals(l10n); + + // Display notification// Calculate title + final title = l10n.unreadMessages(notification.counts?.unread ?? 1); + + // Calculate the body + final body = event.getLocalizedBody( + matrixLocals, + plaintextBody: true, + hideReply: true, + hideEdit: true, + removeMarkdown: true, + ); + + // The person object for the android message style notification + if (isBackgroundMessage) WidgetsFlutterBinding.ensureInitialized(); + final avatar = event.room.avatar?.toString(); + final person = Person( + name: event.room.getLocalizedDisplayname(matrixLocals), + icon: avatar == null ? null : ContentUriAndroidIcon(avatar), + ); + + // Show notification + final androidPlatformChannelSpecifics = AndroidNotificationDetails( + AppConfig.pushNotificationsChannelId, + AppConfig.pushNotificationsChannelName, + AppConfig.pushNotificationsChannelDescription, + styleInformation: MessagingStyleInformation( + person, + conversationTitle: title, + groupConversation: !event.room.isDirectChat, + messages: [ + Message( + body, + event.originServerTs, + person, + ) + ], + ), + ticker: l10n.newMessageInFluffyChat, + importance: Importance.max, + priority: Priority.high, + ); + const iOSPlatformChannelSpecifics = IOSNotificationDetails(); + final platformChannelSpecifics = NotificationDetails( + android: androidPlatformChannelSpecifics, + iOS: iOSPlatformChannelSpecifics, + ); + await _flutterLocalNotificationsPlugin.show( + await mapRoomIdToInt(event.room.id), + event.room.displayname, + body, + platformChannelSpecifics, + payload: event.roomId, + ); + Logs().v('Push helper has been completed!'); +} + +/// Workaround for the problem that local notification IDs must be int but we +/// sort by [roomId] which is a String. To make sure that we don't have duplicated +/// IDs we map the [roomId] to a number and store this number. +Future mapRoomIdToInt(String roomId) async { + final store = await SharedPreferences.getInstance(); + final idMap = Map.from(jsonDecode( + (store.getString(SettingKeys.notificationCurrentIds)) ?? '{}')); + int? currentInt; + try { + currentInt = idMap[roomId]; + } catch (_) { + currentInt = null; + } + if (currentInt != null) { + return currentInt; + } + var nCurrentInt = 0; + while (idMap.values.contains(currentInt)) { + nCurrentInt++; + } + idMap[roomId] = nCurrentInt; + await store.setString(SettingKeys.notificationCurrentIds, json.encode(idMap)); + return nCurrentInt; +} diff --git a/pubspec.lock b/pubspec.lock index 1f9c82b6ec..3466126306 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -980,9 +980,11 @@ packages: matrix: dependency: "direct main" description: - name: matrix - url: "https://pub.dartlang.org" - source: hosted + path: "." + ref: "krille/get-event-from-push" + resolved-ref: "70fa50e0e810ed951cd70fbf63924c5082d4f4a9" + url: "https://gitlab.com/famedly/company/frontend/famedlysdk.git" + source: git version: "0.8.18" matrix_api_lite: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index 8e2e8750a2..c629578829 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,8 +24,7 @@ dependencies: email_validator: ^2.0.1 emoji_picker_flutter: ^1.1.2 encrypt: ^5.0.1 - #fcm_shared_isolate: - # git: https://gitlab.com/famedly/libraries/fcm_shared_isolate.git + #fcm_shared_isolate: ^0.1.0 file_picker_cross: ^4.5.0 flutter: sdk: flutter @@ -142,4 +141,8 @@ dependency_overrides: git: url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git ref: null-safety + matrix: + git: + url: https://gitlab.com/famedly/company/frontend/famedlysdk.git + ref: krille/get-event-from-push provider: 5.0.0 diff --git a/scripts/enable-android-google-services.patch b/scripts/enable-android-google-services.patch index fc2b34b02b..665481f16d 100644 --- a/scripts/enable-android-google-services.patch +++ b/scripts/enable-android-google-services.patch @@ -1,5 +1,5 @@ diff --git a/android/app/build.gradle b/android/app/build.gradle -index 4e018b38..eebf7582 100644 +index ad9ffb87..37baafb1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -44,7 +44,7 @@ android { @@ -11,7 +11,7 @@ index 4e018b38..eebf7582 100644 targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName -@@ -81,7 +81,7 @@ flutter { +@@ -82,11 +82,11 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" @@ -20,7 +20,6 @@ index 4e018b38..eebf7582 100644 testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -@@ -89,4 +89,4 @@ dependencies { implementation 'androidx.multidex:multidex:2.0.1' } @@ -56,11 +55,11 @@ index 85aa8647..3b7e09e7 100644 } diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart -index 3347f176..e304abb4 100644 +index 00ca2aaa..8bb8a156 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart -@@ -39,7 +39,7 @@ import 'famedlysdk_store.dart'; - import 'matrix_sdk_extensions.dart/matrix_locals.dart'; +@@ -39,7 +39,7 @@ import '../config/setting_keys.dart'; + import 'famedlysdk_store.dart'; import 'platform_infos.dart'; -//import 'package:fcm_shared_isolate/fcm_shared_isolate.dart'; @@ -68,28 +67,25 @@ index 3347f176..e304abb4 100644 class NoTokenException implements Exception { String get cause => 'Cannot get firebase token'; -@@ -117,7 +117,7 @@ class BackgroundPush { - setupPush(); - } +@@ -65,7 +65,7 @@ class BackgroundPush { -- final dynamic _fcmSharedIsolate = null; //FcmSharedIsolate(); -+ final dynamic _fcmSharedIsolate = FcmSharedIsolate(); + final pendingTests = >{}; + +- final dynamic firebase = null; //FcmSharedIsolate(); ++ final dynamic firebase = FcmSharedIsolate(); + + DateTime? lastReceivedPush; - StreamSubscription? onLogin; - StreamSubscription? onRoomSync; diff --git a/pubspec.yaml b/pubspec.yaml -index 73b9eca2..5e6f9f16 100644 +index c6295788..8dd17ce4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml -@@ -25,8 +25,8 @@ dependencies: +@@ -24,7 +24,7 @@ dependencies: email_validator: ^2.0.1 emoji_picker_flutter: ^1.1.2 encrypt: ^5.0.1 -- #fcm_shared_isolate: -- # git: https://gitlab.com/famedly/libraries/fcm_shared_isolate.git -+ fcm_shared_isolate: -+ git: https://gitlab.com/famedly/libraries/fcm_shared_isolate.git +- #fcm_shared_isolate: ^0.1.0 ++ fcm_shared_isolate: ^0.1.0 file_picker_cross: ^4.5.0 flutter: sdk: flutter - \ No newline at end of file