182 lines
4.6 KiB
Dart
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();
|
|
}
|
|
}
|