mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-04-20 08:22: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
|
## 0.0.98
|
||||||
|
|
||||||
- New: Groups can now collect flames as well
|
- 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
|
- New: Adds a link if the image contains a QR code
|
||||||
- Improve: Video compression with progress updates
|
- Improve: Video compression with progress updates
|
||||||
- Improve: Show message "Flames restored"
|
- 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">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||||
<application
|
<application
|
||||||
android:label="twonly"
|
android:label="twonly"
|
||||||
android:name="${applicationName}"
|
android:name=".MyApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:fullBackupContent="false"
|
android:fullBackupContent="false"
|
||||||
android:icon="@mipmap/ic_launcher">
|
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 UIKit
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
import flutter_sharing_intent
|
import flutter_sharing_intent
|
||||||
|
import workmanager_apple
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||||
|
|
@ -16,6 +17,17 @@ import flutter_sharing_intent
|
||||||
if let registrar = self.registrar(forPlugin: "VideoCompressionChannel") {
|
if let registrar = self.registrar(forPlugin: "VideoCompressionChannel") {
|
||||||
VideoCompressionChannel.register(with: registrar.messenger())
|
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)
|
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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIApplicationSceneManifest</key>
|
<key>AppGroupId</key>
|
||||||
<dict>
|
<string>$(CUSTOM_GROUP_ID)</string>
|
||||||
<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>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
|
@ -43,6 +24,17 @@
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<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>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>FIREBASE_ANALYTICS_COLLECTION_ENABLED</key>
|
<key>FIREBASE_ANALYTICS_COLLECTION_ENABLED</key>
|
||||||
|
|
@ -51,10 +43,10 @@
|
||||||
<false/>
|
<false/>
|
||||||
<key>FlutterDeepLinkingEnabled</key>
|
<key>FlutterDeepLinkingEnabled</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
|
||||||
<true/>
|
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>Use your camera to make photos or videos and share them encrypted with your friends.</string>
|
<string>Use your camera to make photos or videos and share them encrypted with your friends.</string>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
|
|
@ -63,12 +55,39 @@
|
||||||
<string>Use your microphone to enable audio when making videos.</string>
|
<string>Use your microphone to enable audio when making videos.</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>twonly will save photos or videos to your library.</string>
|
<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>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
<string>remote-notification</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>
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
|
|
@ -89,19 +108,5 @@
|
||||||
</array>
|
</array>
|
||||||
<key>firebase_performance_collection_deactivated</key>
|
<key>firebase_performance_collection_deactivated</key>
|
||||||
<true/>
|
<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>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,9 @@ void Function(SubscriptionPlan plan) globalCallbackUpdatePlan = (plan) {};
|
||||||
Map<String, VoidCallback> globalUserDataChangedCallBack = {};
|
Map<String, VoidCallback> globalUserDataChangedCallBack = {};
|
||||||
|
|
||||||
bool globalIsAppInBackground = true;
|
bool globalIsAppInBackground = true;
|
||||||
|
bool globalIsInBackgroundTask = false;
|
||||||
bool globalAllowErrorTrackingViaSentry = false;
|
bool globalAllowErrorTrackingViaSentry = false;
|
||||||
|
bool globalGotMessageFromServer = false;
|
||||||
|
|
||||||
late String globalApplicationCacheDirectory;
|
late String globalApplicationCacheDirectory;
|
||||||
late String globalApplicationSupportDirectory;
|
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/download.service.dart';
|
||||||
import 'package:twonly/src/services/api/mediafiles/media_background.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/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/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/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/services/notifications/setup.notifications.dart';
|
||||||
import 'package:twonly/src/utils/avatars.dart';
|
import 'package:twonly/src/utils/avatars.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
@ -46,6 +47,7 @@ void main() async {
|
||||||
}
|
}
|
||||||
|
|
||||||
unawaited(performTwonlySafeBackup());
|
unawaited(performTwonlySafeBackup());
|
||||||
|
unawaited(initializeBackgroundTaskManager());
|
||||||
} else {
|
} else {
|
||||||
Log.info('User is not yet register. Ensure all local data is removed.');
|
Log.info('User is not yet register. Ensure all local data is removed.');
|
||||||
await deleteLocalUserData();
|
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(
|
..where(
|
||||||
(t) => (t.uploadState.equals(UploadState.initialized.name) |
|
(t) => (t.uploadState.equals(UploadState.initialized.name) |
|
||||||
t.uploadState.equals(UploadState.uploadLimitReached.name) |
|
t.uploadState.equals(UploadState.uploadLimitReached.name) |
|
||||||
|
t.uploadState.equals(UploadState.uploading.name) |
|
||||||
t.uploadState.equals(UploadState.preprocessing.name)),
|
t.uploadState.equals(UploadState.preprocessing.name)),
|
||||||
))
|
))
|
||||||
.get();
|
.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/messages.table.dart';
|
||||||
import 'package:twonly/src/database/tables/receipts.table.dart';
|
import 'package:twonly/src/database/tables/receipts.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.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';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
part 'receipts.dao.g.dart';
|
part 'receipts.dao.g.dart';
|
||||||
|
|
@ -33,6 +34,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
||||||
type: const Value(MessageActionType.ackByUserAt),
|
type: const Value(MessageActionType.ackByUserAt),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
await handleMediaRelatedResponseFromReceiver(receipt.messageId!);
|
||||||
}
|
}
|
||||||
|
|
||||||
await (delete(receipts)
|
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/messages.dart';
|
||||||
import 'package:twonly/src/services/api/server_messages.dart';
|
import 'package:twonly/src/services/api/server_messages.dart';
|
||||||
import 'package:twonly/src/services/api/utils.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/flame.service.dart';
|
||||||
import 'package:twonly/src/services/group.services.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/notifications/pushkeys.notifications.dart';
|
||||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||||
import 'package:twonly/src/services/signal/utils.signal.dart';
|
import 'package:twonly/src/services/signal/utils.signal.dart';
|
||||||
|
|
@ -90,7 +90,11 @@ class ApiService {
|
||||||
await initFCMAfterAuthenticated();
|
await initFCMAfterAuthenticated();
|
||||||
globalCallbackConnectionState(isConnected: true);
|
globalCallbackConnectionState(isConnected: true);
|
||||||
|
|
||||||
if (!globalIsAppInBackground) {
|
if (globalIsInBackgroundTask) {
|
||||||
|
await retransmitRawBytes();
|
||||||
|
await tryTransmitMessages();
|
||||||
|
await tryDownloadAllMediaFiles();
|
||||||
|
} else if (!globalIsAppInBackground) {
|
||||||
unawaited(retransmitRawBytes());
|
unawaited(retransmitRawBytes());
|
||||||
unawaited(tryTransmitMessages());
|
unawaited(tryTransmitMessages());
|
||||||
unawaited(tryDownloadAllMediaFiles());
|
unawaited(tryDownloadAllMediaFiles());
|
||||||
|
|
@ -124,6 +128,7 @@ class ApiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startReconnectionTimer() async {
|
Future<void> startReconnectionTimer() async {
|
||||||
|
if (globalIsInBackgroundTask) return;
|
||||||
if (reconnectionTimer?.isActive ?? false) {
|
if (reconnectionTimer?.isActive ?? false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -330,7 +335,9 @@ class ApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (res.isError) {
|
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) {
|
if (res.error == ErrorCode.AppVersionOutdated) {
|
||||||
globalCallbackAppIsOutdated();
|
globalCallbackAppIsOutdated();
|
||||||
Log.warn('App Version is OUTDATED.');
|
Log.warn('App Version is OUTDATED.');
|
||||||
|
|
@ -395,6 +402,7 @@ class ApiService {
|
||||||
..userId = Int64(userId)
|
..userId = Int64(userId)
|
||||||
..appVersion = (await PackageInfo.fromPlatform()).version
|
..appVersion = (await PackageInfo.fromPlatform()).version
|
||||||
..deviceId = Int64(user.deviceId)
|
..deviceId = Int64(user.deviceId)
|
||||||
|
..inBackground = globalIsInBackgroundTask
|
||||||
..authToken = base64Decode(apiAuthToken);
|
..authToken = base64Decode(apiAuthToken);
|
||||||
|
|
||||||
final handshake = Handshake()..authenticate = authenticate;
|
final handshake = Handshake()..authenticate = authenticate;
|
||||||
|
|
@ -404,12 +412,19 @@ class ApiService {
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
Log.info('websocket is authenticated');
|
Log.info('websocket is authenticated');
|
||||||
unawaited(onAuthenticated());
|
if (globalIsInBackgroundTask) {
|
||||||
|
await onAuthenticated();
|
||||||
|
} else {
|
||||||
|
unawaited(onAuthenticated());
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (result.isError) {
|
if (result.isError) {
|
||||||
if (result.error != ErrorCode.AuthTokenNotValid) {
|
if (result.error != ErrorCode.AuthTokenNotValid &&
|
||||||
Log.error('got error while authenticating to the server: $result');
|
result.error != ErrorCode.ForegroundSessionConnected) {
|
||||||
|
Log.error(
|
||||||
|
'got error while authenticating to the server: ${result.error}',
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:clock/clock.dart';
|
import 'package:clock/clock.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.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';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
Future<void> handleReaction(
|
Future<void> handleReaction(
|
||||||
|
|
@ -17,6 +18,8 @@ Future<void> handleReaction(
|
||||||
reaction.remove,
|
reaction.remove,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await handleMediaRelatedResponseFromReceiver(reaction.targetMessageId);
|
||||||
|
|
||||||
if (!reaction.remove) {
|
if (!reaction.remove) {
|
||||||
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
|
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:background_downloader/background_downloader.dart';
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
import 'package:clock/clock.dart';
|
|
||||||
import 'package:drift/drift.dart' show Value;
|
import 'package:drift/drift.dart' show Value;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
|
|
@ -75,30 +74,7 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
||||||
if (update.status == TaskStatus.complete) {
|
if (update.status == TaskStatus.complete) {
|
||||||
if (update.responseStatusCode == 200) {
|
if (update.responseStatusCode == 200) {
|
||||||
Log.info('Upload of ${media.mediaId} success!');
|
Log.info('Upload of ${media.mediaId} success!');
|
||||||
|
await markUploadAsSuccessful(media);
|
||||||
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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.error(
|
Log.error(
|
||||||
|
|
@ -123,6 +99,20 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
||||||
'Background status $mediaId with status ${update.status} and ${update.responseStatusCode}. ',
|
'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 ||
|
if (update.status == TaskStatus.failed ||
|
||||||
update.status == TaskStatus.canceled) {
|
update.status == TaskStatus.canceled) {
|
||||||
Log.error(
|
Log.error(
|
||||||
|
|
@ -130,8 +120,11 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
||||||
);
|
);
|
||||||
final mediaService = MediaFileService(media);
|
final mediaService = MediaFileService(media);
|
||||||
|
|
||||||
await mediaService.setUploadState(UploadState.uploading);
|
// in case the media file is already uploaded to not reqtry
|
||||||
// In all other cases just try the upload again...
|
if (mediaService.mediaFile.uploadState != UploadState.uploaded) {
|
||||||
await startBackgroundMediaUpload(mediaService);
|
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/services/mediafiles/mediafile.service.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:workmanager/workmanager.dart' hide TaskStatus;
|
||||||
|
|
||||||
Future<void> finishStartedPreprocessing() async {
|
Future<void> finishStartedPreprocessing() async {
|
||||||
final mediaFiles =
|
final mediaFiles =
|
||||||
await twonlyDB.mediaFilesDao.getAllMediaFilesPendingUpload();
|
await twonlyDB.mediaFilesDao.getAllMediaFilesPendingUpload();
|
||||||
|
|
||||||
|
Log.info('There are ${mediaFiles.length} media files pending');
|
||||||
|
|
||||||
for (final mediaFile in mediaFiles) {
|
for (final mediaFile in mediaFiles) {
|
||||||
if (mediaFile.isDraftMedia) {
|
if (mediaFile.isDraftMedia) {
|
||||||
continue;
|
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(
|
Future<MediaFileService?> initializeMediaUpload(
|
||||||
MediaType type,
|
MediaType type,
|
||||||
int? displayLimitInMilliseconds, {
|
int? displayLimitInMilliseconds, {
|
||||||
|
|
@ -344,8 +388,9 @@ Future<void> _uploadUploadRequest(MediaFileService media) async {
|
||||||
|
|
||||||
final connectivityResult = await Connectivity().checkConnectivity();
|
final connectivityResult = await Connectivity().checkConnectivity();
|
||||||
|
|
||||||
if (!connectivityResult.contains(ConnectivityResult.mobile) &&
|
if (globalIsInBackgroundTask ||
|
||||||
!connectivityResult.contains(ConnectivityResult.wifi)) {
|
!connectivityResult.contains(ConnectivityResult.mobile) &&
|
||||||
|
!connectivityResult.contains(ConnectivityResult.wifi)) {
|
||||||
// no internet, directly put it into the background...
|
// no internet, directly put it into the background...
|
||||||
await FileDownloader().enqueue(task);
|
await FileDownloader().enqueue(task);
|
||||||
await media.setUploadState(UploadState.backgroundUploadTaskStarted);
|
await media.setUploadState(UploadState.backgroundUploadTaskStarted);
|
||||||
|
|
@ -376,15 +421,30 @@ Future<void> uploadFileFastOrEnqueue(
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
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}');
|
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;
|
var status = TaskStatus.failed;
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
status = TaskStatus.complete;
|
status = TaskStatus.complete;
|
||||||
} else if (response.statusCode == 404) {
|
} else if (response.statusCode == 404) {
|
||||||
status = TaskStatus.notFound;
|
status = TaskStatus.notFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Workmanager().cancelByUniqueName(workmanagerUniqueName);
|
||||||
|
|
||||||
await handleUploadStatusUpdate(
|
await handleUploadStatusUpdate(
|
||||||
TaskStatusUpdate(
|
TaskStatusUpdate(
|
||||||
task,
|
task,
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
|
||||||
..response = response;
|
..response = response;
|
||||||
|
|
||||||
await apiService.sendResponse(ClientToServer()..v0 = v0);
|
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:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||||
|
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
|
||||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/storage.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
|
// 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();
|
initLogger();
|
||||||
// Log.info('Handling a background message: ${message.messageId}');
|
// Log.info('Handling a background message: ${message.messageId}');
|
||||||
await handleRemoteMessage(message);
|
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 {
|
Future<void> handleRemoteMessage(RemoteMessage message) async {
|
||||||
|
|
@ -7,7 +7,7 @@ import 'package:hashlib/random.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/constants/secure_storage_keys.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/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/services/notifications/pushkeys.notifications.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/utils/storage.dart';
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue