import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:enough_mail/enough_mail.dart' as imap; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:sharedinbox/core/models/account.dart' as model; import 'package:sharedinbox/core/models/user_preferences.dart'; import 'package:sharedinbox/core/repositories/account_repository.dart'; import 'package:sharedinbox/core/services/body_cache_service.dart'; import 'package:sharedinbox/core/services/notification_service.dart'; import 'package:sharedinbox/data/db/database.dart'; import 'package:sharedinbox/data/imap/imap_client_factory.dart'; import 'package:sharedinbox/data/repositories/account_repository_impl.dart'; import 'package:sharedinbox/data/storage/flutter_secure_storage_impl.dart'; import 'package:workmanager/workmanager.dart'; const _kTaskName = 'si_bg_sync'; const _kPrefetchTaskName = 'si_bg_prefetch'; const _kResourceType = 'background_check'; @pragma('vm:entry-point') void callbackDispatcher() { // Required so that path_provider and other plugins are available in this // background isolate (issue #192). WidgetsFlutterBinding.ensureInitialized(); Workmanager().executeTask((taskName, __) async { try { if (taskName == _kPrefetchTaskName) { await _doBodyPrefetch(); } else { await _doBackgroundSync(); } } catch (_) {} return true; }); } Future registerBackgroundSync() async { try { await Workmanager().initialize(callbackDispatcher); await Workmanager().registerPeriodicTask( _kTaskName, _kTaskName, frequency: const Duration(minutes: 15), constraints: Constraints(networkType: NetworkType.connected), existingWorkPolicy: ExistingPeriodicWorkPolicy.keep, ); } on PlatformException { // WorkManager channel unavailable on this device; background sync disabled. } on MissingPluginException { // Plugin not registered on this device; background sync disabled. } catch (_) { // Unexpected initialization failure; background sync disabled. } } /// Registers (or cancels) the body-prefetch WorkManager task based on [mode]. /// Call on app startup and whenever the user changes the prefetch preference. Future registerBodyPrefetchTask(PrefetchMode mode) async { try { if (mode == PrefetchMode.disabled) { await Workmanager().cancelByUniqueName(_kPrefetchTaskName); return; } final networkType = mode == PrefetchMode.wifiOnly ? NetworkType.unmetered : NetworkType.connected; await Workmanager().registerPeriodicTask( _kPrefetchTaskName, _kPrefetchTaskName, frequency: const Duration(hours: 1), constraints: Constraints(networkType: networkType), existingWorkPolicy: ExistingPeriodicWorkPolicy.replace, ); } on PlatformException { // Ignore — WorkManager unavailable. } on MissingPluginException { // Ignore — plugin not registered. } catch (_) {} } Future _doBackgroundSync() async { final dir = await getApplicationSupportDirectory(); final db = AppDatabase( NativeDatabase(File(p.join(dir.path, 'sharedinbox.db'))), ); try { final accountRepo = AccountRepositoryImpl( db, const FlutterSecureStorageImpl(), ); final accounts = await accountRepo.observeAccounts().first; await initNotifications(); for (final account in accounts) { if (account.type != model.AccountType.imap) continue; await _checkAccount(db, accountRepo, account); } } finally { await db.close(); } } Future _doBodyPrefetch() async { final dir = await getApplicationSupportDirectory(); final db = AppDatabase( NativeDatabase(File(p.join(dir.path, 'sharedinbox.db'))), ); try { final accountRepo = AccountRepositoryImpl( db, const FlutterSecureStorageImpl(), ); await BodyCacheService(db, accountRepo).run(); } finally { await db.close(); } } Future _checkAccount( AppDatabase db, AccountRepository accountRepo, model.Account account, ) async { try { final password = await accountRepo.getPassword(account.id); final username = account.username.isNotEmpty ? account.username : account.email; final client = await connectImap(account, username, password); try { final status = await client.statusMailbox( imap.Mailbox.virtual('INBOX', []), [imap.StatusFlags.uidNext], ); final currentUidNext = status.uidNext; final stored = await (db.select(db.syncStates) ..where( (t) => t.accountId.equals(account.id) & t.resourceType.equals(_kResourceType), )) .getSingleOrNull(); final lastUidNext = _parseUidNext(stored?.state); await db.into(db.syncStates).insertOnConflictUpdate( SyncStatesCompanion.insert( accountId: account.id, resourceType: _kResourceType, state: jsonEncode({'uidNext': currentUidNext}), syncedAt: DateTime.now(), ), ); if (lastUidNext != null && currentUidNext != null && currentUidNext > lastUidNext) { await showNewMailNotification(account.email); } } finally { await client.logout(); } } catch (_) {} } int? _parseUidNext(String? state) { if (state == null) return null; try { final decoded = jsonDecode(state); if (decoded is Map) { return decoded['uidNext'] as int?; } return null; } catch (_) { return null; } }