From 567c08b957e55d07b4ce12b7dbbddd7867704be3 Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Tue, 3 Sep 2024 14:31:02 -0700 Subject: [PATCH] it works! --- lib/entities/background_tasks.dart | 362 +++++-------------------- lib/entities/load_current_wallet.dart | 4 +- lib/src/screens/root/root.dart | 49 +++- lib/store/settings_store.dart | 4 +- lib/view_model/wallet_creation_vm.dart | 2 +- 5 files changed, 115 insertions(+), 306 deletions(-) diff --git a/lib/entities/background_tasks.dart b/lib/entities/background_tasks.dart index c16207a60..e65b96213 100644 --- a/lib/entities/background_tasks.dart +++ b/lib/entities/background_tasks.dart @@ -28,164 +28,37 @@ import 'package:http/http.dart' as http; const moneroSyncTaskKey = "com.fotolockr.cakewallet.monero_sync_task"; const mwebSyncTaskKey = "com.fotolockr.cakewallet.mweb_sync_task"; +const initialNotificationTitle = 'Cake Background Sync'; +const initialNotificationContent = 'On standby - app is in the foreground'; +const notificationId = 888; +const notificationChannelId = 'cake_service'; +const notificationChannelName = 'CAKE BG SERVICE'; +const notificationChannelDescription = 'Cake Wallet Background Service'; + @pragma('vm:entry-point') -void callbackDispatcher() { - // Workmanager().executeTask((task, inputData) async { - // try { - // switch (task) { - // case mwebSyncTaskKey: +void callbackDispatcher() {} - // /// 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 List ltcWallets = getIt - // .get() - // .wallets - // .where((element) => [WalletType.litecoin].contains(element.type)) - // .toList(); - - // if (ltcWallets.isEmpty) { - // return Future.error("No ltc wallets found"); - // } - - // final walletLoadingService = getIt.get(); - - // var wallet = - // await walletLoadingService.load(ltcWallets.first.type, ltcWallets.first.name); - - // print("STARTING SYNC FROM BG!!"); - - // final url = Uri.parse("https://webhook.site/a81e49d8-f5bd-4e57-8b1d-5d2c80c43f2a"); - // final response = await http.get(url); - - // if (response.statusCode == 200) { - // print("Background task starting: ${response.body}"); - // } else { - // print("Failed to post webhook.site"); - // } - - // // await wallet.startSync(); - - // // RpcClient _stub = bitcoin!.getMwebStub(); - - // double syncStatus = 0.0; - - // Timer? _syncTimer; - - // // dynamic _stub = await bitcoin!.getMwebStub(wallet); - - // _syncTimer = Timer.periodic(const Duration(milliseconds: 1500), (timer) async { - // // if (syncStatus is FailedSyncStatus) return; - // // TODO: use the proxy layer: - // // final height = await (wallet as ElectrumWallet).electrumClient.getCurrentBlockChainTip() ?? 0; - // final height = 2726590; - // // final height = 0; - // dynamic resp = await bitcoin!.getStatusRequest(wallet); - // int blockHeaderHeight = resp.blockHeaderHeight as int; - // int mwebHeaderHeight = resp.mwebHeaderHeight as int; - // int mwebUtxosHeight = resp.mwebUtxosHeight as int; - - // print("blockHeaderHeight: $blockHeaderHeight"); - // print("mwebHeaderHeight: $mwebHeaderHeight"); - // print("mwebUtxosHeight: $mwebUtxosHeight"); - - // if (blockHeaderHeight < height) { - // syncStatus = blockHeaderHeight / height; - // } else if (mwebHeaderHeight < height) { - // syncStatus = mwebHeaderHeight / height; - // } else if (mwebUtxosHeight < height) { - // syncStatus = 0.999; - // } else { - // syncStatus = 1; - // } - // print("Sync status ${syncStatus}"); - // }); - - // for (int i = 0;; i++) { - // await Future.delayed(const Duration(seconds: 1)); - // if (syncStatus == 1) { - // print("sync done!"); - // break; - // } - // if (i > 600) { - // return Future.error("Synchronization Timed out"); - // } - // } - // _syncTimer?.cancel(); - - // break; - - // 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 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) => [WalletType.monero, WalletType.wownero].contains(element.type)) - // .toList(); - - // for (int i = 0; i < moneroWallets.length; i++) { - // wallet = - // await walletLoadingService.load(moneroWallets[i].type, moneroWallets[i].name); - // final node = getIt.get().getCurrentNode(moneroWallets[i].type); - // 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 || typeRaw == WalletType.wownero.index) { - // final name = - // getIt.get().getString(PreferencesKey.currentWalletName); - - // wallet = await walletLoadingService.load(WalletType.values[typeRaw!], name!); - // final node = getIt.get().getCurrentNode(WalletType.values[typeRaw]); - - // await wallet.connectToNode(node: node); - // await wallet.startSync(); - // } - // } - - // if (wallet?.syncStatus.progress() == null) { - // return Future.error("No Monero/Wownero 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); - // } - // }); +void setNotificationStandby(FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin) async { + flutterLocalNotificationsPlugin.show( + notificationId, + initialNotificationTitle, + initialNotificationContent, + const NotificationDetails( + android: AndroidNotificationDetails( + notificationChannelId, + notificationChannelName, + icon: 'ic_bg_service_small', + ongoing: true, + ), + ), + ); } @pragma('vm:entry-point') Future onStart(ServiceInstance service) async { print("BACKGROUND SERVICE STARTED!"); + bool bgSyncStarted = false; + Timer? _syncTimer; // commented because the behavior appears to be bugged: // DartPluginRegistrant.ensureInitialized(); @@ -193,40 +66,51 @@ Future onStart(ServiceInstance service) async { final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - // this will be used as notification channel id - // this will be used for notification id, So you can update your custom notification with this id. - const notificationChannelId = 'my_foreground'; - const notificationChannelName = 'MY FOREGROUND SERVICE'; - const notificationId = 888; - - service.on('stopService').listen((event) { - service.stopSelf(); + service.on('stopService').listen((event) async { + print("STOPPING SERVICE!"); + _syncTimer?.cancel(); + await service.stopSelf(); }); service.on('status').listen((event) async { print(event); }); - bool bgSyncStarted = false; - - service.on('foreground').listen((event) async { + service.on('setForeground').listen((event) async { bgSyncStarted = false; + _syncTimer?.cancel(); + // try { + // final wallet = getIt.get().wallet; + // wallet?.close(); + // } catch (_) { + // // this throws a few errors + // } + + // setNotificationStandby(flutterLocalNotificationsPlugin); }); - service.on('startBgSync').listen((event) async { + // we have entered the background, start the sync: + service.on('setBackground').listen((event) async { if (bgSyncStarted) { return; } bgSyncStarted = true; + + await Future.delayed(const Duration(seconds: 30)); print("STARTING SYNC FROM BG!!"); - await initializeAppConfigs(); + try { + await initializeAppConfigs(); + } catch (_) { + // this throws a few errors + } print("initialized app configs"); - Timer.periodic(const Duration(milliseconds: 1500), (timer) { + _syncTimer?.cancel(); + _syncTimer = Timer.periodic(const Duration(milliseconds: 1500), (timer) { final wallet = getIt.get().wallet; - final syncProgress = ((wallet?.syncStatus.progress() ?? 0) * 100).toStringAsPrecision(3); + final syncProgress = ((wallet?.syncStatus.progress() ?? 0) * 100).toStringAsPrecision(5); flutterLocalNotificationsPlugin.show( notificationId, @@ -250,92 +134,6 @@ Future onStart(ServiceInstance service) async { // service.stopSelf(); // }); }); - - // final List ltcWallets = getIt - // .get() - // .wallets - // .where((element) => [WalletType.litecoin].contains(element.type)) - // .toList(); - - // if (ltcWallets.isEmpty) { - // return Future.error("No ltc wallets found"); - // } - - // final walletLoadingService = getIt.get(); - - // var wallet = await walletLoadingService.load(ltcWallets.first.type, ltcWallets.first.name); - - // var wallet = getIt.get().wallet; - - // if (wallet?.type != WalletType.litecoin) { - // return; - // } - - // print("STARTING SYNC FROM BG!!"); - - // final url = Uri.parse("https://webhook.site/a81e49d8-f5bd-4e57-8b1d-5d2c80c43f2a"); - // final response = await http.get(url); - - // if (response.statusCode == 200) { - // print("Background task starting: ${response.body}"); - // } else { - // print("Failed to post webhook.site"); - // } - - // Timer.periodic(Duration(milliseconds: 5000), (timer) async { - // print(wallet!.syncStatus); - // }); - - // await wallet.startSync(); - - // RpcClient _stub = bitcoin!.getMwebStub(); - - // double syncStatus = 0.0; - - // Timer? _syncTimer; - - // dynamic _stub = await bitcoin!.getMwebStub(wallet); - - // _syncTimer = Timer.periodic(const Duration(milliseconds: 1500), (timer) async { - // // if (syncStatus is FailedSyncStatus) return; - // // TODO: use the proxy layer: - // // final height = await (wallet as ElectrumWallet).electrumClient.getCurrentBlockChainTip() ?? 0; - // final height = 2726590; - // // final height = 0; - // dynamic resp = await bitcoin!.getStatusRequest(wallet); - // int blockHeaderHeight = resp.blockHeaderHeight as int; - // int mwebHeaderHeight = resp.mwebHeaderHeight as int; - // int mwebUtxosHeight = resp.mwebUtxosHeight as int; - - // print("blockHeaderHeight: $blockHeaderHeight"); - // print("mwebHeaderHeight: $mwebHeaderHeight"); - // print("mwebUtxosHeight: $mwebUtxosHeight"); - - // if (blockHeaderHeight < height) { - // syncStatus = blockHeaderHeight / height; - // } else if (mwebHeaderHeight < height) { - // syncStatus = mwebHeaderHeight / height; - // } else if (mwebUtxosHeight < height) { - // syncStatus = 0.999; - // } else { - // syncStatus = 1; - // } - // print("Sync status ${syncStatus}"); - - // flutterLocalNotificationsPlugin.show( - // notificationId, - // '${syncStatus}', - // 'Awesome ${DateTime.now()}', - // const NotificationDetails( - // android: AndroidNotificationDetails( - // notificationChannelId, - // 'MY FOREGROUND SERVICE', - // icon: 'ic_bg_service_small', - // ongoing: true, - // ), - // ), - // ); - // }); } @pragma('vm:entry-point') @@ -348,10 +146,10 @@ Future onIosBackground(ServiceInstance service) async { Future initializeService(FlutterBackgroundService bgService) async { const AndroidNotificationChannel channel = AndroidNotificationChannel( - 'my_foreground', // id - 'MY FOREGROUND SERVICE', // title - description: 'This channel is used for important notifications.', // description - importance: Importance.low, // importance must be at low or higher level + notificationChannelId, + notificationChannelName, + description: notificationChannelDescription, + importance: Importance.low, ); final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = @@ -374,25 +172,10 @@ Future initializeService(FlutterBackgroundService bgService) async { .resolvePlatformSpecificImplementation() ?.requestNotificationsPermission(); - const initialNotificationTitle = 'Cake Background Sync'; - const initialNotificationContent = 'On standby - app is in the foreground'; - const notificationId = 888; - const notificationChannelId = 'my_foreground'; - const notificationChannelName = 'MY FOREGROUND SERVICE'; + setNotificationStandby(flutterLocalNotificationsPlugin); - flutterLocalNotificationsPlugin.show( - notificationId, - initialNotificationTitle, - initialNotificationContent, - const NotificationDetails( - android: AndroidNotificationDetails( - notificationChannelId, - notificationChannelName, - icon: 'ic_bg_service_small', - ongoing: true, - ), - ), - ); + // notify the service that we are in the foreground: + bgService.invoke("setForeground"); try { bool isServiceRunning = await bgService.isRunning(); @@ -419,22 +202,22 @@ Future initializeService(FlutterBackgroundService bgService) async { onBackground: onIosBackground, ), ); - - bgService.invoke("foreground"); - - Timer.periodic(const Duration(milliseconds: 1000), (timer) { - var wallet = getIt.get().wallet; - if (wallet?.syncStatus.toString() == "stopped") { - bgService.invoke("startBgSync"); - timer.cancel(); - } - }); } class BackgroundTasks { FlutterBackgroundService bgService = FlutterBackgroundService(); - void registerSyncTask({bool changeExisting = false}) async { + void updateServiceState(bool foreground) { + if (foreground) { + // bgService.invoke("setForeground"); + bgService.invoke('stopService'); + initializeService(bgService); + } else { + bgService.invoke("setBackground"); + } + } + + void registerBackgroundService() async { try { bool hasMonero = getIt .get() @@ -461,14 +244,7 @@ class BackgroundTasks { return; } - // try { - // bool isServiceRunning = await bgService.isRunning(); - // if (isServiceRunning) { - // return; - // // print("Service is ALREADY running!"); - // // bgService.invoke('stopService'); - // } - // } catch (_) {} + bgService.invoke('stopService'); await initializeService(bgService); } catch (error, stackTrace) { diff --git a/lib/entities/load_current_wallet.dart b/lib/entities/load_current_wallet.dart index 0e2df96a5..ede06de4d 100644 --- a/lib/entities/load_current_wallet.dart +++ b/lib/entities/load_current_wallet.dart @@ -21,7 +21,5 @@ Future loadCurrentWallet({String? password}) async { final walletLoadingService = getIt.get(); final wallet = await walletLoadingService.load(type, name, password: password); await appStore.changeCurrentWallet(wallet); - - // TODO: potential for infinite loop here since this is run when the wallet is loaded for syncing from the bg: - getIt.get().registerSyncTask(); + getIt.get().registerBackgroundService(); } diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index e62ce6082..e8f444474 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'dart:io'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cw_core/wallet_base.dart'; @@ -52,6 +54,8 @@ class RootState extends State with WidgetsBindingObserver { bool _postFrameCallback; bool _requestAuth; AppLifecycleState _previousState; + bool wasInBackground = false; + Timer? _stateTimer; StreamSubscription? stream; ReactionDisposer? _walletReactionDisposer; @@ -125,12 +129,17 @@ class RootState extends State with WidgetsBindingObserver { ); } + void setPaused() { + wasInBackground = true; + } + @override void didChangeAppLifecycleState(AppLifecycleState state) { + print("previous state: $_previousState current state: $state"); switch (state) { case AppLifecycleState.paused: if (isQrScannerShown) { - return; + break; } if (!_isInactive && widget.authenticationStore.state == AuthenticationState.allowed) { @@ -151,16 +160,42 @@ class RootState extends State with WidgetsBindingObserver { } }); - // prevent triggering startSync from the notifications menu on android / other non-paused states: - if (_previousState == AppLifecycleState.paused) { - if (widget.appStore.wallet?.type == WalletType.litecoin) { - widget.appStore.wallet?.startSync(); - } - } break; default: break; } + + // background service handling: + switch (state) { + case AppLifecycleState.resumed: + // restart the background service if it was running before: + getIt.get().updateServiceState(true); + _stateTimer?.cancel(); + if (!wasInBackground) { + return; + } + wasInBackground = false; + if (widget.appStore.wallet?.type == WalletType.litecoin) { + // wait a few seconds before starting the sync make sure the background service is fully exited: + Future.delayed(const Duration(seconds: 3), () { + widget.appStore.wallet?.startSync(); + }); + } + break; + case AppLifecycleState.paused: + getIt.get().updateServiceState(false); + case AppLifecycleState.inactive: + case AppLifecycleState.detached: + default: + // if we enter any state other than resumed start a timer for 30 seconds + // after which we'll consider the app to be in the background + _stateTimer?.cancel(); + _stateTimer = Timer(const Duration(seconds: 5), () async { + wasInBackground = true; + getIt.get().updateServiceState(false); + }); + break; + } _previousState = state; } diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 1b1536fdd..78c8dd03d 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -384,13 +384,13 @@ abstract class SettingsStoreBase with Store { reaction((_) => currentSyncMode, (SyncMode syncMode) { sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode.type.index); - _backgroundTasks.registerSyncTask(changeExisting: true); + _backgroundTasks.registerBackgroundService(); }); reaction((_) => currentSyncAll, (bool syncAll) { sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); - _backgroundTasks.registerSyncTask(changeExisting: true); + _backgroundTasks.registerBackgroundService(); }); reaction( diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 5760f5eec..d58012a25 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -108,7 +108,7 @@ abstract class WalletCreationVMBase with Store { walletInfo.address = wallet.walletAddresses.address; await _walletInfoSource.add(walletInfo); await _appStore.changeCurrentWallet(wallet); - getIt.get().registerSyncTask(); + getIt.get().registerBackgroundService(); _appStore.authenticationStore.allowed(); state = ExecutedSuccessfullyState(); } catch (e, _) {