From 5a2049fd4a766672ef5b1e25dbb3860be58cfdce Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 14 Mar 2026 15:05:10 +0100 Subject: [PATCH] support background execution --- CHANGELOG.md | 1 + android/app/src/main/AndroidManifest.xml | 2 +- .../main/kotlin/eu/twonly/MyApplication.kt | 13 ++ ios/Runner/AppDelegate.swift | 12 ++ ios/Runner/Info.plist | 79 ++++++----- lib/globals.dart | 2 + lib/main.dart | 4 +- lib/src/constants/keyvalue.keys.dart | 4 + lib/src/database/daos/mediafiles.dao.dart | 1 + lib/src/database/daos/receipts.dao.dart | 2 + lib/src/services/api.service.dart | 27 +++- .../api/client2client/reaction.c2c.dart | 3 + .../mediafiles/media_background.service.dart | 49 +++---- .../api/mediafiles/upload.service.dart | 68 ++++++++- lib/src/services/api/server_messages.dart | 1 + .../callback_dispatcher.background.dart | 130 ++++++++++++++++++ .../fcm.notifications.dart} | 14 +- lib/src/views/settings/notification.view.dart | 2 +- 18 files changed, 333 insertions(+), 81 deletions(-) create mode 100644 android/app/src/main/kotlin/eu/twonly/MyApplication.kt create mode 100644 lib/src/constants/keyvalue.keys.dart create mode 100644 lib/src/services/background/callback_dispatcher.background.dart rename lib/src/services/{fcm.service.dart => notifications/fcm.notifications.dart} (92%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ac67b2..9b1118b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.0.98 - New: Groups can now collect flames as well +- New: Background execution to pre-load messages - New: Adds a link if the image contains a QR code - Improve: Video compression with progress updates - Improve: Show message "Flames restored" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b090475..3149d62 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ diff --git a/android/app/src/main/kotlin/eu/twonly/MyApplication.kt b/android/app/src/main/kotlin/eu/twonly/MyApplication.kt new file mode 100644 index 0000000..231e247 --- /dev/null +++ b/android/app/src/main/kotlin/eu/twonly/MyApplication.kt @@ -0,0 +1,13 @@ +package eu.twonly + +import io.flutter.app.FlutterApplication +import dev.fluttercommunity.workmanager.WorkmanagerDebug +import dev.fluttercommunity.workmanager.LoggingDebugHandler + +class MyApplication : FlutterApplication() { + override fun onCreate() { + super.onCreate() + // This enables the internal plugin logging to Logcat + WorkmanagerDebug.setCurrent(LoggingDebugHandler()) + } +} \ No newline at end of file diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index ae011d7..7b546b5 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -4,6 +4,7 @@ import Foundation import UIKit import UserNotifications import flutter_sharing_intent +import workmanager_apple @main @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { @@ -16,6 +17,17 @@ import flutter_sharing_intent if let registrar = self.registrar(forPlugin: "VideoCompressionChannel") { VideoCompressionChannel.register(with: registrar.messenger()) } + + WorkmanagerDebug.setCurrent(LoggingDebugHandler()) + + WorkmanagerPlugin.registerPeriodicTask( + withIdentifier: "eu.twonly.periodic_task", + frequency: NSNumber(value: 20 * 60) + ) + + WorkmanagerPlugin.registerBGProcessingTask( + withIdentifier: "eu.twonly.processing_task" + ) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 8b4c63d..a39646a 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,27 +2,8 @@ - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneClassName - UIWindowScene - UISceneDelegateClassName - FlutterSceneDelegate - UISceneConfigurationName - flutter - UISceneStoryboardFile - Main - - - - + AppGroupId + $(CUSTOM_GROUP_ID) CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion @@ -43,6 +24,17 @@ $(FLUTTER_BUILD_NAME) CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + SharingMedia-$(PRODUCT_BUNDLE_IDENTIFIER) + + + CFBundleVersion $(FLUTTER_BUILD_NUMBER) FIREBASE_ANALYTICS_COLLECTION_ENABLED @@ -51,10 +43,10 @@ FlutterDeepLinkingEnabled - LSRequiresIPhoneOS - ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS + NSCameraUsageDescription Use your camera to make photos or videos and share them encrypted with your friends. NSFaceIDUsageDescription @@ -63,12 +55,39 @@ Use your microphone to enable audio when making videos. NSPhotoLibraryUsageDescription twonly will save photos or videos to your library. + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + UIApplicationSupportsIndirectInputEvents UIBackgroundModes fetch remote-notification + processing + + BGTaskSchedulerPermittedIdentifiers + + eu.twonly.periodic_task + eu.twonly.processing_task UILaunchStoryboardName LaunchScreen @@ -89,19 +108,5 @@ firebase_performance_collection_deactivated - - AppGroupId - $(CUSTOM_GROUP_ID) - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - SharingMedia-$(PRODUCT_BUNDLE_IDENTIFIER) - - - diff --git a/lib/globals.dart b/lib/globals.dart index 8731ff1..804a918 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -31,7 +31,9 @@ void Function(SubscriptionPlan plan) globalCallbackUpdatePlan = (plan) {}; Map globalUserDataChangedCallBack = {}; bool globalIsAppInBackground = true; +bool globalIsInBackgroundTask = false; bool globalAllowErrorTrackingViaSentry = false; +bool globalGotMessageFromServer = false; late String globalApplicationCacheDirectory; late String globalApplicationSupportDirectory; diff --git a/lib/main.dart b/lib/main.dart index 600d3a7..84249c4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,9 +17,10 @@ import 'package:twonly/src/services/api.service.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/mediafiles/media_background.service.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/services/background/callback_dispatcher.background.dart'; import 'package:twonly/src/services/backup/create.backup.dart'; -import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/log.dart'; @@ -46,6 +47,7 @@ void main() async { } unawaited(performTwonlySafeBackup()); + unawaited(initializeBackgroundTaskManager()); } else { Log.info('User is not yet register. Ensure all local data is removed.'); await deleteLocalUserData(); diff --git a/lib/src/constants/keyvalue.keys.dart b/lib/src/constants/keyvalue.keys.dart new file mode 100644 index 0000000..4b5e1cf --- /dev/null +++ b/lib/src/constants/keyvalue.keys.dart @@ -0,0 +1,4 @@ +class KeyValueKeys { + static const String lastPeriodicTaskExecution = + 'last_periodic_task_execution'; +} diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index 7edd600..1bbcc54 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -121,6 +121,7 @@ class MediaFilesDao extends DatabaseAccessor ..where( (t) => (t.uploadState.equals(UploadState.initialized.name) | t.uploadState.equals(UploadState.uploadLimitReached.name) | + t.uploadState.equals(UploadState.uploading.name) | t.uploadState.equals(UploadState.preprocessing.name)), )) .get(); diff --git a/lib/src/database/daos/receipts.dao.dart b/lib/src/database/daos/receipts.dao.dart index 4b6a6a1..4190444 100644 --- a/lib/src/database/daos/receipts.dao.dart +++ b/lib/src/database/daos/receipts.dao.dart @@ -4,6 +4,7 @@ import 'package:hashlib/random.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/receipts.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/utils/log.dart'; part 'receipts.dao.g.dart'; @@ -33,6 +34,7 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { type: const Value(MessageActionType.ackByUserAt), ), ); + await handleMediaRelatedResponseFromReceiver(receipt.messageId!); } await (delete(receipts) diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index ec235f2..8f2b606 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -30,9 +30,9 @@ import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/server_messages.dart'; import 'package:twonly/src/services/api/utils.dart'; -import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/group.services.dart'; +import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; @@ -90,7 +90,11 @@ class ApiService { await initFCMAfterAuthenticated(); globalCallbackConnectionState(isConnected: true); - if (!globalIsAppInBackground) { + if (globalIsInBackgroundTask) { + await retransmitRawBytes(); + await tryTransmitMessages(); + await tryDownloadAllMediaFiles(); + } else if (!globalIsAppInBackground) { unawaited(retransmitRawBytes()); unawaited(tryTransmitMessages()); unawaited(tryDownloadAllMediaFiles()); @@ -124,6 +128,7 @@ class ApiService { } Future startReconnectionTimer() async { + if (globalIsInBackgroundTask) return; if (reconnectionTimer?.isActive ?? false) { return; } @@ -330,7 +335,9 @@ class ApiService { } } if (res.isError) { - Log.warn('Got error from server: ${res.error}'); + if (res.error != ErrorCode.ForegroundSessionConnected) { + Log.warn('Got error from server: ${res.error}'); + } if (res.error == ErrorCode.AppVersionOutdated) { globalCallbackAppIsOutdated(); Log.warn('App Version is OUTDATED.'); @@ -395,6 +402,7 @@ class ApiService { ..userId = Int64(userId) ..appVersion = (await PackageInfo.fromPlatform()).version ..deviceId = Int64(user.deviceId) + ..inBackground = globalIsInBackgroundTask ..authToken = base64Decode(apiAuthToken); final handshake = Handshake()..authenticate = authenticate; @@ -404,12 +412,19 @@ class ApiService { if (result.isSuccess) { Log.info('websocket is authenticated'); - unawaited(onAuthenticated()); + if (globalIsInBackgroundTask) { + await onAuthenticated(); + } else { + unawaited(onAuthenticated()); + } return true; } if (result.isError) { - if (result.error != ErrorCode.AuthTokenNotValid) { - Log.error('got error while authenticating to the server: $result'); + if (result.error != ErrorCode.AuthTokenNotValid && + result.error != ErrorCode.ForegroundSessionConnected) { + Log.error( + 'got error while authenticating to the server: ${result.error}', + ); return false; } } diff --git a/lib/src/services/api/client2client/reaction.c2c.dart b/lib/src/services/api/client2client/reaction.c2c.dart index ffad995..9d9b5e9 100644 --- a/lib/src/services/api/client2client/reaction.c2c.dart +++ b/lib/src/services/api/client2client/reaction.c2c.dart @@ -1,6 +1,7 @@ import 'package:clock/clock.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/utils/log.dart'; Future handleReaction( @@ -17,6 +18,8 @@ Future handleReaction( reaction.remove, ); + await handleMediaRelatedResponseFromReceiver(reaction.targetMessageId); + if (!reaction.remove) { await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now()); } diff --git a/lib/src/services/api/mediafiles/media_background.service.dart b/lib/src/services/api/mediafiles/media_background.service.dart index 986617c..5b1edab 100644 --- a/lib/src/services/api/mediafiles/media_background.service.dart +++ b/lib/src/services/api/mediafiles/media_background.service.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:background_downloader/background_downloader.dart'; -import 'package:clock/clock.dart'; import 'package:drift/drift.dart' show Value; import 'package:flutter/foundation.dart'; import 'package:twonly/globals.dart'; @@ -75,30 +74,7 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async { if (update.status == TaskStatus.complete) { if (update.responseStatusCode == 200) { Log.info('Upload of ${media.mediaId} success!'); - - await twonlyDB.mediaFilesDao.updateMedia( - media.mediaId, - const MediaFilesCompanion( - uploadState: Value(UploadState.uploaded), - ), - ); - - /// As the messages where send in a bulk acknowledge all messages. - - final messages = - await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaId); - for (final message in messages) { - final contacts = - await twonlyDB.groupsDao.getGroupNonLeftMembers(message.groupId); - for (final contact in contacts) { - await twonlyDB.messagesDao.handleMessageAckByServer( - contact.contactId, - message.messageId, - clock.now(), - ); - } - } - + await markUploadAsSuccessful(media); return; } Log.error( @@ -123,6 +99,20 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async { 'Background status $mediaId with status ${update.status} and ${update.responseStatusCode}. ', ); + if (update.status == TaskStatus.waitingToRetry) { + if (update.responseStatusCode == 401) { + // auth token is not valid, so either create a new task with a new token, or cancel task + final mediaService = MediaFileService(media); + await FileDownloader().cancelTaskWithId(update.task.taskId); + Log.info('Cancel task, already uploaded or will be reuploaded'); + if (mediaService.mediaFile.uploadState != UploadState.uploaded) { + await mediaService.setUploadState(UploadState.uploading); + // In all other cases just try the upload again... + await startBackgroundMediaUpload(mediaService); + } + } + } + if (update.status == TaskStatus.failed || update.status == TaskStatus.canceled) { Log.error( @@ -130,8 +120,11 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async { ); final mediaService = MediaFileService(media); - await mediaService.setUploadState(UploadState.uploading); - // In all other cases just try the upload again... - await startBackgroundMediaUpload(mediaService); + // in case the media file is already uploaded to not reqtry + if (mediaService.mediaFile.uploadState != UploadState.uploaded) { + await mediaService.setUploadState(UploadState.uploading); + // In all other cases just try the upload again... + await startBackgroundMediaUpload(mediaService); + } } } diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index 6e06a2c..f21513c 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -24,11 +24,14 @@ import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:workmanager/workmanager.dart' hide TaskStatus; Future finishStartedPreprocessing() async { final mediaFiles = await twonlyDB.mediaFilesDao.getAllMediaFilesPendingUpload(); + Log.info('There are ${mediaFiles.length} media files pending'); + for (final mediaFile in mediaFiles) { if (mediaFile.isDraftMedia) { continue; @@ -55,6 +58,47 @@ Future finishStartedPreprocessing() async { } } +/// It can happen, that a media files is uploaded but not yet marked for been uploaded. +/// For example because the background_downloader plugin has not yet reported the finished upload. +/// In case the the message receipts or a reaction was received, mark the media file as been uploaded. +Future handleMediaRelatedResponseFromReceiver(String messageId) async { + final message = + await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull(); + if (message == null || message.mediaId == null) return; + final media = await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!); + if (media == null) return; + + if (media.uploadState != UploadState.uploaded) { + Log.info('Media was not yet marked as uploaded. Doing it now.'); + await markUploadAsSuccessful(media); + } +} + +Future markUploadAsSuccessful(MediaFile media) async { + await twonlyDB.mediaFilesDao.updateMedia( + media.mediaId, + const MediaFilesCompanion( + uploadState: Value(UploadState.uploaded), + ), + ); + + /// As the messages where send in a bulk acknowledge all messages. + + final messages = + await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaId); + for (final message in messages) { + final contacts = + await twonlyDB.groupsDao.getGroupNonLeftMembers(message.groupId); + for (final contact in contacts) { + await twonlyDB.messagesDao.handleMessageAckByServer( + contact.contactId, + message.messageId, + clock.now(), + ); + } + } +} + Future initializeMediaUpload( MediaType type, int? displayLimitInMilliseconds, { @@ -344,8 +388,9 @@ Future _uploadUploadRequest(MediaFileService media) async { final connectivityResult = await Connectivity().checkConnectivity(); - if (!connectivityResult.contains(ConnectivityResult.mobile) && - !connectivityResult.contains(ConnectivityResult.wifi)) { + if (globalIsInBackgroundTask || + !connectivityResult.contains(ConnectivityResult.mobile) && + !connectivityResult.contains(ConnectivityResult.wifi)) { // no internet, directly put it into the background... await FileDownloader().enqueue(task); await media.setUploadState(UploadState.backgroundUploadTaskStarted); @@ -376,15 +421,30 @@ Future uploadFileFastOrEnqueue( ); try { + final workmanagerUniqueName = + 'progressing_finish_uploads_${media.mediaFile.mediaId}'; + + await Workmanager().registerOneOffTask( + workmanagerUniqueName, + 'eu.twonly.processing_task', + initialDelay: const Duration(minutes: 15), + constraints: Constraints( + networkType: NetworkType.connected, + ), + ); + Log.info('Uploading fast: ${task.taskId}'); - final response = - await requestMultipart.send().timeout(const Duration(seconds: 8)); + + final response = await requestMultipart.send(); var status = TaskStatus.failed; if (response.statusCode == 200) { status = TaskStatus.complete; } else if (response.statusCode == 404) { status = TaskStatus.notFound; } + + await Workmanager().cancelByUniqueName(workmanagerUniqueName); + await handleUploadStatusUpdate( TaskStatusUpdate( task, diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index 2914d4e..69b767a 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -63,6 +63,7 @@ Future handleServerMessage(server.ServerToClient msg) async { ..response = response; await apiService.sendResponse(ClientToServer()..v0 = v0); + globalGotMessageFromServer = true; }); } diff --git a/lib/src/services/background/callback_dispatcher.background.dart b/lib/src/services/background/callback_dispatcher.background.dart new file mode 100644 index 0000000..07297b2 --- /dev/null +++ b/lib/src/services/background/callback_dispatcher.background.dart @@ -0,0 +1,130 @@ +import 'dart:async'; +import 'package:path_provider/path_provider.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/constants/keyvalue.keys.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/api.service.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/utils/keyvalue.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/storage.dart'; +import 'package:workmanager/workmanager.dart'; + +// ignore: unreachable_from_main +Future initializeBackgroundTaskManager() async { + await Workmanager().initialize(callbackDispatcher); + + await Workmanager().registerPeriodicTask( + 'fetch_data_from_server', + 'eu.twonly.periodic_task', + frequency: const Duration(minutes: 20), + initialDelay: const Duration(minutes: 5), + constraints: Constraints( + networkType: NetworkType.connected, + ), + ); +} + +@pragma('vm:entry-point') +void callbackDispatcher() { + Workmanager().executeTask((task, inputData) async { + switch (task) { + case 'eu.twonly.periodic_task': + if (await initBackgroundExecution()) { + await handlePeriodicTask(); + } + case 'eu.twonly.processing_task': + if (await initBackgroundExecution()) { + await handleProcessingTask(); + } + default: + Log.error('Unknown task was executed: $task'); + } + return Future.value(true); + }); +} + +Future initBackgroundExecution() async { + SentryWidgetsFlutterBinding.ensureInitialized(); + initLogger(); + + final user = await getUser(); + if (user == null) return false; + gUser = user; + + globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path; + globalApplicationSupportDirectory = + (await getApplicationSupportDirectory()).path; + + twonlyDB = TwonlyDB(); + apiService = ApiService(); + globalIsInBackgroundTask = true; + + return true; +} + +Future handlePeriodicTask() async { + final lastExecution = + await KeyValueStore.get(KeyValueKeys.lastPeriodicTaskExecution); + if (lastExecution == null || !lastExecution.containsKey('timestamp')) { + final lastExecutionTime = lastExecution?['timestamp'] as int?; + if (lastExecutionTime != null) { + final lastExecution = + DateTime.fromMillisecondsSinceEpoch(lastExecutionTime); + if (DateTime.now().difference(lastExecution).inMinutes < 2) { + Log.info( + 'eu.twonly.periodic_task not executed as last execution was within the last two minutes.', + ); + return true; + } + } + } + + await KeyValueStore.put(KeyValueKeys.lastPeriodicTaskExecution, { + 'timestamp': DateTime.now().millisecondsSinceEpoch, + }); + + Log.info('eu.twonly.periodic_task was called.'); + + final stopwatch = Stopwatch()..start(); + + if (!await apiService.connect()) { + Log.info('Could not connect to the api. Returning early.'); + return false; + } + + if (!apiService.isAuthenticated) { + Log.info('Api is not authenticated. Returning early.'); + return false; + } + + while (!globalGotMessageFromServer) { + if (stopwatch.elapsed.inSeconds >= 15) { + Log.info('No new message from the server after 15 seconds.'); + break; + } + await Future.delayed(const Duration(milliseconds: 500)); + } + + if (globalGotMessageFromServer) { + Log.info('Received a server message from the server.'); + } + + await finishStartedPreprocessing(); + + await Future.delayed(const Duration(milliseconds: 2000)); + + await apiService.close(() {}); + stopwatch.stop(); + + Log.info('eu.twonly.periodic_task finished after ${stopwatch.elapsed}.'); + return true; +} + +Future handleProcessingTask() async { + Log.info('eu.twonly.processing_task was called.'); + final stopwatch = Stopwatch()..start(); + await finishStartedPreprocessing(); + Log.info('eu.twonly.processing_task finished after ${stopwatch.elapsed}.'); +} diff --git a/lib/src/services/fcm.service.dart b/lib/src/services/notifications/fcm.notifications.dart similarity index 92% rename from lib/src/services/fcm.service.dart rename to lib/src/services/notifications/fcm.notifications.dart index 5457b76..f202747 100644 --- a/lib/src/services/fcm.service.dart +++ b/lib/src/services/notifications/fcm.notifications.dart @@ -8,11 +8,12 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:twonly/globals.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/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; -import '../../firebase_options.dart'; +import '../../../firebase_options.dart'; // see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de @@ -111,8 +112,15 @@ Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { initLogger(); // Log.info('Handling a background message: ${message.messageId}'); await handleRemoteMessage(message); - // make sure every thing run... - await Future.delayed(const Duration(milliseconds: 2000)); + + if (Platform.isAndroid) { + if (await initBackgroundExecution()) { + await handlePeriodicTask(); + } + } else { + // make sure every thing run... + await Future.delayed(const Duration(milliseconds: 2000)); + } } Future handleRemoteMessage(RemoteMessage message) async { diff --git a/lib/src/views/settings/notification.view.dart b/lib/src/views/settings/notification.view.dart index 49b4e72..e39c2d7 100644 --- a/lib/src/views/settings/notification.view.dart +++ b/lib/src/views/settings/notification.view.dart @@ -7,7 +7,7 @@ import 'package:hashlib/random.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; -import 'package:twonly/src/services/fcm.service.dart'; +import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart';