import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:ota_update/ota_update.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import '../utils/app_config.dart'; import '../utils/app_update_util.dart'; enum OtaPhase { downloading, installing, done, error } class UpdateProgressViewModel extends ChangeNotifier { final UpdateInfo update; final bool simulate; final bool simulateError; OtaPhase _phase = OtaPhase.downloading; double _progress = 0; String _statusText = 'Preparing download…'; String? _errorMessage; bool _done = false; StreamSubscription? _subscription; Timer? _simTimer; OtaPhase get phase => _phase; double get progress => _progress; String get statusText => _statusText; String? get errorMessage => _errorMessage; UpdateProgressViewModel({ required this.update, this.simulate = false, this.simulateError = false, }); Future start() async { if (simulate) { _runSimulation(); return; } final url = update.download.startsWith('http') ? update.download : '$kUpdateServerUrl${update.download}'; debugPrint('UpdateProgressViewModel: starting download from $url'); _subscription = OtaUpdate() .execute( url, destinationFilename: 'update.apk', sha256checksum: update.sha256, ) .listen( _onEvent, onError: _onError, onDone: _onDone, ); } void _runSimulation() { debugPrint('UpdateProgressViewModel: running simulated download'); int step = 0; const totalSteps = 20; const stepDuration = Duration(milliseconds: 500); _simTimer = Timer.periodic(stepDuration, (timer) { step++; if (simulateError && step == 10) { timer.cancel(); _phase = OtaPhase.error; _errorMessage = 'Simulated download error (network timeout)'; _statusText = 'Download failed'; notifyListeners(); return; } if (step < totalSteps) { _progress = step / totalSteps; _statusText = 'Downloading ${(_progress * 100).toStringAsFixed(0)}%'; _phase = OtaPhase.downloading; notifyListeners(); } else { timer.cancel(); _startInstallPhase(); } }); } void _startInstallPhase() { _phase = OtaPhase.installing; _progress = 1.0; _statusText = 'Installing update…'; notifyListeners(); _simTimer = Timer(const Duration(seconds: 3), () { _phase = OtaPhase.done; _progress = 1.0; _statusText = 'Update installed! Restarting…'; notifyListeners(); }); } void _onDone() { if (_done) return; _done = true; _phase = OtaPhase.done; _progress = 1.0; _statusText = 'Update installed! Restarting…'; notifyListeners(); } void _onEvent(OtaEvent event) { switch (event.status) { case OtaStatus.DOWNLOADING: final raw = double.tryParse(event.value ?? '0') ?? 0; _progress = (raw / 100).clamp(0.0, 1.0); _statusText = 'Downloading ${_progress.toStringAsFixed(0)}%'; _phase = OtaPhase.downloading; notifyListeners(); case OtaStatus.INSTALLING: _phase = OtaPhase.installing; _progress = 1.0; _statusText = 'Installing update…'; notifyListeners(); case OtaStatus.INSTALLATION_DONE: _onDone(); return; case OtaStatus.DOWNLOAD_ERROR: _phase = OtaPhase.error; _errorMessage = 'Download failed: ${event.value}'; _statusText = 'Download failed'; Sentry.captureMessage('OTA download error: ${event.value}'); notifyListeners(); case OtaStatus.INSTALLATION_ERROR: _phase = OtaPhase.error; _errorMessage = 'Installation failed: ${event.value}'; _statusText = 'Installation failed'; Sentry.captureMessage('OTA installation error: ${event.value}'); notifyListeners(); case OtaStatus.ALREADY_RUNNING_ERROR: _phase = OtaPhase.error; _errorMessage = 'Update already in progress'; _statusText = 'Update already running'; notifyListeners(); default: break; } } void _onError(Object error, StackTrace stack) { debugPrint('UpdateProgressViewModel: stream error: $error'); _phase = OtaPhase.error; _errorMessage = error.toString(); _statusText = 'Update failed'; Sentry.captureException(error, stackTrace: stack); notifyListeners(); } void cancel() { _simTimer?.cancel(); _simTimer = null; _subscription?.cancel(); _subscription = null; } @override void dispose() { cancel(); super.dispose(); } }