diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 44d370e59..e3822ae2d 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -60,6 +60,8 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans bool get isHardwareWallet => walletInfo.isHardwareWallet; + Future<void> init(); + Future<void> connectToNode({required Node node}); // there is a default definition here because only coins with a pow node (nano based) need to override this diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index dc3efcb91..f4ecb9014 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -158,10 +158,7 @@ abstract class MoneroWalletBase } } - // TODO: this doesn't work, need to find a better way to check if we are on the main thread - bool isMainThread = Isolate.current.debugName == "main"; - printV("isMainThread: $isMainThread"); - + _autoSaveTimer?.cancel(); _autoSaveTimer = Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save()); @@ -275,6 +272,8 @@ abstract class MoneroWalletBase return; } monero_wallet.stopSync(); + _autoSaveTimer?.cancel(); + monero_wallet.closeCurrentWallet(); } @override @@ -737,13 +736,17 @@ abstract class MoneroWalletBase if (walletInfo.isRecovery) { await _askForUpdateTransactionHistory(); _askForUpdateBalance(); - walletAddresses.accountList.update(); + if (!isBackgroundSyncing) { + walletAddresses.accountList.update(); + } } if (blocksLeft < 100) { await _askForUpdateTransactionHistory(); _askForUpdateBalance(); - walletAddresses.accountList.update(); + if (!isBackgroundSyncing) { + walletAddresses.accountList.update(); + } syncStatus = SyncedSyncStatus(); if (!_hasSyncAfterStartup) { diff --git a/lib/entities/background_tasks.dart b/lib/entities/background_tasks.dart index b5416962e..1ca9a1536 100644 --- a/lib/entities/background_tasks.dart +++ b/lib/entities/background_tasks.dart @@ -118,6 +118,22 @@ void setWalletNotification(FlutterLocalNotificationsPlugin flutterLocalNotificat ); } +AppLifecycleState appStateFromString(String state) { + switch (state) { + case "AppLifecycleState.paused": + return AppLifecycleState.paused; + case "AppLifecycleState.resumed": + return AppLifecycleState.resumed; + case "AppLifecycleState.hidden": + return AppLifecycleState.hidden; + case "AppLifecycleState.detached": + return AppLifecycleState.detached; + case "AppLifecycleState.inactive": + return AppLifecycleState.inactive; + } + throw Exception("unknown app state: $state"); +} + @pragma("vm:entry-point") Future<void> onStart(ServiceInstance service) async { printV("BACKGROUND SERVICE STARTED"); @@ -125,10 +141,13 @@ Future<void> onStart(ServiceInstance service) async { Timer? _syncTimer; Timer? _stuckSyncTimer; Timer? _queueTimer; + Timer? _appStateTimer; List<WalletBase> syncingWallets = []; List<WalletBase> standbyWallets = []; Timer? _bgTimer; - int fgCount = 0; + AppLifecycleState lastAppState = AppLifecycleState.resumed; + final List<AppLifecycleState> lastAppStates = []; + String serviceState = "NOT_RUNNING"; // commented because the behavior appears to be bugged: // DartPluginRegistrant.ensureInitialized(); @@ -136,8 +155,7 @@ Future<void> onStart(ServiceInstance service) async { final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - service.on("stopService").listen((event) async { - printV("STOPPING BACKGROUND SERVICE"); + Future<void> stopAllSyncing() async { _syncTimer?.cancel(); _stuckSyncTimer?.cancel(); _queueTimer?.cancel(); @@ -157,6 +175,11 @@ Future<void> onStart(ServiceInstance service) async { } catch (e) { printV("error stopping sync: $e"); } + } + + service.on("stopService").listen((event) async { + printV("STOPPING BACKGROUND SERVICE"); + await stopAllSyncing(); // stop the service itself: service.invoke("serviceState", {"state": "NOT_RUNNING"}); await service.stopSelf(); @@ -166,36 +189,39 @@ Future<void> onStart(ServiceInstance service) async { printV(event); }); - void setForeground() { + Future<void> setForeground() async { + serviceState = "FOREGROUND"; bgSyncStarted = false; - _syncTimer?.cancel(); setNotificationStandby(flutterLocalNotificationsPlugin); } service.on("setForeground").listen((event) async { - setForeground(); + await setForeground(); service.invoke("serviceState", {"state": "FOREGROUND"}); }); void setReady() { - setNotificationReady(flutterLocalNotificationsPlugin); + if (serviceState != "READY" && serviceState != "BACKGROUND") { + serviceState = "READY"; + setNotificationReady(flutterLocalNotificationsPlugin); + } } service.on("setReady").listen((event) async { setReady(); - service.invoke("serviceState", {"state": "READY"}); }); - service.on("foregroundPing").listen((event) async { - fgCount = 0; + service.on("appState").listen((event) async { + printV("APP STATE: ${event?["state"]}"); + lastAppState = appStateFromString(event?["state"] as String); }); // we have entered the background, start the sync: void setBackground() async { - if (bgSyncStarted) { - return; - } + // only runs once per service instance: + if (bgSyncStarted) return; bgSyncStarted = true; + serviceState = "BACKGROUND"; await Future.delayed(const Duration(seconds: DELAY_SECONDS_BEFORE_SYNC_START)); printV("STARTING SYNC FROM BG"); @@ -482,23 +508,24 @@ Future<void> onStart(ServiceInstance service) async { setBackground(); }); - // this is a backup timer to trigger in case the user fully closes the app, so that we still - // start the background sync process: - // annoyingly foreground code still runs in the background on android for some time, so we still use the original method - // to detect if we are in the background since it's much faster - _bgTimer = Timer.periodic(const Duration(seconds: 1), (timer) async { - fgCount++; - // we haven't been pinged in a while, so we are likely in the background: - if (fgCount == 4) { - setReady(); - return; + // if the app state changes to paused, setReady() + // if the app state has been paused for more than 10 seconds, setBackground() + _appStateTimer = Timer.periodic(const Duration(seconds: 1), (timer) async { + lastAppStates.add(lastAppState); + if (lastAppStates.length > 10) { + lastAppStates.removeAt(0); } - - if (fgCount > 10) { - fgCount = 0; + // printV(lastAppStates); + // if (lastAppState == AppLifecycleState.resumed && serviceState != "FOREGROUND") { + // setForeground(); + // } + if (lastAppStates.length < 5) return; + if (lastAppState == AppLifecycleState.paused && serviceState != "READY") { + setReady(); + } + // if all 10 states are paused, setBackground() + if (lastAppStates.every((state) => state == AppLifecycleState.paused) && !bgSyncStarted) { setBackground(); - service.invoke("serviceState", {"state": "BACKGROUND"}); - _bgTimer?.cancel(); } }); } @@ -592,16 +619,22 @@ class BackgroundTasks { bgService.invoke("foregroundPing"); } - Future<void> serviceForeground() async { - if (serviceState == "FOREGROUND") { - return; - } + void lastAppState(AppLifecycleState state) { + bgService.invoke("appState", {"state": state.toString()}); + } + Future<void> serviceForeground() async { + printV("SERVICE FOREGROUNDED"); final settingsStore = getIt.get<SettingsStore>(); bool showNotifications = settingsStore.showSyncNotification; bgService.invoke("stopService"); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(const Duration(seconds: 3)); initializeService(bgService, showNotifications); + bgService.invoke("setForeground"); + } + + Future<bool> isServiceRunning() async { + return await bgService.isRunning(); } void serviceReady() { diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 95313d386..53005c63d 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -133,7 +133,7 @@ class RootState extends State<Root> with WidgetsBindingObserver { } @override - void didChangeAppLifecycleState(AppLifecycleState state) { + void didChangeAppLifecycleState(AppLifecycleState state) async { final syncingWalletTypes = [WalletType.litecoin, WalletType.monero, WalletType.bitcoin]; switch (state) { case AppLifecycleState.paused: @@ -145,10 +145,6 @@ class RootState extends State<Root> with WidgetsBindingObserver { setState(() => _setInactive(true)); } - if (FeatureFlag.isBackgroundSyncEnabled && syncingWalletTypes.contains(widget.appStore.wallet?.type)) { - widget.appStore.wallet?.stopSync(); - } - break; case AppLifecycleState.resumed: widget.authService.requireAuth().then((value) { @@ -163,37 +159,53 @@ class RootState extends State<Root> with WidgetsBindingObserver { break; } + // _stateTimer?.cancel(); + // _stateTimer = Timer(const Duration(seconds: 1), () async { + // getIt.get<BackgroundTasks>().lastAppState(state); + // }); + + getIt.get<BackgroundTasks>().lastAppState(state); + // background service handling: + printV("state: $state"); switch (state) { case AppLifecycleState.resumed: - // restart the background service if it was running before: - getIt.get<BackgroundTasks>().serviceForeground(); - _stateTimer?.cancel(); - if (!wasInBackground) { - return; - } - wasInBackground = false; - if (syncingWalletTypes.contains(widget.appStore.wallet?.type)) { - // 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(); - }); - } + // // restart the background service if it was running before: + await getIt.get<BackgroundTasks>().serviceForeground(); + // _stateTimer?.cancel(); + // if (!wasInBackground) { + // return; + // } + // wasInBackground = false; + // final appStore = widget.appStore; + // final wallet = appStore.wallet; + // if (syncingWalletTypes.contains(wallet?.type)) { + // // wait a few seconds before starting the sync to make sure the background service is fully exited: + // Future.delayed(const Duration(seconds: 50), () async { + // final node = appStore.settingsStore.getCurrentNode(wallet!.type); + // await wallet.stopSync(); + // await wallet.init(); + // wallet.connectToNode(node: node); + // wallet.startSync(); + // }); + // } break; - case AppLifecycleState.paused: - getIt.get<BackgroundTasks>().serviceReady(); + case AppLifecycleState.hidden: case AppLifecycleState.inactive: case AppLifecycleState.detached: - default: - // anything other than resumed update the notification to say we're in the "ready" state: - // 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(); - // TODO: bump this to > 30 seconds when testing is done: - _stateTimer = Timer(const Duration(seconds: 10), () async { - wasInBackground = true; - getIt.get<BackgroundTasks>().serviceBackground(); - }); + break; + case AppLifecycleState.paused: + widget.appStore.wallet?.stopSync(); + // getIt.get<BackgroundTasks>().serviceReady(); + // // if (FeatureFlag.isBackgroundSyncEnabled && + // // syncingWalletTypes.contains(widget.appStore.wallet?.type)) { + // // widget.appStore.wallet?.stopSync(); + // // } + // _stateTimer?.cancel(); + // _stateTimer = Timer(const Duration(seconds: 10), () async { + // wasInBackground = true; + // getIt.get<BackgroundTasks>().serviceBackground(); + // }); break; } _previousState = state;