From b1af1d8533177d624bf99ffe9d24c77e88536c83 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 23 May 2023 12:56:21 +0700 Subject: [PATCH] TF-1861 Config and abstraction work manager (cherry picked from commit ee2dca9b34a907c8356ceccf0b23efba2893a01c) --- core/lib/core.dart | 1 + core/lib/utils/platform_info.dart | 14 ++++ .../config/work_manager_config.dart | 19 ++++++ .../config/work_manager_constants.dart | 4 ++ .../controller/work_scheduler_controller.dart | 66 +++++++++++++++++++ .../offline_mode/manager/work_dispatcher.dart | 8 +++ .../observer/sending_email_observer.dart | 28 ++++++++ .../offline_mode/observer/work_observer.dart | 28 ++++++++ .../scheduler/one_time_work_request.dart | 25 +++++++ .../scheduler/periodic_work_request.dart | 31 +++++++++ .../offline_mode/scheduler/work_request.dart | 38 +++++++++++ .../offline_mode/scheduler/work_status.dart | 6 ++ .../offline_mode/scheduler/worker.dart | 29 ++++++++ .../offline_mode/scheduler/worker_type.dart | 22 +++++++ lib/main.dart | 14 ++-- 15 files changed, 329 insertions(+), 4 deletions(-) create mode 100644 core/lib/utils/platform_info.dart create mode 100644 lib/features/offline_mode/config/work_manager_config.dart create mode 100644 lib/features/offline_mode/config/work_manager_constants.dart create mode 100644 lib/features/offline_mode/controller/work_scheduler_controller.dart create mode 100644 lib/features/offline_mode/manager/work_dispatcher.dart create mode 100644 lib/features/offline_mode/observer/sending_email_observer.dart create mode 100644 lib/features/offline_mode/observer/work_observer.dart create mode 100644 lib/features/offline_mode/scheduler/one_time_work_request.dart create mode 100644 lib/features/offline_mode/scheduler/periodic_work_request.dart create mode 100644 lib/features/offline_mode/scheduler/work_request.dart create mode 100644 lib/features/offline_mode/scheduler/work_status.dart create mode 100644 lib/features/offline_mode/scheduler/worker.dart create mode 100644 lib/features/offline_mode/scheduler/worker_type.dart diff --git a/core/lib/core.dart b/core/lib/core.dart index 3aa837295..6a91c3387 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -41,6 +41,7 @@ export 'utils/config/app_config_loader.dart'; export 'utils/config/app_config_parser.dart'; export 'utils/config/errors.dart'; export 'data/utils/compress_file_utils.dart'; +export 'utils/platform_info.dart'; // Views export 'presentation/views/text/slogan_builder.dart'; diff --git a/core/lib/utils/platform_info.dart b/core/lib/utils/platform_info.dart new file mode 100644 index 000000000..a1d932762 --- /dev/null +++ b/core/lib/utils/platform_info.dart @@ -0,0 +1,14 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; + +abstract class PlatformInfo { + static bool get isWeb => kIsWeb; + static bool get isLinux => !kIsWeb && Platform.isLinux; + static bool get isWindows => !kIsWeb && Platform.isWindows; + static bool get isMacOS => !kIsWeb && Platform.isMacOS; + static bool get isIOS => !kIsWeb && Platform.isIOS; + static bool get isAndroid => !kIsWeb && Platform.isAndroid; + static bool get isMobile => isAndroid || isIOS; + static bool get isDesktop => isLinux || isWindows || isMacOS; +} \ No newline at end of file diff --git a/lib/features/offline_mode/config/work_manager_config.dart b/lib/features/offline_mode/config/work_manager_config.dart new file mode 100644 index 000000000..81ae7c828 --- /dev/null +++ b/lib/features/offline_mode/config/work_manager_config.dart @@ -0,0 +1,19 @@ + +import 'package:core/core.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/work_dispatcher.dart'; +import 'package:workmanager/workmanager.dart'; + +class WorkManagerConfig { + static WorkManagerConfig? _instance; + + WorkManagerConfig._(); + + factory WorkManagerConfig() => _instance ??= WorkManagerConfig._(); + + Future initialize() { + return Workmanager().initialize( + callbackDispatcher, + isInDebugMode: BuildUtils.isDebugMode + ); + } +} \ No newline at end of file diff --git a/lib/features/offline_mode/config/work_manager_constants.dart b/lib/features/offline_mode/config/work_manager_constants.dart new file mode 100644 index 000000000..de24ee7be --- /dev/null +++ b/lib/features/offline_mode/config/work_manager_constants.dart @@ -0,0 +1,4 @@ + +class WorkManagerConstants { + static const String sendingEmailUniqueId = 'com.linagora.ios.teammail.sendingQueue'; +} \ No newline at end of file diff --git a/lib/features/offline_mode/controller/work_scheduler_controller.dart b/lib/features/offline_mode/controller/work_scheduler_controller.dart new file mode 100644 index 000000000..9ecb7d1f7 --- /dev/null +++ b/lib/features/offline_mode/controller/work_scheduler_controller.dart @@ -0,0 +1,66 @@ + +import 'package:collection/collection.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:tmail_ui_user/features/offline_mode/scheduler/one_time_work_request.dart'; +import 'package:tmail_ui_user/features/offline_mode/scheduler/periodic_work_request.dart'; +import 'package:tmail_ui_user/features/offline_mode/scheduler/work_request.dart'; +import 'package:tmail_ui_user/features/offline_mode/scheduler/worker_type.dart'; +import 'package:workmanager/workmanager.dart'; + +class WorkSchedulerController { + + static WorkSchedulerController? _instance; + + WorkSchedulerController._(); + + factory WorkSchedulerController() => _instance ??= WorkSchedulerController._(); + + Future enqueue(WorkRequest workRequest) { + if (workRequest is OneTimeWorkRequest) { + return Workmanager().registerOneOffTask( + workRequest.worker.uniqueId, + workRequest.worker.id, + tag: workRequest.worker.type.name, + initialDelay: workRequest.initialDelay, + constraints: workRequest.constraints, + backoffPolicy: workRequest.backoffPolicy, + backoffPolicyDelay: workRequest.backoffPolicyDelay, + outOfQuotaPolicy: workRequest.outOfQuotaPolicy, + inputData: workRequest.worker.inputData + ); + } if (workRequest is PeriodicWorkRequest) { + return Workmanager().registerPeriodicTask( + workRequest.worker.uniqueId, + workRequest.worker.id, + frequency: workRequest.frequency, + tag: workRequest.worker.type.name, + initialDelay: workRequest.initialDelay, + constraints: workRequest.constraints, + backoffPolicy: workRequest.backoffPolicy, + backoffPolicyDelay: workRequest.backoffPolicyDelay, + outOfQuotaPolicy: workRequest.outOfQuotaPolicy, + inputData: workRequest.worker.inputData + ); + } else { + return Future.value(); + } + } + + Future handleBackgroundTask(String taskName, Map? inputData) { + if (inputData != null && inputData.isNotEmpty) { + final typeTask = inputData['workerType']; + final dataObject = inputData['data']; + log('WorkSchedulerController::handleBackgroundTask():typeTask: $typeTask | dataObject: $dataObject'); + final workType = WorkerType.values.firstWhereOrNull((type) => type.name == typeTask); + log('WorkSchedulerController::handleBackgroundTask():workType: $workType'); + } + + return Future.value(true); + } + + Future cancelByWorkType(WorkerType type) => Workmanager().cancelByTag(type.name); + + Future cancelByUniqueId(String uniqueId) => Workmanager().cancelByUniqueName(uniqueId); + + Future cancelAll() => Workmanager().cancelAll(); +} \ No newline at end of file diff --git a/lib/features/offline_mode/manager/work_dispatcher.dart b/lib/features/offline_mode/manager/work_dispatcher.dart new file mode 100644 index 000000000..3ae7cc84c --- /dev/null +++ b/lib/features/offline_mode/manager/work_dispatcher.dart @@ -0,0 +1,8 @@ + +import 'package:tmail_ui_user/features/offline_mode/controller/work_scheduler_controller.dart'; +import 'package:workmanager/workmanager.dart'; + +@pragma('vm:entry-point') +void callbackDispatcher() { + Workmanager().executeTask(WorkSchedulerController().handleBackgroundTask); +} diff --git a/lib/features/offline_mode/observer/sending_email_observer.dart b/lib/features/offline_mode/observer/sending_email_observer.dart new file mode 100644 index 000000000..acc40285a --- /dev/null +++ b/lib/features/offline_mode/observer/sending_email_observer.dart @@ -0,0 +1,28 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/offline_mode/observer/work_observer.dart'; +import 'package:tmail_ui_user/features/offline_mode/scheduler/work_status.dart'; + +class SendingEmailObserver extends WorkObserver { + + static SendingEmailObserver? _instance; + + SendingEmailObserver._(); + + factory SendingEmailObserver() => _instance ??= SendingEmailObserver._(); + + @override + void handleFailureViewState(Failure failure) { + } + + @override + void handleSuccessViewState(Success success) { + } + + @override + WorkStatus observe(Email value) { + return WorkStatus.failure; + } +} \ No newline at end of file diff --git a/lib/features/offline_mode/observer/work_observer.dart b/lib/features/offline_mode/observer/work_observer.dart new file mode 100644 index 000000000..3119277ab --- /dev/null +++ b/lib/features/offline_mode/observer/work_observer.dart @@ -0,0 +1,28 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/offline_mode/scheduler/work_status.dart'; + +abstract class WorkObserver { + WorkStatus observe(T value); + + void consumeState(Stream> newStateStream) { + newStateStream.listen( + _handleStateStream, + onError: (error, stackTrace) { + logError('WorkObserver::consumeState():onError:error: $error'); + logError('WorkObserver::consumeState():onError:stackTrace: $stackTrace'); + } + ); + } + + void _handleStateStream(Either newState) { + newState.fold(handleFailureViewState, handleSuccessViewState); + } + + void handleFailureViewState(Failure failure); + + void handleSuccessViewState(Success success); +} \ No newline at end of file diff --git a/lib/features/offline_mode/scheduler/one_time_work_request.dart b/lib/features/offline_mode/scheduler/one_time_work_request.dart new file mode 100644 index 000000000..ba54888f9 --- /dev/null +++ b/lib/features/offline_mode/scheduler/one_time_work_request.dart @@ -0,0 +1,25 @@ + +import 'package:tmail_ui_user/features/offline_mode/scheduler/work_request.dart'; +import 'package:tmail_ui_user/features/offline_mode/scheduler/worker.dart'; +import 'package:workmanager/workmanager.dart'; + +/// A WorkRequest for non-repeating work. +class OneTimeWorkRequest extends WorkRequest { + OneTimeWorkRequest( + Worker worker, { + Duration initialDelay = Duration.zero, + Duration backoffPolicyDelay = Duration.zero, + ExistingWorkPolicy? existingWorkPolicy, + BackoffPolicy? backoffPolicy, + OutOfQuotaPolicy? outOfQuotaPolicy, + Constraints? constraints + }) : super( + worker, + initialDelay: initialDelay, + backoffPolicyDelay: backoffPolicyDelay, + existingWorkPolicy: existingWorkPolicy, + backoffPolicy: backoffPolicy, + outOfQuotaPolicy: outOfQuotaPolicy, + constraints: constraints + ); +} \ No newline at end of file diff --git a/lib/features/offline_mode/scheduler/periodic_work_request.dart b/lib/features/offline_mode/scheduler/periodic_work_request.dart new file mode 100644 index 000000000..093fc1fea --- /dev/null +++ b/lib/features/offline_mode/scheduler/periodic_work_request.dart @@ -0,0 +1,31 @@ + +import 'package:tmail_ui_user/features/offline_mode/scheduler/work_request.dart'; +import 'package:tmail_ui_user/features/offline_mode/scheduler/worker.dart'; +import 'package:workmanager/workmanager.dart'; + +/// A WorkRequest for repeating work. +class PeriodicWorkRequest extends WorkRequest { + final Duration? frequency; + + PeriodicWorkRequest( + Worker worker, { + Duration initialDelay = Duration.zero, + Duration backoffPolicyDelay = Duration.zero, + ExistingWorkPolicy? existingWorkPolicy, + BackoffPolicy? backoffPolicy, + OutOfQuotaPolicy? outOfQuotaPolicy, + Constraints? constraints, + this.frequency + }) : super( + worker, + initialDelay: initialDelay, + backoffPolicyDelay: backoffPolicyDelay, + existingWorkPolicy: existingWorkPolicy, + backoffPolicy: backoffPolicy, + outOfQuotaPolicy: outOfQuotaPolicy, + constraints: constraints + ); + + @override + List get props => [super.props, frequency]; +} \ No newline at end of file diff --git a/lib/features/offline_mode/scheduler/work_request.dart b/lib/features/offline_mode/scheduler/work_request.dart new file mode 100644 index 000000000..424da2355 --- /dev/null +++ b/lib/features/offline_mode/scheduler/work_request.dart @@ -0,0 +1,38 @@ + +import 'package:equatable/equatable.dart'; +import 'package:tmail_ui_user/features/offline_mode/scheduler/worker.dart'; +import 'package:workmanager/workmanager.dart'; + +/// Represents the scheduling of requests +abstract class WorkRequest with EquatableMixin { + final Worker worker; + final Duration initialDelay; + final Duration backoffPolicyDelay; + final ExistingWorkPolicy? existingWorkPolicy; + final BackoffPolicy? backoffPolicy; + final OutOfQuotaPolicy? outOfQuotaPolicy; + final Constraints? constraints; + + WorkRequest( + this.worker, + { + this.initialDelay = Duration.zero, + this.backoffPolicyDelay = Duration.zero, + this.existingWorkPolicy, + this.backoffPolicy, + this.outOfQuotaPolicy, + this.constraints + } + ); + + @override + List get props => [ + worker, + initialDelay, + backoffPolicyDelay, + existingWorkPolicy, + backoffPolicy, + outOfQuotaPolicy, + constraints + ]; +} \ No newline at end of file diff --git a/lib/features/offline_mode/scheduler/work_status.dart b/lib/features/offline_mode/scheduler/work_status.dart new file mode 100644 index 000000000..63be27783 --- /dev/null +++ b/lib/features/offline_mode/scheduler/work_status.dart @@ -0,0 +1,6 @@ + +enum WorkStatus { + success, + retry, + failure; +} \ No newline at end of file diff --git a/lib/features/offline_mode/scheduler/worker.dart b/lib/features/offline_mode/scheduler/worker.dart new file mode 100644 index 000000000..182fc289c --- /dev/null +++ b/lib/features/offline_mode/scheduler/worker.dart @@ -0,0 +1,29 @@ + +import 'package:core/utils/platform_info.dart'; +import 'package:equatable/equatable.dart'; +import 'package:tmail_ui_user/features/offline_mode/scheduler/worker_type.dart'; + +/// Equivalent to the task or work that needs to be done in the background +class Worker with EquatableMixin { + + final String id; + final WorkerType type; + final Map data; + + Worker(this.id, this.type, this.data); + + String get uniqueId { + if (PlatformInfo.isIOS) { + return type.iOSUniqueId; + } + return id; + } + + Map get inputData { + data['workerType'] = type; + return data; + } + + @override + List get props => [id, type, data]; +} \ No newline at end of file diff --git a/lib/features/offline_mode/scheduler/worker_type.dart b/lib/features/offline_mode/scheduler/worker_type.dart new file mode 100644 index 000000000..4ab093789 --- /dev/null +++ b/lib/features/offline_mode/scheduler/worker_type.dart @@ -0,0 +1,22 @@ + +import 'package:tmail_ui_user/features/offline_mode/config/work_manager_constants.dart'; +import 'package:tmail_ui_user/features/offline_mode/observer/sending_email_observer.dart'; +import 'package:tmail_ui_user/features/offline_mode/observer/work_observer.dart'; + +enum WorkerType { + sendingEmail; + + WorkObserver usingObserver() { + switch(this) { + case WorkerType.sendingEmail: + return SendingEmailObserver(); + } + } + + String get iOSUniqueId { + switch(this) { + case WorkerType.sendingEmail: + return WorkManagerConstants.sendingEmailUniqueId; + } + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 871052cc6..0a342f5e6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; +import 'package:tmail_ui_user/features/offline_mode/config/work_manager_config.dart'; import 'package:tmail_ui_user/main/bindings/main_bindings.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations_delegate.dart'; @@ -17,11 +18,16 @@ void main() async { initLogger(() async { WidgetsFlutterBinding.ensureInitialized(); ThemeUtils.setSystemLightUIStyle(); - await MainBindings().dependencies(); - await HiveCacheConfig().setUp(); + + await Future.wait([ + MainBindings().dependencies(), + HiveCacheConfig().setUp(), + Executor().warmUp(), + WorkManagerConfig().initialize(), + AppUtils.loadEnvFile() + ]); await HiveCacheConfig.initializeEncryptionKey(); - await Executor().warmUp(); - await AppUtils.loadEnvFile(); + runApp(const TMailApp()); }); }