From 1b1da8c4810bfd94e811d96c315eadbe5285226e Mon Sep 17 00:00:00 2001 From: otsmr Date: Wed, 12 Feb 2025 01:00:14 +0100 Subject: [PATCH] switch to google fcm fix #14 --- .vscode/launch.json | 25 ----- .vscode/settings.json | 5 - android/app/build.gradle | 23 +++- android/app/google-services.json | 48 +++++++++ android/settings.gradle | 3 + firebase.json | 1 + ios/Runner.xcodeproj/project.pbxproj | 4 + ios/Runner/GoogleService-Info.plist | 30 ++++++ lib/firebase_options.dart | 68 ++++++++++++ lib/main.dart | 14 +-- lib/src/app.dart | 18 ++-- lib/src/localization/app_de.arb | 1 + lib/src/localization/app_en.arb | 1 + lib/src/proto/api/client_to_server.pb.dart | 70 +++++++++++- .../proto/api/client_to_server.pbjson.dart | 30 ++++-- lib/src/proto/api/error.pbenum.dart | 2 + lib/src/proto/api/error.pbjson.dart | 3 +- lib/src/providers/api_provider.dart | 16 ++- lib/src/services/fcm_service.dart | 100 ++++++++++++++++++ lib/src/utils/misc.dart | 2 +- lib/src/views/settings/diagnostics_view.dart | 88 +++++++++++++++ lib/src/views/settings/help_view.dart | 13 ++- pubspec.lock | 56 ++++++++++ pubspec.yaml | 2 + 24 files changed, 558 insertions(+), 65 deletions(-) delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/settings.json create mode 100644 android/app/google-services.json create mode 100644 firebase.json create mode 100644 ios/Runner/GoogleService-Info.plist create mode 100644 lib/firebase_options.dart create mode 100644 lib/src/services/fcm_service.dart create mode 100644 lib/src/views/settings/diagnostics_view.dart diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 58a9b76..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - // Verwendet IntelliSense zum Ermitteln möglicher Attribute. - // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen. - // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Connect-App", - "request": "launch", - "type": "dart", - }, - { - "name": "Connect-App (profile mode)", - "request": "launch", - "type": "dart", - "flutterMode": "profile" - }, - { - "name": "Connect-App (release mode)", - "request": "launch", - "type": "dart", - "flutterMode": "release" - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 08cc630..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "cSpell.words": [ - "thounthends" - ] -} \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 1594b64..30543a3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,10 +1,19 @@ plugins { id "com.android.application" + // START: FlutterFire Configuration + id 'com.google.gms.google-services' + // END: FlutterFire Configuration id "kotlin-android" // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id "dev.flutter.flutter-gradle-plugin" } +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + android { namespace = "com.example.connect" // compileSdk = flutter.compileSdkVersion @@ -33,13 +42,19 @@ android { versionCode = flutter.versionCode versionName = flutter.versionName } + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.debug - } + signingConfig signingConfigs.release + } } } diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..5990240 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,48 @@ +{ + "project_info": { + "project_number": "650346093942", + "project_id": "twonly-ff605", + "storage_bucket": "twonly-ff605.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:650346093942:android:040816fb819b1bfb81ae57", + "android_client_info": { + "package_name": "eu.twonly" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyA4d5ORP11WpuVgmoYtWmOcMzZYWLPVtBk" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:650346093942:android:706cb87c3131dabe81ae57", + "android_client_info": { + "package_name": "eu.twonly.testing" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyA4d5ORP11WpuVgmoYtWmOcMzZYWLPVtBk" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle index b5e1b3f..f6965fa 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,6 +19,9 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "8.3.2" apply false + // START: FlutterFire Configuration + id "com.google.gms.google-services" version "4.3.15" apply false + // END: FlutterFire Configuration id "org.jetbrains.kotlin.android" version "2.0.20" apply false } diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..16d24a3 --- /dev/null +++ b/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"twonly-ff605","appId":"1:650346093942:android:706cb87c3131dabe81ae57","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"twonly-ff605","appId":"1:650346093942:ios:e80075ff3de823c581ae57","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"twonly-ff605","configurations":{"android":"1:650346093942:android:706cb87c3131dabe81ae57","ios":"1:650346093942:ios:e80075ff3de823c581ae57"}}}}}} \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index b485680..48e3c98 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -42,6 +43,7 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; @@ -94,6 +96,7 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */, ); sourceTree = ""; }; @@ -216,6 +219,7 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..cc43006 --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyDr7MEoz2XvrYxU0kYvZVVjej53q0gANnE + GCM_SENDER_ID + 650346093942 + PLIST_VERSION + 1 + BUNDLE_ID + com.example.connect + PROJECT_ID + twonly-ff605 + STORAGE_BUCKET + twonly-ff605.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:650346093942:ios:e80075ff3de823c581ae57 + + \ No newline at end of file diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..0c7d436 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,68 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyA4d5ORP11WpuVgmoYtWmOcMzZYWLPVtBk', + appId: '1:650346093942:android:706cb87c3131dabe81ae57', + messagingSenderId: '650346093942', + projectId: 'twonly-ff605', + storageBucket: 'twonly-ff605.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyDr7MEoz2XvrYxU0kYvZVVjej53q0gANnE', + appId: '1:650346093942:ios:e80075ff3de823c581ae57', + messagingSenderId: '650346093942', + projectId: 'twonly-ff605', + storageBucket: 'twonly-ff605.firebasestorage.app', + iosBundleId: 'com.example.connect', + ); +} diff --git a/lib/main.dart b/lib/main.dart index 472e851..d443f8e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,7 +12,9 @@ import 'package:twonly/src/providers/messages_change_provider.dart'; import 'package:twonly/src/providers/contacts_change_provider.dart'; import 'package:twonly/src/providers/send_next_media_to.dart'; import 'package:twonly/src/providers/settings_change_provider.dart'; +import 'package:twonly/src/services/fcm_service.dart'; import 'package:twonly/src/services/notification_service.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'src/app.dart'; void main() async { @@ -26,16 +28,16 @@ void main() async { Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL; Logger.root.onRecord.listen((record) { - // if (kReleaseMode) { - // writeLogToFile(record); - // } else { - print( - '${record.level.name}: twonly:${record.loggerName}: ${record.message}'); - // } + writeLogToFile(record); + if (kDebugMode) { + print( + '${record.level.name}: twonly:${record.loggerName}: ${record.message}'); + } }); await setupPushNotification(); await initMediaStorage(); + await initFCMService(); dbProvider = DbProvider(); await dbProvider.ready; diff --git a/lib/src/app.dart b/lib/src/app.dart index 4ec6d69..9e737a6 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -73,17 +73,17 @@ class _MyAppState extends State with WidgetsBindingObserver { .updateLastMessageFor(userId, messageId); }; - WidgetsBinding.instance.addPostFrameCallback((_) { - _requestPermissions(); - _initService(); - }); + // WidgetsBinding.instance.addPostFrameCallback((_) { + // _requestPermissions(); + // _initService(); + // }); initAsync(); } Future initAsync() async { // make sure the front end service will be killed - FlutterForegroundTask.sendDataToTask(""); - await FlutterForegroundTask.stopService(); + // FlutterForegroundTask.sendDataToTask(""); + // await FlutterForegroundTask.stopService(); // connect async to the backend api apiProvider.connect(); } @@ -177,13 +177,15 @@ class _MyAppState extends State with WidgetsBindingObserver { if (state == AppLifecycleState.resumed) { if (wasPaused) { globalIsAppInBackground = false; - _stopService(); + apiProvider.connect(); + // _stopService(); } } else if (state == AppLifecycleState.paused) { wasPaused = true; globalIsAppInBackground = true; apiProvider.close(() { - _startService(); + // use this only when uploading an image + // _startService(); }); } } diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 6f4d92a..3ed667d 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -57,6 +57,7 @@ "settingsNotification": "Benachrichtigung", "settingsHelp": "Hilfe", "settingsHelpSupport": "Support-Center", + "settingsHelpDiagnostics": "Diagnoseprotokoll", "settingsHelpVersion": "Version", "settingsHelpLicenses": "Lizenzen", "settingsHelpLegal": "Nutzungsbedingungen & Datenschutzrichtlinie", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 564aad8..9202ddf 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -56,6 +56,7 @@ "settingsPrivacyBlockUsersCount": "{len} contact(s)", "settingsNotification": "Notification", "settingsHelp": "Help", + "settingsHelpDiagnostics": "Diagnostic protocol", "settingsHelpSupport": "Support Center", "settingsHelpVersion": "Version", "settingsHelpLicenses": "Licenses", diff --git a/lib/src/proto/api/client_to_server.pb.dart b/lib/src/proto/api/client_to_server.pb.dart index d32219a..3b5d3ce 100644 --- a/lib/src/proto/api/client_to_server.pb.dart +++ b/lib/src/proto/api/client_to_server.pb.dart @@ -641,6 +641,56 @@ class ApplicationData_GetUserByUsername extends $pb.GeneratedMessage { void clearUsername() => clearField(1); } +class ApplicationData_UpdateGoogleFcmToken extends $pb.GeneratedMessage { + factory ApplicationData_UpdateGoogleFcmToken({ + $core.String? googleFcm, + }) { + final $result = create(); + if (googleFcm != null) { + $result.googleFcm = googleFcm; + } + return $result; + } + ApplicationData_UpdateGoogleFcmToken._() : super(); + factory ApplicationData_UpdateGoogleFcmToken.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ApplicationData_UpdateGoogleFcmToken.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ApplicationData.UpdateGoogleFcmToken', package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'googleFcm') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ApplicationData_UpdateGoogleFcmToken clone() => ApplicationData_UpdateGoogleFcmToken()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ApplicationData_UpdateGoogleFcmToken copyWith(void Function(ApplicationData_UpdateGoogleFcmToken) updates) => super.copyWith((message) => updates(message as ApplicationData_UpdateGoogleFcmToken)) as ApplicationData_UpdateGoogleFcmToken; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ApplicationData_UpdateGoogleFcmToken create() => ApplicationData_UpdateGoogleFcmToken._(); + ApplicationData_UpdateGoogleFcmToken createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ApplicationData_UpdateGoogleFcmToken getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ApplicationData_UpdateGoogleFcmToken? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get googleFcm => $_getSZ(0); + @$pb.TagNumber(1) + set googleFcm($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasGoogleFcm() => $_has(0); + @$pb.TagNumber(1) + void clearGoogleFcm() => clearField(1); +} + class ApplicationData_GetUserById extends $pb.GeneratedMessage { factory ApplicationData_GetUserById({ $fixnum.Int64? userId, @@ -923,6 +973,7 @@ enum ApplicationData_ApplicationData { uploaddata, getuserbyid, downloaddata, + updategooglefcmtoken, notSet } @@ -935,6 +986,7 @@ class ApplicationData extends $pb.GeneratedMessage { ApplicationData_UploadData? uploaddata, ApplicationData_GetUserById? getuserbyid, ApplicationData_DownloadData? downloaddata, + ApplicationData_UpdateGoogleFcmToken? updategooglefcmtoken, }) { final $result = create(); if (textmessage != null) { @@ -958,6 +1010,9 @@ class ApplicationData extends $pb.GeneratedMessage { if (downloaddata != null) { $result.downloaddata = downloaddata; } + if (updategooglefcmtoken != null) { + $result.updategooglefcmtoken = updategooglefcmtoken; + } return $result; } ApplicationData._() : super(); @@ -972,10 +1027,11 @@ class ApplicationData extends $pb.GeneratedMessage { 5 : ApplicationData_ApplicationData.uploaddata, 6 : ApplicationData_ApplicationData.getuserbyid, 7 : ApplicationData_ApplicationData.downloaddata, + 8 : ApplicationData_ApplicationData.updategooglefcmtoken, 0 : ApplicationData_ApplicationData.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ApplicationData', package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) - ..oo(0, [1, 2, 3, 4, 5, 6, 7]) + ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8]) ..aOM(1, _omitFieldNames ? '' : 'textmessage', subBuilder: ApplicationData_TextMessage.create) ..aOM(2, _omitFieldNames ? '' : 'getuserbyusername', subBuilder: ApplicationData_GetUserByUsername.create) ..aOM(3, _omitFieldNames ? '' : 'getprekeysbyuserid', subBuilder: ApplicationData_GetPrekeysByUserId.create) @@ -983,6 +1039,7 @@ class ApplicationData extends $pb.GeneratedMessage { ..aOM(5, _omitFieldNames ? '' : 'uploaddata', subBuilder: ApplicationData_UploadData.create) ..aOM(6, _omitFieldNames ? '' : 'getuserbyid', subBuilder: ApplicationData_GetUserById.create) ..aOM(7, _omitFieldNames ? '' : 'downloaddata', subBuilder: ApplicationData_DownloadData.create) + ..aOM(8, _omitFieldNames ? '' : 'updategooglefcmtoken', subBuilder: ApplicationData_UpdateGoogleFcmToken.create) ..hasRequiredFields = false ; @@ -1086,6 +1143,17 @@ class ApplicationData extends $pb.GeneratedMessage { void clearDownloaddata() => clearField(7); @$pb.TagNumber(7) ApplicationData_DownloadData ensureDownloaddata() => $_ensure(6); + + @$pb.TagNumber(8) + ApplicationData_UpdateGoogleFcmToken get updategooglefcmtoken => $_getN(7); + @$pb.TagNumber(8) + set updategooglefcmtoken(ApplicationData_UpdateGoogleFcmToken v) { setField(8, v); } + @$pb.TagNumber(8) + $core.bool hasUpdategooglefcmtoken() => $_has(7); + @$pb.TagNumber(8) + void clearUpdategooglefcmtoken() => clearField(8); + @$pb.TagNumber(8) + ApplicationData_UpdateGoogleFcmToken ensureUpdategooglefcmtoken() => $_ensure(7); } class Response_PreKey extends $pb.GeneratedMessage { diff --git a/lib/src/proto/api/client_to_server.pbjson.dart b/lib/src/proto/api/client_to_server.pbjson.dart index 8c912fc..e8e3f97 100644 --- a/lib/src/proto/api/client_to_server.pbjson.dart +++ b/lib/src/proto/api/client_to_server.pbjson.dart @@ -122,8 +122,9 @@ const ApplicationData$json = { {'1': 'uploaddata', '3': 5, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.UploadData', '9': 0, '10': 'uploaddata'}, {'1': 'getuserbyid', '3': 6, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetUserById', '9': 0, '10': 'getuserbyid'}, {'1': 'downloaddata', '3': 7, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.DownloadData', '9': 0, '10': 'downloaddata'}, + {'1': 'updategooglefcmtoken', '3': 8, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.UpdateGoogleFcmToken', '9': 0, '10': 'updategooglefcmtoken'}, ], - '3': [ApplicationData_TextMessage$json, ApplicationData_GetUserByUsername$json, ApplicationData_GetUserById$json, ApplicationData_GetPrekeysByUserId$json, ApplicationData_GetUploadToken$json, ApplicationData_UploadData$json, ApplicationData_DownloadData$json], + '3': [ApplicationData_TextMessage$json, ApplicationData_GetUserByUsername$json, ApplicationData_UpdateGoogleFcmToken$json, ApplicationData_GetUserById$json, ApplicationData_GetPrekeysByUserId$json, ApplicationData_GetUploadToken$json, ApplicationData_UploadData$json, ApplicationData_DownloadData$json], '8': [ {'1': 'ApplicationData'}, ], @@ -146,6 +147,14 @@ const ApplicationData_GetUserByUsername$json = { ], }; +@$core.Deprecated('Use applicationDataDescriptor instead') +const ApplicationData_UpdateGoogleFcmToken$json = { + '1': 'UpdateGoogleFcmToken', + '2': [ + {'1': 'google_fcm', '3': 1, '4': 1, '5': 9, '10': 'googleFcm'}, + ], +}; + @$core.Deprecated('Use applicationDataDescriptor instead') const ApplicationData_GetUserById$json = { '1': 'GetUserById', @@ -199,14 +208,17 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( 'dGlvbkRhdGEuVXBsb2FkRGF0YUgAUgp1cGxvYWRkYXRhElEKC2dldHVzZXJieWlkGAYgASgLMi' '0uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuR2V0VXNlckJ5SWRIAFILZ2V0dXNl' 'cmJ5aWQSVAoMZG93bmxvYWRkYXRhGAcgASgLMi4uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdG' - 'lvbkRhdGEuRG93bmxvYWREYXRhSABSDGRvd25sb2FkZGF0YRo6CgtUZXh0TWVzc2FnZRIXCgd1' - 'c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgDIAEoDFIEYm9keRovChFHZXRVc2VyQnlVc2' - 'VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaJgoLR2V0VXNlckJ5SWQSFwoHdXNl' - 'cl9pZBgBIAEoA1IGdXNlcklkGi0KEkdldFByZWtleXNCeVVzZXJJZBIXCgd1c2VyX2lkGAEgAS' - 'gDUgZ1c2VySWQaEAoOR2V0VXBsb2FkVG9rZW4aWwoKVXBsb2FkRGF0YRIhCgx1cGxvYWRfdG9r' - 'ZW4YASABKAxSC3VwbG9hZFRva2VuEhYKBm9mZnNldBgCIAEoDVIGb2Zmc2V0EhIKBGRhdGEYAy' - 'ABKAxSBGRhdGEaSQoMRG93bmxvYWREYXRhEiEKDHVwbG9hZF90b2tlbhgBIAEoDFILdXBsb2Fk' - 'VG9rZW4SFgoGb2Zmc2V0GAIgASgNUgZvZmZzZXRCEQoPQXBwbGljYXRpb25EYXRh'); + 'lvbkRhdGEuRG93bmxvYWREYXRhSABSDGRvd25sb2FkZGF0YRJsChR1cGRhdGVnb29nbGVmY210' + 'b2tlbhgIIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLlVwZGF0ZUdvb2' + 'dsZUZjbVRva2VuSABSFHVwZGF0ZWdvb2dsZWZjbXRva2VuGjoKC1RleHRNZXNzYWdlEhcKB3Vz' + 'ZXJfaWQYASABKANSBnVzZXJJZBISCgRib2R5GAMgASgMUgRib2R5Gi8KEUdldFVzZXJCeVVzZX' + 'JuYW1lEhoKCHVzZXJuYW1lGAEgASgJUgh1c2VybmFtZRo1ChRVcGRhdGVHb29nbGVGY21Ub2tl' + 'bhIdCgpnb29nbGVfZmNtGAEgASgJUglnb29nbGVGY20aJgoLR2V0VXNlckJ5SWQSFwoHdXNlcl' + '9pZBgBIAEoA1IGdXNlcklkGi0KEkdldFByZWtleXNCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgD' + 'UgZ1c2VySWQaEAoOR2V0VXBsb2FkVG9rZW4aWwoKVXBsb2FkRGF0YRIhCgx1cGxvYWRfdG9rZW' + '4YASABKAxSC3VwbG9hZFRva2VuEhYKBm9mZnNldBgCIAEoDVIGb2Zmc2V0EhIKBGRhdGEYAyAB' + 'KAxSBGRhdGEaSQoMRG93bmxvYWREYXRhEiEKDHVwbG9hZF90b2tlbhgBIAEoDFILdXBsb2FkVG' + '9rZW4SFgoGb2Zmc2V0GAIgASgNUgZvZmZzZXRCEQoPQXBwbGljYXRpb25EYXRh'); @$core.Deprecated('Use responseDescriptor instead') const Response$json = { diff --git a/lib/src/proto/api/error.pbenum.dart b/lib/src/proto/api/error.pbenum.dart index 2bb60e5..d2d4a26 100644 --- a/lib/src/proto/api/error.pbenum.dart +++ b/lib/src/proto/api/error.pbenum.dart @@ -30,6 +30,7 @@ class ErrorCode extends $pb.ProtobufEnum { static const ErrorCode UploadLimitReached = ErrorCode._(1011, _omitEnumNames ? '' : 'UploadLimitReached'); static const ErrorCode InvalidUpdateToken = ErrorCode._(1012, _omitEnumNames ? '' : 'InvalidUpdateToken'); static const ErrorCode InvalidOffset = ErrorCode._(1013, _omitEnumNames ? '' : 'InvalidOffset'); + static const ErrorCode InvalidGoogleFcmToken = ErrorCode._(1014, _omitEnumNames ? '' : 'InvalidGoogleFcmToken'); static const $core.List values = [ Unknown, @@ -48,6 +49,7 @@ class ErrorCode extends $pb.ProtobufEnum { UploadLimitReached, InvalidUpdateToken, InvalidOffset, + InvalidGoogleFcmToken, ]; static final $core.Map<$core.int, ErrorCode> _byValue = $pb.ProtobufEnum.initByValue(values); diff --git a/lib/src/proto/api/error.pbjson.dart b/lib/src/proto/api/error.pbjson.dart index 7b1e994..68b54d9 100644 --- a/lib/src/proto/api/error.pbjson.dart +++ b/lib/src/proto/api/error.pbjson.dart @@ -33,6 +33,7 @@ const ErrorCode$json = { {'1': 'UploadLimitReached', '2': 1011}, {'1': 'InvalidUpdateToken', '2': 1012}, {'1': 'InvalidOffset', '2': 1013}, + {'1': 'InvalidGoogleFcmToken', '2': 1014}, ], }; @@ -45,5 +46,5 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode( 'bGljS2V5EO8HEiAKG1Nlc3Npb25BbHJlYWR5QXV0aGVudGljYXRlZBDwBxIcChdTZXNzaW9uTm' '90QXV0aGVudGljYXRlZBDxBxIaChVPbmx5T25lU2Vzc2lvbkFsbG93ZWQQ8gcSFwoSVXBsb2Fk' 'TGltaXRSZWFjaGVkEPMHEhcKEkludmFsaWRVcGRhdGVUb2tlbhD0BxISCg1JbnZhbGlkT2Zmc2' - 'V0EPUH'); + 'V0EPUHEhoKFUludmFsaWRHb29nbGVGY21Ub2tlbhD2Bw=='); diff --git a/lib/src/providers/api_provider.dart b/lib/src/providers/api_provider.dart index 24c3836..a51ea13 100644 --- a/lib/src/providers/api_provider.dart +++ b/lib/src/providers/api_provider.dart @@ -11,6 +11,7 @@ import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server; import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api_utils.dart'; import 'package:twonly/src/providers/api/server_messages.dart'; +import 'package:twonly/src/services/fcm_service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; // ignore: library_prefixes @@ -59,6 +60,11 @@ class ApiProvider { } } + // Function is called after the user is authenticated at the server + Future onAuthenticated() async { + initFCMAfterAuthenticated(); + } + Future onConnected() async { await authenticate(); globalCallbackConnectionState(true); @@ -212,7 +218,6 @@ class ApiProvider { } Future authenticate() async { - print("try authenticate $isAuthenticated"); if (isAuthenticated) return; if (await SignalHelper.getSignalIdentity() == null) { return; @@ -221,7 +226,6 @@ class ApiProvider { var handshake = Handshake()..getchallenge = Handshake_GetChallenge(); var req = createClientToServerFromHandshake(handshake); - print("try authenticate send to server"); final result = await _sendRequestV0(req, authenticated: false); if (result.isError) { log.shout("Error auth", result); @@ -253,6 +257,7 @@ class ApiProvider { } log.info("Authenticated!"); + onAuthenticated(); isAuthenticated = true; } @@ -329,6 +334,13 @@ class ApiProvider { return await _sendRequestV0(req); } + Future updateFCMToken(String googleFcm) async { + var get = ApplicationData_UpdateGoogleFcmToken()..googleFcm = googleFcm; + var appData = ApplicationData()..updategooglefcmtoken = get; + var req = createClientToServerFromApplicationData(appData); + return await _sendRequestV0(req); + } + Future sendTextMessage(Int64 target, Uint8List msg) async { var testMessage = ApplicationData_TextMessage() ..userId = target diff --git a/lib/src/services/fcm_service.dart b/lib/src/services/fcm_service.dart new file mode 100644 index 0000000..1794eb4 --- /dev/null +++ b/lib/src/services/fcm_service.dart @@ -0,0 +1,100 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:logging/logging.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/app.dart'; +import 'package:twonly/src/providers/api_provider.dart'; +import 'package:twonly/src/providers/db_provider.dart'; +import 'package:twonly/src/utils/misc.dart'; + +import '../../firebase_options.dart'; + +// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de + +Future initFCMAfterAuthenticated() async { + if (globalIsAppInBackground) return; + + final storage = getSecureStorage(); + + String? storedToken = await storage.read(key: "google_fcm"); + + final fcmToken = await FirebaseMessaging.instance.getToken(); + if (fcmToken == null) { + Logger("init_fcm_service").shout("Error getting fcmToken"); + return; + } + + if (storedToken == null || fcmToken != storedToken) { + await apiProvider.updateFCMToken(fcmToken); + await storage.write(key: "google_fcm", value: fcmToken); + } + + FirebaseMessaging.instance.onTokenRefresh.listen((fcmToken) async { + await apiProvider.updateFCMToken(fcmToken); + await storage.write(key: "google_fcm", value: fcmToken); + }).onError((err) { + // Logger("init_fcm_service").shout("Error getting fcmToken"); + }); +} + +Future initFCMService() async { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + // You may set the permission requests to "provisional" which allows the user to choose what type + // of notifications they would like to receive once the user receives a notification. + // final notificationSettings = + await FirebaseMessaging.instance.requestPermission(provisional: true); + + // For apple platforms, ensure the APNS token is available before making any FCM plugin API calls + // final apnsToken = await FirebaseMessaging.instance.getAPNSToken(); + // if (apnsToken != null) { + // APNS token is available, make FCM plugin API requests... + // } + + FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); + + FirebaseMessaging.onMessage.listen((RemoteMessage message) { + print('Got a message whilst in the foreground!'); + print('Message data: ${message.data}'); + + if (message.notification != null) { + print('Message also contained a notification: ${message.notification}'); + } + }); +} + +@pragma('vm:entry-point') +Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { + // If you're going to use other Firebase services in the background, such as Firestore, + // make sure you call `initializeApp` before using other Firebase services. + + // Wenn Tasks länger als 30 Sekunden ausgeführt werden, wird der Prozess möglicherweise automatisch vom Gerät beendet. + // -> offer backend via http? + + print("Handling a background message: ${message.messageId}"); + + bool gotMessage = false; + + globalCallBackOnMessageChange = (a, b) async { + gotMessage = true; + print("Got message can exit"); + }; + + dbProvider = DbProvider(); + await dbProvider.ready; + apiProvider = ApiProvider(); + await apiProvider.connect(); + + final stopwatch = Stopwatch()..start(); + while (!gotMessage) { + print("gotMessage: $gotMessage"); + if (stopwatch.elapsed >= Duration(seconds: 20)) { + Logger("firebase-background").shout('Timeout reached. Exiting the loop.'); + break; // Exit the loop if the timeout is reached. + } + await Future.delayed(Duration(milliseconds: 10)); + } + await apiProvider.close(() {}); +} diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 70aa4b4..7606f48 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -34,7 +34,7 @@ Future writeLogToFile(LogRecord record) async { // Prepare the log message final logMessage = - '${record.level.name}: ${record.loggerName}: ${record.message}\n'; + '${DateTime.now()}: ${record.level.name}: ${record.loggerName}: ${record.message}\n'; // Append the log message to the file await logFile.writeAsString(logMessage, mode: FileMode.append); diff --git a/lib/src/views/settings/diagnostics_view.dart b/lib/src/views/settings/diagnostics_view.dart new file mode 100644 index 0000000..6356bd2 --- /dev/null +++ b/lib/src/views/settings/diagnostics_view.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'dart:io'; +import 'package:path_provider/path_provider.dart'; +import 'package:flutter/services.dart'; + +class DiagnosticsView extends StatelessWidget { + const DiagnosticsView({super.key}); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _loadLogFile(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else { + final logText = snapshot.data ?? ''; + + return Scaffold( + appBar: AppBar(title: const Text('Diagnostics')), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Text(logText), + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: logText)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Log copied to clipboard!')), + ); + }, + child: const Text('Copy All Text'), + ), + TextButton( + onPressed: () async { + final directory = + await getApplicationDocumentsDirectory(); + final logFile = File('${directory.path}/app.log'); + + if (await logFile.exists()) { + await logFile.delete(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Log file deleted!')), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Log file does not exist.')), + ); + } + }, + child: const Text('Delete Log File'), + ), + ], + ), + ), + ], + ), + ); + } + }, + ); + } + + Future _loadLogFile() async { + final directory = await getApplicationDocumentsDirectory(); + final logFile = File('${directory.path}/app.log'); + + if (await logFile.exists()) { + return await logFile.readAsString(); + } else { + return 'Log file does not exist.'; + } + } +} diff --git a/lib/src/views/settings/help_view.dart b/lib/src/views/settings/help_view.dart index c131fa3..c52017f 100644 --- a/lib/src/views/settings/help_view.dart +++ b/lib/src/views/settings/help_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/settings/diagnostics_view.dart'; import 'package:url_launcher/url_launcher.dart'; class HelpView extends StatelessWidget { @@ -43,9 +44,15 @@ class HelpView extends StatelessWidget { showLicensePage(context: context); }, ), - - // Diagnoseprotokoll - + ListTile( + title: Text(context.lang.settingsHelpDiagnostics), + onTap: () async { + await Navigator.push(context, + MaterialPageRoute(builder: (context) { + return DiagnosticsView(); + })); + }, + ), ListTile( title: Text(context.lang.settingsHelpLegal), onTap: () { diff --git a/pubspec.lock b/pubspec.lock index 1ef1b9d..0518c02 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "80.0.0" + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: e051259913915ea5bc8fe18664596bea08592fd123930605d562969cd7315fcd + url: "https://pub.dev" + source: hosted + version: "1.3.51" adaptive_number: dependency: transitive description: @@ -305,6 +313,54 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "93dc4dd12f9b02c5767f235307f609e61ed9211047132d07f9e02c668f0bfc33" + url: "https://pub.dev" + source: hosted + version: "3.11.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf + url: "https://pub.dev" + source: hosted + version: "5.4.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "0e13c80f0de8acaa5d0519cbe23c8b4cc138a2d5d508b5755c861bdfc9762678" + url: "https://pub.dev" + source: hosted + version: "2.20.0" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + sha256: "3dee3b0cbfe719e64773cb7d1cad57c58b2235a8c136f5715fe733a54058c783" + url: "https://pub.dev" + source: hosted + version: "15.2.2" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: e9ea726b9bb864fc6223bb66422bd9877b9973ae51967754a769b0d01e201c1e + url: "https://pub.dev" + source: hosted + version: "4.6.2" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "5f7b40e8bf861a37f8b8196e347d8a919750421a45f0b45d1bb74e98fa72726e" + url: "https://pub.dev" + source: hosted + version: "3.10.2" fixnum: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d8cb5b7..a689a89 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,8 @@ dependencies: connectivity_plus: ^6.1.2 cv: ^1.1.3 exif: ^3.3.0 + firebase_core: ^3.11.0 + firebase_messaging: ^15.2.2 fixnum: ^1.1.1 flutter: sdk: flutter