mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-04-16 06:32:54 +00:00
support background execution
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
60ed2775bb
commit
5a2049fd4a
18 changed files with 333 additions and 81 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
<application
|
||||
android:label="twonly"
|
||||
android:name="${applicationName}"
|
||||
android:name=".MyApplication"
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
|
|
|
|||
13
android/app/src/main/kotlin/eu/twonly/MyApplication.kt
Normal file
13
android/app/src/main/kotlin/eu/twonly/MyApplication.kt
Normal file
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,27 +2,8 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>FlutterSceneDelegate</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>AppGroupId</key>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
|
|
@ -43,6 +24,17 @@
|
|||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>SharingMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>FIREBASE_ANALYTICS_COLLECTION_ENABLED</key>
|
||||
|
|
@ -51,10 +43,10 @@
|
|||
<false/>
|
||||
<key>FlutterDeepLinkingEnabled</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Use your camera to make photos or videos and share them encrypted with your friends.</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
|
|
@ -63,12 +55,39 @@
|
|||
<string>Use your microphone to enable audio when making videos.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>twonly will save photos or videos to your library.</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>FlutterSceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>remote-notification</string>
|
||||
<string>processing</string>
|
||||
</array>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>eu.twonly.periodic_task</string>
|
||||
<string>eu.twonly.processing_task</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
|
|
@ -89,19 +108,5 @@
|
|||
</array>
|
||||
<key>firebase_performance_collection_deactivated</key>
|
||||
<true/>
|
||||
|
||||
<key>AppGroupId</key>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>SharingMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ void Function(SubscriptionPlan plan) globalCallbackUpdatePlan = (plan) {};
|
|||
Map<String, VoidCallback> globalUserDataChangedCallBack = {};
|
||||
|
||||
bool globalIsAppInBackground = true;
|
||||
bool globalIsInBackgroundTask = false;
|
||||
bool globalAllowErrorTrackingViaSentry = false;
|
||||
bool globalGotMessageFromServer = false;
|
||||
|
||||
late String globalApplicationCacheDirectory;
|
||||
late String globalApplicationSupportDirectory;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
4
lib/src/constants/keyvalue.keys.dart
Normal file
4
lib/src/constants/keyvalue.keys.dart
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
class KeyValueKeys {
|
||||
static const String lastPeriodicTaskExecution =
|
||||
'last_periodic_task_execution';
|
||||
}
|
||||
|
|
@ -121,6 +121,7 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
..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();
|
||||
|
|
|
|||
|
|
@ -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<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
type: const Value(MessageActionType.ackByUserAt),
|
||||
),
|
||||
);
|
||||
await handleMediaRelatedResponseFromReceiver(receipt.messageId!);
|
||||
}
|
||||
|
||||
await (delete(receipts)
|
||||
|
|
|
|||
|
|
@ -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<void> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> handleReaction(
|
||||
|
|
@ -17,6 +18,8 @@ Future<void> handleReaction(
|
|||
reaction.remove,
|
||||
);
|
||||
|
||||
await handleMediaRelatedResponseFromReceiver(reaction.targetMessageId);
|
||||
|
||||
if (!reaction.remove) {
|
||||
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> 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<void> 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<void> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> 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<void> 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<void> 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<void> 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<MediaFileService?> initializeMediaUpload(
|
||||
MediaType type,
|
||||
int? displayLimitInMilliseconds, {
|
||||
|
|
@ -344,8 +388,9 @@ Future<void> _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<void> 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,
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
|
|||
..response = response;
|
||||
|
||||
await apiService.sendResponse(ClientToServer()..v0 = v0);
|
||||
globalGotMessageFromServer = true;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
130
lib/src/services/background/callback_dispatcher.background.dart
Normal file
130
lib/src/services/background/callback_dispatcher.background.dart
Normal file
|
|
@ -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<void> 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<bool> 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<bool> 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<void> 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}.');
|
||||
}
|
||||
|
|
@ -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<void> _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<void> handleRemoteMessage(RemoteMessage message) async {
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in a new issue