diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 7b9ba35269..0aae1ce1b8 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -120,16 +120,6 @@
android:exported="false"
tools:node="remove"/>
-
-
-
-
Twake Mail
- Started
- In Progress
- Canceled
- Failed
- Completed
- Paused
\ No newline at end of file
diff --git a/docs/adr/0079-reduce-twake-mail-mobile-memory-usage.md b/docs/adr/0079-reduce-twake-mail-mobile-memory-usage.md
new file mode 100644
index 0000000000..c39f567200
--- /dev/null
+++ b/docs/adr/0079-reduce-twake-mail-mobile-memory-usage.md
@@ -0,0 +1,55 @@
+# 0079 - Reduce Twake Mail mobile memory usage
+
+Date: 2026-04-09
+
+## Status
+
+Accepted
+
+## Context
+
+Twake Mail is allocating a lot of memory on mobile.
+- ~375 MB on Android (Oneplus 8T)
+
+
+
+- ~297 MB on iOS (iPhone 11 Pro)
+
+
+
+## Findings
+
+### `worker_manager` library's inefficient memory allocation (Android & iOS)
+- Version `5.0.3` counts the number of processors (x) on the device, create forever-live x - 1 isolates
+- Version `7.2.7` adds an ability to create isolate on-demand, and dispose when done. However, it still leaks 1 isolate when init.
+### `firebase_messaging` library's eager background Dart isolate (Android)
+- FirebaseMessaging.onBackgroundMessage() creates a forever-live isolate even when the app is in foreground.
+- Related: https://github.com/firebase/flutterfire/issues/17163
+### Unused `flutter_downloader` library's worker (iOS)
+- The only usage was deleted in https://github.com/linagora/tmail-flutter/commit/be8eaf625818b17e60ca65846053cb8c26a71a15#diff-451741ba5146e6ad711c77e4c2fe34958a36595e4926cd43c2ddb97586ef6d88, but the library and initialization process remained, causing 1 forever-live isolate.
+
+## Decision
+
+### `worker_manager`
+- Upgrade to `7.2.7`
+- Create an upstream fix for init's isolate leak
+### `firebase_messaging`
+- Wait for https://github.com/firebase/flutterfire/pull/18122, update when merged
+### `flutter_downloader`
+- Remove the library
+
+## Consequences
+
+- Android: ~118 MB
+
+
+
+- iOS: ~78 MB
+
+
+
+- No changes for web
+
+| Before | After |
+| :--- | :--- |
+|
|
|
\ No newline at end of file
diff --git a/docs/images/android-after-4435.png b/docs/images/android-after-4435.png
new file mode 100644
index 0000000000..dfef3fb865
Binary files /dev/null and b/docs/images/android-after-4435.png differ
diff --git a/docs/images/android-before-4435.png b/docs/images/android-before-4435.png
new file mode 100644
index 0000000000..d5ac290069
Binary files /dev/null and b/docs/images/android-before-4435.png differ
diff --git a/docs/images/ios-after-4435.png b/docs/images/ios-after-4435.png
new file mode 100644
index 0000000000..b21e0ce82d
Binary files /dev/null and b/docs/images/ios-after-4435.png differ
diff --git a/docs/images/ios-before-4435.png b/docs/images/ios-before-4435.png
new file mode 100644
index 0000000000..bc79dbb512
Binary files /dev/null and b/docs/images/ios-before-4435.png differ
diff --git a/docs/images/web-after-4435.png b/docs/images/web-after-4435.png
new file mode 100644
index 0000000000..9512058713
Binary files /dev/null and b/docs/images/web-after-4435.png differ
diff --git a/docs/images/web-before-4435.png b/docs/images/web-before-4435.png
new file mode 100644
index 0000000000..1fbefac788
Binary files /dev/null and b/docs/images/web-before-4435.png differ
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index b5ca23af67..a9b15dea26 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -97,8 +97,6 @@ PODS:
- Flutter
- FlutterMacOS
- UniversalDetector2 (= 2.0.1)
- - flutter_downloader (0.0.1):
- - Flutter
- flutter_file_dialog (0.0.1):
- Flutter
- flutter_image_compress_common (1.0.0):
@@ -164,7 +162,7 @@ PODS:
- libwebp/sharpyuv (1.5.0)
- libwebp/webp (1.5.0):
- libwebp/sharpyuv
- - lottie-ios (4.4.1)
+ - lottie-ios (4.4.3)
- lottie_native (0.0.1):
- Flutter
- lottie-ios (~> 4.4.1)
@@ -206,10 +204,10 @@ PODS:
- ReachabilitySwift (5.2.4)
- receive_sharing_intent (1.8.1):
- Flutter
- - SDWebImage (5.21.1):
- - SDWebImage/Core (= 5.21.1)
- - SDWebImage/Core (5.21.1)
- - SDWebImageWebPCoder (0.14.6):
+ - SDWebImage (5.21.7):
+ - SDWebImage/Core (= 5.21.7)
+ - SDWebImage/Core (5.21.7)
+ - SDWebImageWebPCoder (0.15.0):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17)
- Sentry/HybridSDK (8.56.2)
@@ -228,8 +226,6 @@ PODS:
- UniversalDetector2 (2.0.1)
- url_launcher_ios (0.0.1):
- Flutter
- - workmanager_apple (0.0.1):
- - Flutter
DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
@@ -246,7 +242,6 @@ DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_appauth (from `.symlinks/plugins/flutter_appauth/ios`)
- flutter_charset_detector_darwin (from `.symlinks/plugins/flutter_charset_detector_darwin/darwin`)
- - flutter_downloader (from `.symlinks/plugins/flutter_downloader/ios`)
- flutter_file_dialog (from `.symlinks/plugins/flutter_file_dialog/ios`)
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
@@ -272,7 +267,6 @@ DEPENDENCIES:
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- super_dns_client (from `.symlinks/plugins/super_dns_client/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- - workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`)
SPEC REPOS:
trunk:
@@ -329,8 +323,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_appauth/ios"
flutter_charset_detector_darwin:
:path: ".symlinks/plugins/flutter_charset_detector_darwin/darwin"
- flutter_downloader:
- :path: ".symlinks/plugins/flutter_downloader/ios"
flutter_file_dialog:
:path: ".symlinks/plugins/flutter_file_dialog/ios"
flutter_image_compress_common:
@@ -381,8 +373,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/super_dns_client/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
- workmanager_apple:
- :path: ".symlinks/plugins/workmanager_apple/ios"
SPEC CHECKSUMS:
app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874
@@ -408,7 +398,6 @@ SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_appauth: d4abcf54856e5d8ba82ed7646ffc83245d4aa448
flutter_charset_detector_darwin: 14f055ebeed6896144cc96b046749df51127a0a3
- flutter_downloader: 78da0da1084e709cbfd3b723c7ea349c71681f09
flutter_file_dialog: ca8d7fbd1772d4f0c2777b4ab20a7787ef4e7dd8
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
@@ -420,7 +409,7 @@ SPEC CHECKSUMS:
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
- lottie-ios: e047b1d2e6239b787cc5e9755b988869cf190494
+ lottie-ios: fcb5e73e17ba4c983140b7d21095c834b3087418
lottie_native: c2e590a297861fc32a0188cf8dab39aa97f86d81
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5
@@ -438,8 +427,8 @@ SPEC CHECKSUMS:
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
- SDWebImage: f29024626962457f3470184232766516dee8dfea
- SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
+ SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
+ SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
Sentry: b53951377b78e21a734f5dc8318e333dbfc682d7
sentry_flutter: 4c33648b7e83310aa1fdb1b10c5491027d9643f0
share_plus: de6030e33b4e106470e09322d87cf2a4258d2d1d
@@ -448,7 +437,6 @@ SPEC CHECKSUMS:
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
UniversalDetector2: 7c9ffd935cf050eeb19edf7e90f6febe3743a1af
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
- workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
PODFILE CHECKSUM: 40b12ce0bc437886ee4f4050970375d7d253708d
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index 9f10b9960d..27500c86dd 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -1,6 +1,5 @@
import UIKit
import Flutter
-import flutter_downloader
import receive_sharing_intent
import flutter_local_notifications
@@ -34,12 +33,6 @@ import flutter_local_notifications
GeneratedPluginRegistrant.register(with: registry)
}
- FlutterDownloaderPlugin.setPluginRegistrantCallback { registry in
- if (!registry.hasPlugin("FlutterDownloaderPlugin")) {
- FlutterDownloaderPlugin.register(with: registry.registrar(forPlugin: "FlutterDownloaderPlugin")!)
- }
- }
-
let sharingIntent = SwiftReceiveSharingIntentPlugin.instance
if let url = launchOptions?[UIApplication.LaunchOptionsKey.url] as? URL {
if url.scheme == "mailto" {
diff --git a/lib/features/home/presentation/home_controller.dart b/lib/features/home/presentation/home_controller.dart
index 516c7005ab..e3692db702 100644
--- a/lib/features/home/presentation/home_controller.dart
+++ b/lib/features/home/presentation/home_controller.dart
@@ -4,8 +4,6 @@ import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:model/account/personal_account.dart';
@@ -68,7 +66,6 @@ class HomeController extends ReloadableController {
@override
void onInit() {
if (PlatformInfo.isMobile) {
- _initFlutterDownloader();
_registerReceivingFileSharing();
_registerDeepLinks();
}
@@ -96,14 +93,6 @@ class HomeController extends ReloadableController {
clearDataAndGoToLoginPage();
}
- void _initFlutterDownloader() {
- FlutterDownloader
- .initialize(debug: kDebugMode)
- .then((_) => FlutterDownloader.registerCallback(downloadCallback));
- }
-
- static void downloadCallback(String id, int status, int progress) {}
-
Future _handleNavigateToScreen() async {
await Future.delayed(2.seconds);
final arguments = Get.arguments;
diff --git a/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart b/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart
index 2ce7efb8fa..4f2f0eaf36 100644
--- a/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart
+++ b/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart
@@ -1,4 +1,5 @@
import 'dart:async';
+import 'dart:io';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
@@ -172,7 +173,7 @@ class MailboxDataSourceImpl extends MailboxDataSource {
StreamController>? onProgressController,
}) {
return Future.sync(() async {
- if (PlatformInfo.isWeb) {
+ if (PlatformInfo.isWeb || Platform.numberOfProcessors == 1) {
return await mailboxAPI.moveFolderContent(
session: session,
accountId: accountId,
diff --git a/lib/features/mailbox/data/network/mailbox_isolate_worker.dart b/lib/features/mailbox/data/network/mailbox_isolate_worker.dart
index 7c664c35c4..e7bf21e2dd 100644
--- a/lib/features/mailbox/data/network/mailbox_isolate_worker.dart
+++ b/lib/features/mailbox/data/network/mailbox_isolate_worker.dart
@@ -1,4 +1,5 @@
import 'dart:async';
+import 'dart:io';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
@@ -39,9 +40,8 @@ class MailboxIsolateWorker {
final ThreadAPI _threadApi;
final EmailAPI _emailApi;
- final Executor _isolateExecutor;
- MailboxIsolateWorker(this._threadApi, this._emailApi, this._isolateExecutor);
+ MailboxIsolateWorker(this._threadApi, this._emailApi);
Future> markAsMailboxRead(
Session session,
@@ -50,8 +50,8 @@ class MailboxIsolateWorker {
int totalEmailUnread,
StreamController> onProgressController
) async {
- if (PlatformInfo.isWeb) {
- return _handleMarkAsMailboxReadActionOnWeb(
+ if (PlatformInfo.isWeb || Platform.numberOfProcessors == 1) {
+ return await _handleMarkAsMailboxReadActionOnMainIsolate(
session,
accountId,
mailboxId,
@@ -63,108 +63,87 @@ class MailboxIsolateWorker {
throw const CanNotGetRootIsolateToken();
}
- final result = await _isolateExecutor.execute(
- arg1: MailboxMarkAsReadArguments(
- session,
- _threadApi,
- _emailApi,
- accountId,
- mailboxId,
- rootIsolateToken
- ),
- fun1: _handleMarkAsMailboxReadAction,
- notification: (value) {
- if (value is List) {
- log('MailboxIsolateWorker::markAsMailboxRead(): onUpdateProgress: PERCENT ${value.length / totalEmailUnread}');
- onProgressController.add(Right(UpdatingMarkAsMailboxReadState(
- mailboxId: mailboxId,
- totalUnread: totalEmailUnread,
- countRead: value.length)));
- }
- });
- return result;
+ final args = MailboxMarkAsReadArguments(
+ session,
+ _threadApi,
+ _emailApi,
+ accountId,
+ mailboxId,
+ rootIsolateToken,
+ );
+ return await workerManager.executeWithPort, int>(
+ _buildMarkAsReadClosure(args),
+ onMessage: (countRead) {
+ log('MailboxIsolateWorker::markAsMailboxRead(): onUpdateProgress: PERCENT ${countRead / totalEmailUnread}');
+ onProgressController.add(Right(UpdatingMarkAsMailboxReadState(
+ mailboxId: mailboxId,
+ totalUnread: totalEmailUnread,
+ countRead: countRead)));
+ },
+ );
}
}
static Future> _handleMarkAsMailboxReadAction(
- MailboxMarkAsReadArguments args,
- TypeSendPort sendPort
+ MailboxMarkAsReadArguments args,
+ SendPort sendPort,
) async {
final rootIsolateToken = args.isolateToken;
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
await HiveCacheConfig.instance.setUp();
- List emailIdsCompleted = List.empty(growable: true);
- bool mailboxHasEmails = true;
- UTCDate? lastReceivedDate;
- EmailId? lastEmailId;
-
- while (mailboxHasEmails) {
- final emailResponse = await args.threadAPI
- .getAllEmail(
- args.session,
- args.accountId,
- limit: UnsignedInt(30),
- filter: EmailFilterCondition(
- inMailbox: args.mailboxId,
- notKeyword: KeyWordIdentifier.emailSeen.value,
- before: lastReceivedDate),
- sort: {}..add(
- EmailComparator(EmailComparatorProperty.receivedAt)
- ..setIsAscending(false)),
- properties: Properties({
- EmailProperty.id,
- EmailProperty.keywords,
- EmailProperty.receivedAt,
- }))
- .then((response) {
- var listEmails = response.emailList;
- if (listEmails != null && listEmails.isNotEmpty && lastEmailId != null) {
- listEmails = listEmails
- .where((email) => email.id != lastEmailId)
- .toList();
- }
- return EmailsResponse(emailList: listEmails, state: response.state);
- });
- final listEmailUnread = emailResponse.emailList;
-
- log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): listEmailUnread: ${listEmailUnread?.length}');
-
- if (listEmailUnread == null || listEmailUnread.isEmpty) {
- mailboxHasEmails = false;
- } else {
- lastEmailId = listEmailUnread.last.id;
- lastReceivedDate = listEmailUnread.last.receivedAt;
-
- final result = await args.emailAPI.markAsRead(
- args.session,
- args.accountId,
- listEmailUnread.listEmailIds,
- ReadActions.markAsRead);
-
- log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): MARK_READ: ${result.emailIdsSuccess.length}');
- emailIdsCompleted.addAll(result.emailIdsSuccess);
- sendPort.send(emailIdsCompleted);
- }
- }
+ final emailIdsCompleted = await _executeMarkAsMailboxRead(
+ threadAPI: args.threadAPI,
+ emailAPI: args.emailAPI,
+ session: args.session,
+ accountId: args.accountId,
+ mailboxId: args.mailboxId,
+ onProgress: sendPort.send,
+ );
log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): TOTAL_READ: ${emailIdsCompleted.length}');
return emailIdsCompleted;
}
- Future> _handleMarkAsMailboxReadActionOnWeb(
+ Future> _handleMarkAsMailboxReadActionOnMainIsolate(
Session session,
AccountId accountId,
MailboxId mailboxId,
int totalEmailUnread,
- StreamController> onProgressController
+ StreamController> onProgressController,
) async {
+ final result = await _executeMarkAsMailboxRead(
+ threadAPI: _threadApi,
+ emailAPI: _emailApi,
+ session: session,
+ accountId: accountId,
+ mailboxId: mailboxId,
+ onProgress: (countRead) => onProgressController.add(Right(
+ UpdatingMarkAsMailboxReadState(
+ mailboxId: mailboxId,
+ totalUnread: totalEmailUnread,
+ countRead: countRead,
+ ),
+ )),
+ );
+ log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnMainIsolate(): TOTAL_READ: ${result.length}');
+ return result;
+ }
+
+ static Future> _executeMarkAsMailboxRead({
+ required ThreadAPI threadAPI,
+ required EmailAPI emailAPI,
+ required Session session,
+ required AccountId accountId,
+ required MailboxId mailboxId,
+ required void Function(int countRead) onProgress,
+ }) async {
List emailIdsCompleted = List.empty(growable: true);
bool mailboxHasEmails = true;
UTCDate? lastReceivedDate;
EmailId? lastEmailId;
while (mailboxHasEmails) {
- final emailResponse = await _threadApi
+ final emailResponse = await threadAPI
.getAllEmail(
session,
accountId,
@@ -192,7 +171,7 @@ class MailboxIsolateWorker {
});
final listEmailUnread = emailResponse.emailList;
- log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): listEmailUnread: ${listEmailUnread?.length}');
+ log('MailboxIsolateWorker::_executeMarkAsMailboxRead(): listEmailUnread: ${listEmailUnread?.length}');
if (listEmailUnread == null || listEmailUnread.isEmpty) {
mailboxHasEmails = false;
@@ -200,22 +179,19 @@ class MailboxIsolateWorker {
lastEmailId = listEmailUnread.last.id;
lastReceivedDate = listEmailUnread.last.receivedAt;
- final result = await _emailApi.markAsRead(
+ final result = await emailAPI.markAsRead(
session,
accountId,
listEmailUnread.listEmailIds,
ReadActions.markAsRead,
);
- log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): MARK_READ: ${result.emailIdsSuccess.length}');
+ log('MailboxIsolateWorker::_executeMarkAsMailboxRead(): MARK_READ: ${result.emailIdsSuccess.length}');
emailIdsCompleted.addAll(result.emailIdsSuccess);
- onProgressController.add(Right(UpdatingMarkAsMailboxReadState(
- mailboxId: mailboxId,
- totalUnread: totalEmailUnread,
- countRead: emailIdsCompleted.length)));
+ onProgress(emailIdsCompleted.length);
}
}
- log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): TOTAL_READ: ${emailIdsCompleted.length}');
+ log('MailboxIsolateWorker::_executeMarkAsMailboxRead(): TOTAL_READ: ${emailIdsCompleted.length}');
return emailIdsCompleted;
}
@@ -230,29 +206,27 @@ class MailboxIsolateWorker {
throw const CanNotGetRootIsolateToken();
}
- final countEmailsCompleted = await _isolateExecutor.execute(
- arg1: MoveFolderContentIsolateArguments(
- session: session,
- accountId: accountId,
- threadAPI: _threadApi,
- emailAPI: _emailApi,
- currentMailboxId: request.mailboxId,
- destinationMailboxId: request.destinationMailboxId,
- isolateToken: rootIsolateToken,
- markAsRead: request.markAsRead,
- ),
- fun1: _moveFolderContentIsolateMethod,
- notification: (value) {
- if (value is int) {
- log('$runtimeType::moveFolderContent(): Progress percent is ${value / request.totalEmails}');
- onProgressController?.add(
- Right(MoveFolderContentProgressState(
- request.mailboxId,
- value,
- request.totalEmails,
- )),
- );
- }
+ final args = MoveFolderContentIsolateArguments(
+ session: session,
+ accountId: accountId,
+ threadAPI: _threadApi,
+ emailAPI: _emailApi,
+ currentMailboxId: request.mailboxId,
+ destinationMailboxId: request.destinationMailboxId,
+ isolateToken: rootIsolateToken,
+ markAsRead: request.markAsRead,
+ );
+ final countEmailsCompleted = await workerManager.executeWithPort(
+ _buildMoveFolderClosure(args),
+ onMessage: (value) {
+ log('$runtimeType::moveFolderContent(): Progress percent is ${value / request.totalEmails}');
+ onProgressController?.add(
+ Right(MoveFolderContentProgressState(
+ request.mailboxId,
+ value,
+ request.totalEmails,
+ )),
+ );
},
);
@@ -263,9 +237,17 @@ class MailboxIsolateWorker {
}
}
+ static Future> Function(SendPort) _buildMarkAsReadClosure(
+ MailboxMarkAsReadArguments args,
+ ) => (sendPort) => _handleMarkAsMailboxReadAction(args, sendPort);
+
+ static Future Function(SendPort) _buildMoveFolderClosure(
+ MoveFolderContentIsolateArguments args,
+ ) => (sendPort) => _moveFolderContentIsolateMethod(args, sendPort);
+
static Future _moveFolderContentIsolateMethod(
MoveFolderContentIsolateArguments args,
- TypeSendPort sendPort,
+ SendPort sendPort,
) async {
final rootIsolateToken = args.isolateToken;
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
diff --git a/lib/features/thread/data/network/thread_isolate_worker.dart b/lib/features/thread/data/network/thread_isolate_worker.dart
index 2505ddfc84..1352921765 100644
--- a/lib/features/thread/data/network/thread_isolate_worker.dart
+++ b/lib/features/thread/data/network/thread_isolate_worker.dart
@@ -1,4 +1,5 @@
import 'dart:async';
+import 'dart:io';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
@@ -30,9 +31,8 @@ import 'package:worker_manager/worker_manager.dart';
class ThreadIsolateWorker {
final ThreadAPI _threadAPI;
final EmailAPI _emailAPI;
- final Executor _isolateExecutor;
- ThreadIsolateWorker(this._threadAPI, this._emailAPI, this._isolateExecutor);
+ ThreadIsolateWorker(this._threadAPI, this._emailAPI);
Future> emptyMailboxFolder(
Session session,
@@ -41,31 +41,29 @@ class ThreadIsolateWorker {
int totalEmails,
StreamController> onProgressController
) async {
- if (PlatformInfo.isWeb) {
- return _emptyMailboxFolderOnWeb(session, accountId, mailboxId, totalEmails, onProgressController);
+ if (PlatformInfo.isWeb || Platform.numberOfProcessors == 1) {
+ return _emptyMailboxFolderOnMainIsolate(session, accountId, mailboxId, totalEmails, onProgressController);
} else {
final rootIsolateToken = RootIsolateToken.instance;
if (rootIsolateToken == null) {
throw const CanNotGetRootIsolateToken();
}
- final result = await _isolateExecutor.execute(
- arg1: EmptyMailboxFolderArguments(
- session,
- _threadAPI,
- _emailAPI,
- accountId,
- mailboxId,
- rootIsolateToken
- ),
- fun1: _emptyMailboxFolderAction,
- notification: (value) {
- if (value is List) {
- log('ThreadIsolateWorker::emptyMailboxFolder(): processed ${value.length} - totalEmails $totalEmails');
- onProgressController.add(Right(EmptyingFolderState(
- mailboxId, value.length, totalEmails
- )));
- }
+ final args = EmptyMailboxFolderArguments(
+ session,
+ _threadAPI,
+ _emailAPI,
+ accountId,
+ mailboxId,
+ rootIsolateToken,
+ );
+ final result = await workerManager.executeWithPort, int>(
+ _buildEmptyMailboxClosure(args),
+ onMessage: (processedCount) {
+ log('ThreadIsolateWorker::emptyMailboxFolder(): processed $processedCount - totalEmails $totalEmails');
+ onProgressController.add(Right(EmptyingFolderState(
+ mailboxId, processedCount, totalEmails
+ )));
},
);
@@ -77,9 +75,13 @@ class ThreadIsolateWorker {
}
}
+ static Future> Function(SendPort) _buildEmptyMailboxClosure(
+ EmptyMailboxFolderArguments args,
+ ) => (sendPort) => _emptyMailboxFolderAction(args, sendPort);
+
static Future> _emptyMailboxFolderAction(
EmptyMailboxFolderArguments args,
- TypeSendPort sendPort
+ SendPort sendPort,
) async {
final rootIsolateToken = args.isolateToken;
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
@@ -119,7 +121,7 @@ class ThreadIsolateWorker {
args.accountId,
newEmailList.listEmailIds);
emailListCompleted.addAll(listEmailIdDeleted.emailIdsSuccess);
- sendPort.send(emailListCompleted);
+ sendPort.send(emailListCompleted.length);
} else {
hasEmails = false;
}
@@ -128,7 +130,7 @@ class ThreadIsolateWorker {
return emailListCompleted;
}
- Future> _emptyMailboxFolderOnWeb(
+ Future> _emptyMailboxFolderOnMainIsolate(
Session session,
AccountId accountId,
MailboxId mailboxId,
@@ -158,7 +160,7 @@ class ThreadIsolateWorker {
newEmailList = newEmailList.where((email) => email.id != lastEmail!.id).toList();
}
- log('ThreadIsolateWorker::_emptyMailboxFolderOnWeb(): ${newEmailList.length}');
+ log('ThreadIsolateWorker::_emptyMailboxFolderOnMainIsolate(): ${newEmailList.length}');
if (newEmailList.isNotEmpty) {
lastEmail = newEmailList.last;
@@ -176,7 +178,7 @@ class ThreadIsolateWorker {
hasEmails = false;
}
}
- log('ThreadIsolateWorker::_emptyMailboxFolderOnWeb(): TOTAL_REMOVE: ${emailListCompleted.length}');
+ log('ThreadIsolateWorker::_emptyMailboxFolderOnMainIsolate(): TOTAL_REMOVE: ${emailListCompleted.length}');
return emailListCompleted;
}
}
diff --git a/lib/features/upload/data/network/file_uploader.dart b/lib/features/upload/data/network/file_uploader.dart
index aab9b5fca9..64fb7f736a 100644
--- a/lib/features/upload/data/network/file_uploader.dart
+++ b/lib/features/upload/data/network/file_uploader.dart
@@ -31,12 +31,10 @@ class FileUploader {
static const String filePathExtraKey = 'path';
final DioClient _dioClient;
- final worker.Executor _isolateExecutor;
final FileUtils _fileUtils;
FileUploader(
this._dioClient,
- this._isolateExecutor,
this._fileUtils,
);
@@ -49,8 +47,8 @@ class FileUploader {
StreamController>? onSendController,
}
) async {
- if (PlatformInfo.isWeb) {
- return _handleUploadAttachmentActionOnWeb(
+ if (PlatformInfo.isWeb || Platform.numberOfProcessors == 1) {
+ return _handleUploadAttachmentActionOnMainIsolate(
uploadId,
fileInfo,
uploadUri,
@@ -63,31 +61,33 @@ class FileUploader {
throw const CanNotGetRootIsolateToken();
}
- return await _isolateExecutor.execute(
- arg1: UploadFileArguments(
- _dioClient,
- _fileUtils,
- uploadId,
- fileInfo,
- uploadUri,
- rootIsolateToken,
- ),
- fun1: _handleUploadAttachmentAction,
- notification: (value) {
- if (value is Success) {
- log('FileUploader::uploadAttachment(): onUpdateProgress: $value');
- onSendController?.add(Right(value));
- }
- }
+ final args = UploadFileArguments(
+ _dioClient,
+ _fileUtils,
+ uploadId,
+ fileInfo,
+ uploadUri,
+ rootIsolateToken,
+ );
+ return await worker.workerManager.executeWithPort(
+ _buildUploadClosure(args),
+ onMessage: (value) {
+ log('FileUploader::uploadAttachment(): onUpdateProgress: $value');
+ onSendController?.add(Right(value));
+ },
)
.then((value) => value)
.catchError((error) => throw error);
}
}
+ static Future Function(worker.SendPort) _buildUploadClosure(
+ UploadFileArguments args,
+ ) => (sendPort) => _handleUploadAttachmentAction(args, sendPort);
+
static Future _handleUploadAttachmentAction(
- UploadFileArguments argsUpload,
- worker.TypeSendPort sendPort
+ UploadFileArguments argsUpload,
+ worker.SendPort sendPort,
) async {
try {
final rootIsolateToken = argsUpload.isolateToken;
@@ -159,7 +159,7 @@ class FileUploader {
}
}
- Future _handleUploadAttachmentActionOnWeb(
+ Future _handleUploadAttachmentActionOnMainIsolate(
UploadTaskId uploadId,
FileInfo fileInfo,
Uri uploadUri,
@@ -188,7 +188,7 @@ class FileUploader {
data: BodyBytesStream.fromBytes(fileInfo.bytes!),
cancelToken: cancelToken,
onSendProgress: (count, total) {
- log('FileUploader::_handleUploadAttachmentActionOnWeb():onSendProgress: FILE[${uploadId.id}] : { PROGRESS = $count | TOTAL = $total}');
+ log('FileUploader::_handleUploadAttachmentActionOnMainIsolate():onSendProgress: FILE[${uploadId.id}] : { PROGRESS = $count | TOTAL = $total}');
onSendController?.add(
Right(UploadingAttachmentUploadState(
uploadId,
@@ -198,7 +198,7 @@ class FileUploader {
);
}
);
- log('FileUploader::_handleUploadAttachmentActionOnWeb(): RESULT_JSON = $resultJson');
+ log('FileUploader::_handleUploadAttachmentActionOnMainIsolate(): RESULT_JSON = $resultJson');
if (fileInfo.mimeType == FileUtils.TEXT_PLAIN_MIME_TYPE) {
final fileCharset = await _fileUtils.getCharsetFromBytes(fileInfo.bytes!);
return _parsingResponse(
diff --git a/lib/main/bindings/network/network_bindings.dart b/lib/main/bindings/network/network_bindings.dart
index 2567e469e3..dd72b59453 100644
--- a/lib/main/bindings/network/network_bindings.dart
+++ b/lib/main/bindings/network/network_bindings.dart
@@ -40,7 +40,6 @@ import 'package:tmail_ui_user/main/exceptions/thrower/remote_exception_thrower.d
import 'package:tmail_ui_user/main/exceptions/thrower/send_email_exception_thrower.dart';
import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart';
import 'package:uuid/uuid.dart';
-import 'package:worker_manager/worker_manager.dart';
class NetworkBindings extends Bindings {
@@ -85,7 +84,6 @@ class NetworkBindings extends Bindings {
Get.find(),
Get.find(),
));
- Get.put(Executor());
}
void _bindingInterceptors() {
diff --git a/lib/main/bindings/network/network_isolate_binding.dart b/lib/main/bindings/network/network_isolate_binding.dart
index 0ec431d37d..8acde6ce4f 100644
--- a/lib/main/bindings/network/network_isolate_binding.dart
+++ b/lib/main/bindings/network/network_isolate_binding.dart
@@ -24,7 +24,6 @@ import 'package:tmail_ui_user/features/upload/data/network/file_uploader.dart';
import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart';
import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart';
import 'package:uuid/uuid.dart';
-import 'package:worker_manager/worker_manager.dart';
class NetworkIsolateBindings extends Bindings {
@@ -96,16 +95,13 @@ class NetworkIsolateBindings extends Bindings {
Get.put(ThreadIsolateWorker(
Get.find(tag: PlatformInfo.isMobile ? BindingTag.isolateTag : null),
Get.find(tag: PlatformInfo.isMobile ? BindingTag.isolateTag : null),
- Get.find(),
));
Get.put(MailboxIsolateWorker(
Get.find(tag: PlatformInfo.isMobile ? BindingTag.isolateTag : null),
Get.find(tag: PlatformInfo.isMobile ? BindingTag.isolateTag : null),
- Get.find(),
));
Get.put(FileUploader(
Get.find(tag: PlatformInfo.isMobile ? BindingTag.isolateTag : null),
- Get.find(),
Get.find(),
));
}
diff --git a/lib/main/main_entry.dart b/lib/main/main_entry.dart
index c251152f44..c2c0aad95c 100644
--- a/lib/main/main_entry.dart
+++ b/lib/main/main_entry.dart
@@ -3,7 +3,6 @@ import 'package:core/utils/build_utils.dart';
import 'package:core/utils/config/env_loader.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/widgets.dart';
-import 'package:get/get.dart';
import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart';
import 'package:tmail_ui_user/main.dart';
import 'package:tmail_ui_user/main/bindings/main_bindings.dart';
@@ -27,7 +26,10 @@ Future runTmailPreload() async {
if (PlatformInfo.isWeb) AssetPreloader.preloadHtmlEditorAssets(),
], eagerError: false);
- await Get.find().warmUp(log: BuildUtils.isDebugMode);
+ if (PlatformInfo.isMobile) {
+ await workerManager.init(dynamicSpawning: true);
+ workerManager.log = BuildUtils.isDebugMode;
+ }
await CozyIntegration.integrateCozy();
await HiveCacheConfig.instance.initializeEncryptionKey();
diff --git a/pubspec.lock b/pubspec.lock
index 190129348d..fff891976b 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -773,14 +773,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.2"
- flutter_downloader:
- dependency: "direct main"
- description:
- name: flutter_downloader
- sha256: "93a9ddbd561f8a3f5483b4189453fba145a0a1014a88143c96a966296b78a118"
- url: "https://pub.dev"
- source: hosted
- version: "1.12.0"
flutter_file_dialog:
dependency: "direct main"
description:
@@ -2583,43 +2575,12 @@ packages:
worker_manager:
dependency: "direct main"
description:
- name: worker_manager
- sha256: "42501e49ee0acad9eeda562984e3dcfe6fe3d26f2d8dc410bd76308a86447eb5"
- url: "https://pub.dev"
- source: hosted
- version: "5.0.3"
- workmanager:
- dependency: "direct main"
- description:
- name: workmanager
- sha256: "065673b2a465865183093806925419d311a9a5e0995aa74ccf8920fd695e2d10"
- url: "https://pub.dev"
- source: hosted
- version: "0.9.0+3"
- workmanager_android:
- dependency: transitive
- description:
- name: workmanager_android
- sha256: "9ae744db4ef891f5fcd2fb8671fccc712f4f96489a487a1411e0c8675e5e8cb7"
- url: "https://pub.dev"
- source: hosted
- version: "0.9.0+2"
- workmanager_apple:
- dependency: transitive
- description:
- name: workmanager_apple
- sha256: "1cc12ae3cbf5535e72f7ba4fde0c12dd11b757caf493a28e22d684052701f2ca"
- url: "https://pub.dev"
- source: hosted
- version: "0.9.1+2"
- workmanager_platform_interface:
- dependency: transitive
- description:
- name: workmanager_platform_interface
- sha256: f40422f10b970c67abb84230b44da22b075147637532ac501729256fcea10a47
- url: "https://pub.dev"
- source: hosted
- version: "0.9.1+1"
+ path: "."
+ ref: "hotfix/worker-init-memory-leak"
+ resolved-ref: dd04544217c9fcc08b2a32634583f38d22cc2309
+ url: "https://github.com/linagora/worker_manager.git"
+ source: git
+ version: "7.2.7"
xdg_directories:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 3d7a68d139..a08f71c575 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -143,8 +143,6 @@ dependencies:
uuid: 3.0.7
- flutter_downloader: 1.12.0
-
external_path: 2.2.0
path_provider: 2.1.5
@@ -177,7 +175,11 @@ dependencies:
percent_indicator: 4.2.2
- worker_manager: 5.0.3
+ # TODO: Replace with upstream when https://github.com/dsrenesanse/worker_manager/pull/123 is merged
+ worker_manager:
+ git:
+ url: https://github.com/linagora/worker_manager.git
+ ref: hotfix/worker-init-memory-leak
async: 2.13.0
@@ -211,8 +213,6 @@ dependencies:
intl: 0.20.2
- workmanager: 0.9.0+3
-
flutter_typeahead: 5.0.2
flutter_keyboard_visibility: 6.0.0
diff --git a/test/features/identity_creator/presentation/identity_creator_controller_test.dart b/test/features/identity_creator/presentation/identity_creator_controller_test.dart
index 36dc0ecaa3..6acf57c452 100644
--- a/test/features/identity_creator/presentation/identity_creator_controller_test.dart
+++ b/test/features/identity_creator/presentation/identity_creator_controller_test.dart
@@ -52,7 +52,6 @@ import 'package:tmail_ui_user/main/universal_import/html_stub.dart';
import 'package:tmail_ui_user/main/utils/toast_manager.dart';
import 'package:tmail_ui_user/main/utils/twake_app_manager.dart';
import 'package:uuid/uuid.dart';
-import 'package:worker_manager/worker_manager.dart';
import 'identity_creator_controller_test.mocks.dart';
@@ -76,7 +75,6 @@ import 'identity_creator_controller_test.mocks.dart';
MockSpec(),
MockSpec(),
MockSpec(),
- MockSpec(),
MockSpec(),
MockSpec(),
MockSpec(),
@@ -159,7 +157,6 @@ void main() {
mockSaveIdentityCacheOnWebInteractor = MockSaveIdentityCacheOnWebInteractor();
Get.put(MockDioClient(), tag: BindingTag.isolateTag);
- Get.put(MockExecutor());
Get.put(MockFileUtils());
Get.put(MockFileUploader());
Get.put(MockRemoteExceptionThrower());