Files
kooltab/lib/viewmodels/update_progress_view_model.dart
T
2026-07-30 04:46:03 +02:00

182 lines
4.6 KiB
Dart

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<void> 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();
}
}