From aedf310c9dcb1307c5506965d1c0a441f38c55d0 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 4 Aug 2023 20:55:56 +0300 Subject: [PATCH] Cw 155 monero synchronization (#1014) * Run Monero Synchronization task in background on Android * Add monero sync task in the load function to be registered/cancelled when user changes wallets * Revert unused file changes * Register Sync task on all monero wallets if any * Add Sync Modes and change task frequency based on user's choice * Register background task after current wallet is set * Add Sync All toggle and change task wallets to sync accordingly * Enable background notifications in release mode temporarily * Disable constraints and increase the frequency of tasks * Decrease frequency of background tasks * Delay the background task thread till the syncing thread finish (Dummy Trial-1) * Start Sync process and wait for it to finish * Wait for synchronization to finish before ending the background thread Add 10 minutes timeout duration for sync process * Connect to node before syncing wallet * replace testing configuration with the configurations agreed on * Fix Conflicts with main * Update and Migrate Background tasks to null safety * Update workmanager version in pubspec_base also * Move Sync options to Connection and sync page Show Sync options only for Monero and Haven Minor Enhancements * Remove debugging notifications Revert aggressive mode frequency to 6 hours [skip ci] * Add iOS configs * Revert debugging changes Fix conflicts with main * Add/Extract Sync configurations to/from backup file [skip ci] --- cw_core/lib/set_app_secure_native.dart | 10 +- ios/Runner.xcodeproj/project.pbxproj | 5 + ios/Runner/AppDelegate.swift | 10 + ios/Runner/InfoBase.plist | 5 + lib/core/backup_service.dart | 12 ++ lib/di.dart | 9 +- lib/entities/background_tasks.dart | 164 +++++++++++++++++ lib/entities/load_current_wallet.dart | 5 +- lib/entities/preferences_key.dart | 2 + lib/main.dart | 172 +++++++++--------- lib/router.dart | 4 + lib/routes.dart | 1 + .../settings/connection_sync_page.dart | 101 ++++------ .../screens/settings/manage_nodes_page.dart | 85 +++++++++ .../screens/settings/other_settings_page.dart | 46 +++-- lib/src/widgets/standard_list.dart | 2 +- lib/store/settings_store.dart | 40 +++- .../dashboard/dashboard_view_model.dart | 13 ++ .../settings/choices_list_item.dart | 14 +- lib/view_model/settings/sync_mode.dart | 15 ++ lib/view_model/wallet_creation_vm.dart | 3 + .../wallet_list/wallet_list_view_model.dart | 5 +- pubspec_base.yaml | 1 + res/values/strings_ar.arb | 1 + res/values/strings_bg.arb | 1 + res/values/strings_cs.arb | 1 + res/values/strings_de.arb | 3 + res/values/strings_en.arb | 3 + res/values/strings_es.arb | 3 + res/values/strings_fr.arb | 5 +- res/values/strings_ha.arb | 1 + res/values/strings_hi.arb | 3 + res/values/strings_hr.arb | 3 + res/values/strings_id.arb | 1 + res/values/strings_it.arb | 3 + res/values/strings_ja.arb | 3 + res/values/strings_ko.arb | 3 + res/values/strings_my.arb | 1 + res/values/strings_nl.arb | 3 + res/values/strings_pl.arb | 3 + res/values/strings_pt.arb | 3 + res/values/strings_ru.arb | 3 + res/values/strings_th.arb | 1 + res/values/strings_tr.arb | 3 +- res/values/strings_uk.arb | 3 + res/values/strings_ur.arb | 1 + res/values/strings_yo.arb | 1 + res/values/strings_zh.arb | 3 + 48 files changed, 593 insertions(+), 190 deletions(-) create mode 100644 lib/entities/background_tasks.dart create mode 100644 lib/src/screens/settings/manage_nodes_page.dart create mode 100644 lib/view_model/settings/sync_mode.dart diff --git a/cw_core/lib/set_app_secure_native.dart b/cw_core/lib/set_app_secure_native.dart index 09e01556c..84096e2d6 100644 --- a/cw_core/lib/set_app_secure_native.dart +++ b/cw_core/lib/set_app_secure_native.dart @@ -1,7 +1,9 @@ import 'package:flutter/services.dart'; -const utils = const MethodChannel('com.cake_wallet/native_utils'); - void setIsAppSecureNative(bool isAppSecure) { - utils.invokeMethod('setIsAppSecure', {'isAppSecure': isAppSecure}); -} \ No newline at end of file + try { + final utils = const MethodChannel('com.cake_wallet/native_utils'); + + utils.invokeMethod('setIsAppSecure', {'isAppSecure': isAppSecure}); + } catch (_) {} +} diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 05cf659e8..4bc10f9be 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -606,4 +606,9 @@ /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; + SystemCapabilities = { + com.apple.BackgroundModes = { + enabled = 1; + }; + }; } diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 401509606..acdfa4346 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,6 +1,7 @@ import UIKit import Flutter import UnstoppableDomainsResolution +import workmanager @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { @@ -16,6 +17,15 @@ import UnstoppableDomainsResolution UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate } + WorkmanagerPlugin.setPluginRegistrantCallback { registry in + // Registry in this case is the FlutterEngine that is created in Workmanager's + // performFetchWithCompletionHandler or BGAppRefreshTask. + // This will make other plugins available during a background operation. + GeneratedPluginRegistrant.register(with: registry) + } + + WorkmanagerPlugin.registerTask(withIdentifier: "com.fotolockr.cakewallet.monero_sync_task") + makeSecure() let controller : FlutterViewController = window?.rootViewController as! FlutterViewController diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index 29cd24cb4..821df195e 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -2,6 +2,10 @@ + BGTaskSchedulerPermittedIdentifiers + + com.fotolockr.cakewallet.monero_sync_task + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -113,6 +117,7 @@ UIBackgroundModes fetch + processing remote-notification UILaunchStoryboardName diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index a109b75cd..2e27d83c9 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -243,6 +243,8 @@ class BackupService { final sortBalanceTokensBy = data[PreferencesKey.sortBalanceBy] as int?; final pinNativeTokenAtTop = data[PreferencesKey.pinNativeTokenAtTop] as bool?; final useEtherscan = data[PreferencesKey.useEtherscan] as bool?; + final syncAll = data[PreferencesKey.syncAllKey] as bool?; + final syncMode = data[PreferencesKey.syncModeKey] as int?; await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName); @@ -361,6 +363,12 @@ class BackupService { if (useEtherscan != null) await _sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan); + if (syncAll != null) + await _sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); + + if (syncMode != null) + await _sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode); + await preferencesFile.delete(); } @@ -510,6 +518,10 @@ class BackupService { _sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop), PreferencesKey.useEtherscan: _sharedPreferences.getBool(PreferencesKey.useEtherscan), + PreferencesKey.syncModeKey: + _sharedPreferences.getInt(PreferencesKey.syncModeKey), + PreferencesKey.syncAllKey: + _sharedPreferences.getBool(PreferencesKey.syncAllKey), }; return json.encode(preferences); diff --git a/lib/di.dart b/lib/di.dart index c27659e7a..e2108f26d 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart'; import 'package:cake_wallet/core/yat_service.dart'; +import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/receive_page_option.dart'; @@ -23,6 +24,7 @@ import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; +import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; @@ -244,6 +246,8 @@ Future setup({ getIt.registerSingletonAsync(() => SharedPreferences.getInstance()); } + getIt.registerFactory(() => BackgroundTasks()); + final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty) && (secrets.wyreApiKey.isNotEmpty) && (secrets.wyreAccountId.isNotEmpty); @@ -681,8 +685,7 @@ Future setup({ return NodeListViewModel(_nodeSource, appStore); }); - getIt.registerFactory( - () => ConnectionSyncPage(getIt.get(), getIt.get())); + getIt.registerFactory(() => ConnectionSyncPage(getIt.get())); getIt.registerFactory( () => SecurityBackupPage(getIt.get(), getIt.get())); @@ -1055,5 +1058,7 @@ Future setup({ ), ); + getIt.registerFactory(() => ManageNodesPage(getIt.get())); + _isSetupFinished = true; } diff --git a/lib/entities/background_tasks.dart b/lib/entities/background_tasks.dart new file mode 100644 index 000000000..ce1e2f6d8 --- /dev/null +++ b/lib/entities/background_tasks.dart @@ -0,0 +1,164 @@ +import 'dart:io'; + +import 'package:cake_wallet/core/wallet_loading_service.dart'; +import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/view_model/settings/sync_mode.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:workmanager/workmanager.dart'; +import 'package:cake_wallet/main.dart'; +import 'package:cake_wallet/di.dart'; + +const moneroSyncTaskKey = "com.fotolockr.cakewallet.monero_sync_task"; + +@pragma('vm:entry-point') +void callbackDispatcher() { + Workmanager().executeTask((task, inputData) async { + try { + switch (task) { + case moneroSyncTaskKey: + + /// The work manager runs on a separate isolate from the main flutter isolate. + /// thus we initialize app configs first; hive, getIt, etc... + await initializeAppConfigs(); + + final walletLoadingService = getIt.get(); + + final node = getIt.get().getCurrentNode(WalletType.monero); + + final typeRaw = getIt.get().getInt(PreferencesKey.currentWalletType); + + WalletBase? wallet; + + if (inputData!['sync_all'] as bool) { + /// get all Monero wallets of the user and sync them + final List moneroWallets = getIt + .get() + .wallets + .where((element) => element.type == WalletType.monero) + .toList(); + + for (int i = 0; i < moneroWallets.length; i++) { + wallet = await walletLoadingService.load(WalletType.monero, moneroWallets[i].name); + + await wallet.connectToNode(node: node); + await wallet.startSync(); + } + } else { + /// if the user chose to sync only active wallet + /// if the current wallet is monero; sync it only + if (typeRaw == WalletType.monero.index) { + final name = + getIt.get().getString(PreferencesKey.currentWalletName); + + wallet = await walletLoadingService.load(WalletType.monero, name!); + + await wallet.connectToNode(node: node); + await wallet.startSync(); + } + } + + if (wallet?.syncStatus.progress() == null) { + return Future.error("No Monero wallet found"); + } + + for (int i = 0;; i++) { + await Future.delayed(const Duration(seconds: 1)); + if (wallet?.syncStatus.progress() == 1.0) { + break; + } + if (i > 600) { + return Future.error("Synchronization Timed out"); + } + } + break; + } + + return Future.value(true); + } catch (error, stackTrace) { + print(error); + print(stackTrace); + return Future.error(error); + } + }); +} + +class BackgroundTasks { + void registerSyncTask({bool changeExisting = false}) async { + try { + bool hasMonero = getIt + .get() + .wallets + .any((element) => element.type == WalletType.monero); + + /// if its not android nor ios, or the user has no monero wallets; exit + if (!DeviceInfo.instance.isMobile || !hasMonero) { + return; + } + + final settingsStore = getIt.get(); + + final SyncMode syncMode = settingsStore.currentSyncMode; + final bool syncAll = settingsStore.currentSyncAll; + + if (syncMode.type == SyncType.disabled) { + cancelSyncTask(); + return; + } + + await Workmanager().initialize( + callbackDispatcher, + isInDebugMode: kDebugMode, + ); + + final inputData = {"sync_all": syncAll}; + final constraints = Constraints( + networkType: + syncMode.type == SyncType.unobtrusive ? NetworkType.unmetered : NetworkType.connected, + requiresBatteryNotLow: syncMode.type == SyncType.unobtrusive, + requiresCharging: syncMode.type == SyncType.unobtrusive, + requiresDeviceIdle: syncMode.type == SyncType.unobtrusive, + ); + + if (Platform.isIOS) { + await Workmanager().registerOneOffTask( + moneroSyncTaskKey, + moneroSyncTaskKey, + initialDelay: syncMode.frequency, + existingWorkPolicy: ExistingWorkPolicy.replace, + inputData: inputData, + constraints: constraints, + ); + return; + } + + await Workmanager().registerPeriodicTask( + moneroSyncTaskKey, + moneroSyncTaskKey, + initialDelay: syncMode.frequency, + frequency: syncMode.frequency, + existingWorkPolicy: changeExisting ? ExistingWorkPolicy.replace : ExistingWorkPolicy.keep, + inputData: inputData, + constraints: constraints, + ); + } catch (error, stackTrace) { + print(error); + print(stackTrace); + } + } + + void cancelSyncTask() { + try { + Workmanager().cancelByUniqueName(moneroSyncTaskKey); + } catch (error, stackTrace) { + print(error); + print(stackTrace); + } + } +} diff --git a/lib/entities/load_current_wallet.dart b/lib/entities/load_current_wallet.dart index 882d1840e..d758b6697 100644 --- a/lib/entities/load_current_wallet.dart +++ b/lib/entities/load_current_wallet.dart @@ -1,8 +1,7 @@ import 'package:cake_wallet/di.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/core/key_service.dart'; -import 'package:cw_core/wallet_service.dart'; +import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; @@ -24,4 +23,6 @@ Future loadCurrentWallet() async { final walletLoadingService = getIt.get(); final wallet = await walletLoadingService.load(type, name); appStore.changeCurrentWallet(wallet); + + getIt.get().registerSyncTask(); } diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 62c47ea02..c50629c1b 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -36,6 +36,8 @@ class PreferencesKey { static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowYatPopup = 'should_show_yat_popup'; static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1'; + static const syncModeKey = 'sync_mode'; + static const syncAllKey = 'sync_all'; static const pinTimeOutDuration = 'pin_timeout_duration'; static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds'; static const lastPopupDate = 'last_popup_date'; diff --git a/lib/main.dart b/lib/main.dart index 0860a167f..e0cf58e62 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -57,97 +57,103 @@ Future main() async { return true; }; - final appDir = await getApplicationDocumentsDirectory(); await Hive.close(); - Hive.init(appDir.path); - if (!Hive.isAdapterRegistered(Contact.typeId)) { - Hive.registerAdapter(ContactAdapter()); - } + await initializeAppConfigs(); - if (!Hive.isAdapterRegistered(Node.typeId)) { - Hive.registerAdapter(NodeAdapter()); - } - - if (!Hive.isAdapterRegistered(TransactionDescription.typeId)) { - Hive.registerAdapter(TransactionDescriptionAdapter()); - } - - if (!Hive.isAdapterRegistered(Trade.typeId)) { - Hive.registerAdapter(TradeAdapter()); - } - - if (!Hive.isAdapterRegistered(WalletInfo.typeId)) { - Hive.registerAdapter(WalletInfoAdapter()); - } - - if (!Hive.isAdapterRegistered(walletTypeTypeId)) { - Hive.registerAdapter(WalletTypeAdapter()); - } - - if (!Hive.isAdapterRegistered(Template.typeId)) { - Hive.registerAdapter(TemplateAdapter()); - } - - if (!Hive.isAdapterRegistered(ExchangeTemplate.typeId)) { - Hive.registerAdapter(ExchangeTemplateAdapter()); - } - - if (!Hive.isAdapterRegistered(Order.typeId)) { - Hive.registerAdapter(OrderAdapter()); - } - - if (!isMoneroOnly && !Hive.isAdapterRegistered(UnspentCoinsInfo.typeId)) { - Hive.registerAdapter(UnspentCoinsInfoAdapter()); - } - - if (!Hive.isAdapterRegistered(AnonpayInvoiceInfo.typeId)) { - Hive.registerAdapter(AnonpayInvoiceInfoAdapter()); - } - - final secureStorage = FlutterSecureStorage(); - final transactionDescriptionsBoxKey = - await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey); - final tradesBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: Trade.boxKey); - final ordersBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: Order.boxKey); - final contacts = await Hive.openBox(Contact.boxName); - final nodes = await Hive.openBox(Node.boxName); - final transactionDescriptions = await Hive.openBox( - TransactionDescription.boxName, - encryptionKey: transactionDescriptionsBoxKey); - final trades = await Hive.openBox(Trade.boxName, encryptionKey: tradesBoxKey); - final orders = await Hive.openBox(Order.boxName, encryptionKey: ordersBoxKey); - final walletInfoSource = await Hive.openBox(WalletInfo.boxName); - final templates = await Hive.openBox