| 1 | import 'dart:async'; |
| 2 | import 'dart:io'; |
| 3 | |
| 4 | import 'package:cake_wallet/core/key_service.dart'; |
| 5 | import 'package:cake_wallet/core/wallet_loading_service.dart'; |
| 6 | import 'package:cake_wallet/di.dart'; |
| 7 | import 'package:cake_wallet/entities/preferences_key.dart'; |
| 8 | import 'package:cake_wallet/reactions/wallet_connect.dart'; |
| 9 | import 'package:cake_wallet/store/settings_store.dart'; |
| 10 | import 'package:cake_wallet/utils/feature_flag.dart'; |
| 11 | import 'package:cake_wallet/utils/tor.dart'; |
| 12 | import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; |
| 13 | import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; |
| 14 | import 'package:cw_core/sync_status.dart'; |
| 15 | import 'package:cw_core/transaction_direction.dart'; |
| 16 | import 'package:cw_core/utils/print_verbose.dart'; |
| 17 | import 'package:cw_core/wallet_type.dart'; |
| 18 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; |
| 19 | import 'package:shared_preferences/shared_preferences.dart'; |
| 20 | import 'package:cake_wallet/evm/evm.dart'; |
| 21 | |
| 22 | class BackgroundSync { |
| 23 | final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin(); |
| 24 | bool _isInitialized = false; |
| 25 | |
| 26 | Future<void> _initializeNotifications() async { |
| 27 | if (_isInitialized) return; |
| 28 | |
| 29 | const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); |
| 30 | |
| 31 | const iosSettings = DarwinInitializationSettings( |
| 32 | requestAlertPermission: true, |
| 33 | requestBadgePermission: true, |
| 34 | requestSoundPermission: true, |
| 35 | ); |
| 36 | |
| 37 | const initializationSettings = InitializationSettings( |
| 38 | android: androidSettings, |
| 39 | iOS: iosSettings, |
| 40 | ); |
| 41 | |
| 42 | await _notificationsPlugin.initialize(initializationSettings); |
| 43 | _isInitialized = true; |
| 44 | } |
| 45 | |
| 46 | Future<bool> requestPermissions() async { |
| 47 | if (Platform.isIOS || Platform.isMacOS) { |
| 48 | return await _notificationsPlugin |
| 49 | .resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>() |
| 50 | ?.requestPermissions( |
| 51 | alert: true, |
| 52 | badge: true, |
| 53 | sound: true, |
| 54 | ) ?? |
| 55 | false; |
| 56 | } else if (Platform.isAndroid) { |
| 57 | return await _notificationsPlugin |
| 58 | .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>() |
| 59 | ?.areNotificationsEnabled() ?? |
| 60 | false; |
| 61 | } |
| 62 | return false; |
| 63 | } |
| 64 | |
| 65 | Future<void> showNotification(String title, String content) async { |
| 66 | await _initializeNotifications(); |
| 67 | final hasPermission = await requestPermissions(); |
| 68 | |
| 69 | if (!hasPermission) { |
| 70 | printV('Notification permissions not granted'); |
| 71 | return; |
| 72 | } |
| 73 | |
| 74 | const androidDetails = AndroidNotificationDetails( |
| 75 | 'transactions', |
| 76 | 'Transactions', |
| 77 | channelDescription: 'Channel for notifications about transactions', |
| 78 | importance: Importance.defaultImportance, |
| 79 | priority: Priority.defaultPriority, |
| 80 | ); |
| 81 | |
| 82 | const iosDetails = DarwinNotificationDetails(); |
| 83 | |
| 84 | const notificationDetails = NotificationDetails( |
| 85 | android: androidDetails, |
| 86 | iOS: iosDetails, |
| 87 | ); |
| 88 | |
| 89 | await _notificationsPlugin.show( |
| 90 | DateTime.now().millisecondsSinceEpoch.hashCode, |
| 91 | title, |
| 92 | content, |
| 93 | notificationDetails, |
| 94 | ); |
| 95 | } |
| 96 | |
| 97 | Future<void> sync() async { |
| 98 | final settingsStore = getIt.get<SettingsStore>(); |
| 99 | if (settingsStore.currentBuiltinTor) { |
| 100 | printV("Starting Tor"); |
| 101 | await ensureTorStarted(context: null); |
| 102 | } |
| 103 | printV("Background sync started"); |
| 104 | await _syncWallets(); |
| 105 | printV("Background sync completed"); |
| 106 | } |
| 107 | |
| 108 | Future<void> _syncWallets() async { |
| 109 | final walletLoadingService = getIt.get<WalletLoadingService>(); |
| 110 | final walletListViewModel = getIt.get<WalletListViewModel>(); |
| 111 | final settingsStore = getIt.get<SettingsStore>(); |
| 112 | |
| 113 | final List<WalletListItem> moneroWallets = walletListViewModel.wallets |
| 114 | .where((element) => !element.isHardware) |
| 115 | .where((element) => ![WalletType.haven, WalletType.decred].contains(element.type)) |
| 116 | .toList(); |
| 117 | for (int i = 0; i < moneroWallets.length; i++) { |
| 118 | final wallet = await walletLoadingService.load(moneroWallets[i].type, moneroWallets[i].name, |
| 119 | isBackground: true); |
| 120 | int syncedTicks = 0; |
| 121 | final keyService = getIt.get<KeyService>(); |
| 122 | |
| 123 | int stuckTicks = 0; |
| 124 | |
| 125 | inner: |
| 126 | while (true) { |
| 127 | await Future.delayed(const Duration(seconds: 1)); |
| 128 | final syncStatus = wallet.syncStatus; |
| 129 | final progress = syncStatus.progress(); |
| 130 | if (syncStatus is ConnectedSyncStatus || |
| 131 | syncStatus is AttemptingSyncStatus || |
| 132 | syncStatus is NotConnectedSyncStatus) { |
| 133 | stuckTicks++; |
| 134 | if (stuckTicks > 30) { |
| 135 | printV("${wallet.name} STUCK SYNCING"); |
| 136 | break inner; |
| 137 | } |
| 138 | } else { |
| 139 | stuckTicks = 0; |
| 140 | } |
| 141 | if (syncStatus is NotConnectedSyncStatus) { |
| 142 | printV("${wallet.name} NOT CONNECTED"); |
| 143 | |
| 144 | int? chainId; |
| 145 | if (isEVMCompatibleChain(wallet.type)) { |
| 146 | chainId = evm!.getSelectedChainId(wallet); |
| 147 | } |
| 148 | |
| 149 | final node = settingsStore.getCurrentNode(wallet.type, chainId: chainId); |
| 150 | await wallet.connectToNode(node: node); |
| 151 | await wallet.startBackgroundSync(); |
| 152 | printV("STARTED SYNC"); |
| 153 | continue inner; |
| 154 | } |
| 155 | |
| 156 | if (progress > 0.999 || syncStatus is SyncedSyncStatus) { |
| 157 | syncedTicks++; |
| 158 | if (syncedTicks > 5) { |
| 159 | syncedTicks = 0; |
| 160 | printV("WALLET $i SYNCED"); |
| 161 | try { |
| 162 | await wallet.stopBackgroundSync( |
| 163 | (await keyService.getWalletPassword(walletName: wallet.name))); |
| 164 | } catch (e) { |
| 165 | printV("error stopping sync: $e"); |
| 166 | } |
| 167 | break inner; |
| 168 | } |
| 169 | } else { |
| 170 | syncedTicks = 0; |
| 171 | } |
| 172 | if (FeatureFlag.hasDevOptions) { |
| 173 | if (syncStatus is SyncingSyncStatus) { |
| 174 | final blocksLeft = syncStatus.blocksLeft; |
| 175 | printV("$blocksLeft Blocks Left"); |
| 176 | } else if (syncStatus is SyncedSyncStatus) { |
| 177 | printV("Synced"); |
| 178 | } else if (syncStatus is SyncedTipSyncStatus) { |
| 179 | printV("Scanned Tip: ${syncStatus.tip}"); |
| 180 | } else if (syncStatus is NotConnectedSyncStatus) { |
| 181 | printV("Still Not Connected"); |
| 182 | } else if (syncStatus is AttemptingSyncStatus) { |
| 183 | printV("Attempting Sync"); |
| 184 | } else if (syncStatus is StartingScanSyncStatus) { |
| 185 | printV("Starting Scan"); |
| 186 | } else if (syncStatus is SyncronizingSyncStatus) { |
| 187 | printV("Syncronizing"); |
| 188 | } else if (syncStatus is FailedSyncStatus) { |
| 189 | printV("Failed Sync"); |
| 190 | } else if (syncStatus is ConnectingSyncStatus) { |
| 191 | printV("Connecting"); |
| 192 | } else { |
| 193 | printV("Unknown Sync Status ${syncStatus.runtimeType}"); |
| 194 | } |
| 195 | } |
| 196 | } |
| 197 | final txs = wallet.transactionHistory; |
| 198 | final sortedTxs = txs.transactions.values.toList()..sort((a, b) => a.date.compareTo(b.date)); |
| 199 | final sharedPreferences = await SharedPreferences.getInstance(); |
| 200 | for (final tx in sortedTxs) { |
| 201 | final lastTriggerString = |
| 202 | sharedPreferences.getString(PreferencesKey.backgroundSyncLastTrigger(wallet.name)); |
| 203 | final lastTriggerDate = |
| 204 | lastTriggerString != null ? DateTime.parse(lastTriggerString) : DateTime.now(); |
| 205 | final keys = sharedPreferences.getKeys(); |
| 206 | if (tx.date.isBefore(lastTriggerDate)) { |
| 207 | printV( |
| 208 | "w: ${wallet.name}, tx: ${tx.date} is before $lastTriggerDate (lastTriggerString: $lastTriggerString) (k: ${keys.length})"); |
| 209 | continue; |
| 210 | } |
| 211 | await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name), |
| 212 | tx.date.add(Duration(minutes: 1)).toIso8601String()); |
| 213 | final action = tx.additionalInfo['isAutoShield'] == true |
| 214 | ? "Autoshield" |
| 215 | : tx.direction == TransactionDirection.incoming |
| 216 | ? "Received" |
| 217 | : "Sent"; |
| 218 | if (sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false) { |
| 219 | await showNotification( |
| 220 | "$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amount.toString()}"); |
| 221 | } |
| 222 | printV( |
| 223 | "${wallet.currency.fullName} in ${wallet.name}: TX: ${tx.date} ${tx.amount} ${tx.direction}"); |
| 224 | } |
| 225 | wallet.id; |
| 226 | await wallet.stopBackgroundSync(await keyService.getWalletPassword(walletName: wallet.name)); |
| 227 | await wallet.close(shouldCleanup: true); |
| 228 | } |
| 229 | } |
| 230 | } |