import 'dart:async'; import 'dart:io'; import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter_background_service/flutter_background_service.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; const _notificationChannelId = 'worldhopper_downloads'; const _notificationId = 888; const _completeNotificationId = 889; /// Plugin instance for showing rich notifications from the main isolate. FlutterLocalNotificationsPlugin? _flnPlugin; /// Timestamp of the last progress notification update (for throttling). DateTime _lastProgressUpdate = DateTime(0); /// Initializes the background service configuration. /// Must be called once at app startup. Future initializeBackgroundService() async { final service = FlutterBackgroundService(); if (Platform.isAndroid) { final plugin = FlutterLocalNotificationsPlugin(); const channel = AndroidNotificationChannel( _notificationChannelId, 'Downloads', description: 'Worldhopper download progress', importance: Importance.low, ); await plugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.createNotificationChannel(channel); const initSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); await plugin.initialize( const InitializationSettings(android: initSettings), ); _flnPlugin = plugin; } await service.configure( androidConfiguration: AndroidConfiguration( onStart: _onStart, autoStart: false, isForegroundMode: true, notificationChannelId: _notificationChannelId, initialNotificationTitle: 'Worldhopper', initialNotificationContent: 'Downloading...', foregroundServiceNotificationId: _notificationId, foregroundServiceTypes: [AndroidForegroundType.dataSync], ), iosConfiguration: IosConfiguration( autoStart: false, onForeground: _onStart, onBackground: _onIosBackground, ), ); } @pragma('vm:entry-point') Future _onIosBackground(ServiceInstance service) async { DartPluginRegistrant.ensureInitialized(); return true; } /// Background isolate entry point. /// This isolate does NO work itself — it only keeps the Android foreground /// service (and its notification) alive. The main isolate sends /// `stopService` events. Notification updates are handled directly /// via flutter_local_notifications from the main isolate. @pragma('vm:entry-point') void _onStart(ServiceInstance service) async { DartPluginRegistrant.ensureInitialized(); // Main isolate tells us to stop when all downloads finish service.on('stopService').listen((_) { service.stopSelf(); }); } // --------------------------------------------------------------------------- // Public helpers called from the main isolate // --------------------------------------------------------------------------- /// Start the foreground service so Android keeps the process alive. Future startForegroundService() async { if (!Platform.isAndroid) return; try { final service = FlutterBackgroundService(); final running = await service.isRunning(); if (!running) { await service.startService(); } } catch (e) { debugPrint('Could not start foreground service: $e'); } } /// Update the foreground notification from the main isolate. /// /// When [progress] and [maxProgress] are provided, shows a native progress /// bar. Updates are throttled to at most once per 500ms unless [force] is true. void updateDownloadNotification({ required String title, required String content, int? progress, int? maxProgress, bool force = false, }) { if (!Platform.isAndroid) return; // Throttle: skip if <500ms since last update (unless forced) final now = DateTime.now(); if (!force && now.difference(_lastProgressUpdate).inMilliseconds < 500) { return; } _lastProgressUpdate = now; // Try rich notification via flutter_local_notifications if (_flnPlugin != null && progress != null && maxProgress != null) { final details = AndroidNotificationDetails( _notificationChannelId, 'Downloads', channelDescription: 'Worldhopper download progress', importance: Importance.low, priority: Priority.low, showProgress: true, maxProgress: maxProgress, progress: progress, ongoing: true, onlyAlertOnce: true, icon: '@mipmap/ic_launcher', ); _flnPlugin! .show( _notificationId, title, content, NotificationDetails(android: details), ) .catchError( (e) => debugPrint('Failed to show notification: $e'), ); return; } // Fallback: text-only via background service try { final service = FlutterBackgroundService(); service.invoke('updateNotification', { 'title': title, 'content': content, }); } catch (e) { debugPrint('Notification fallback failed: $e'); } } /// Dismiss the progress notification (e.g. when download finishes). void dismissDownloadNotification() { if (!Platform.isAndroid) return; _flnPlugin?.cancel(_notificationId); } /// Show a brief "download complete" notification that auto-dismisses on tap. void showDownloadCompleteNotification({ required String title, required String body, }) { if (!Platform.isAndroid || _flnPlugin == null) return; const details = AndroidNotificationDetails( _notificationChannelId, 'Downloads', channelDescription: 'Worldhopper download progress', importance: Importance.defaultImportance, priority: Priority.defaultPriority, autoCancel: true, icon: '@mipmap/ic_launcher', ); _flnPlugin! .show( _completeNotificationId, title, body, const NotificationDetails(android: details), ) .catchError( (e) => debugPrint('Failed to show completion notification: $e'), ); } /// Stop the foreground service (e.g. when all downloads finish). Future stopForegroundService() async { if (!Platform.isAndroid) return; try { final service = FlutterBackgroundService(); final running = await service.isRunning(); if (running) { service.invoke('stopService'); } } catch (e) { debugPrint('Could not stop foreground service: $e'); } }