Files
kooltab/lib/viewmodels/update_progress_view_model.dart
T

209 lines
5.4 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:ota_update/ota_update.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../models/update_failure.dart';
import '../utils/app_config.dart';
import '../utils/app_update_util.dart';
enum UpdatePhase {
downloading,
installing,
/// Android's installer prompt is open; the app is replaced once the user
/// confirms it, so there is no "done" state to show.
waitingForInstaller,
failed,
}
class UpdateProgressViewModel extends ChangeNotifier {
final UpdateInfo update;
final bool simulate;
final bool simulateError;
UpdatePhase _phase = UpdatePhase.downloading;
double? _progress;
UpdateFailure? _failure;
String? _currentVersion;
StreamSubscription<OtaEvent>? _subscription;
Timer? _simTimer;
UpdatePhase get phase => _phase;
/// Download progress from 0 to 1, or null while it is not known yet.
double? get progress => _progress;
UpdateFailure? get failure => _failure;
String? get currentVersion => _currentVersion;
bool get isBusy =>
_phase == UpdatePhase.downloading || _phase == UpdatePhase.installing;
UpdateProgressViewModel({
required this.update,
this.simulate = false,
this.simulateError = false,
});
void init() {
unawaited(_loadCurrentVersion());
start();
}
void start() {
cancel();
_phase = UpdatePhase.downloading;
_progress = null;
_failure = null;
notifyListeners();
if (simulate) {
_runSimulation();
return;
}
final url = update.download.startsWith('http')
? update.download
: '$kUpdateServerUrl${update.download}';
_subscription = OtaUpdate()
.execute(
url,
destinationFilename: 'update.apk',
sha256checksum: update.sha256,
)
.listen(_onEvent, onError: _onError, onDone: _onDone);
}
void cancel() {
_simTimer?.cancel();
_simTimer = null;
unawaited(_subscription?.cancel());
_subscription = null;
}
Future<void> _loadCurrentVersion() async {
try {
final info = await PackageInfo.fromPlatform();
_currentVersion = info.version;
notifyListeners();
} catch (e) {
debugPrint('UpdateProgressViewModel: could not read version: $e');
}
}
void _onEvent(OtaEvent event) {
switch (event.status) {
case OtaStatus.DOWNLOADING:
final percent = double.tryParse(event.value ?? '') ?? 0;
_progress = (percent / 100).clamp(0.0, 1.0);
_phase = UpdatePhase.downloading;
notifyListeners();
case OtaStatus.INSTALLING:
_progress = 1;
_phase = UpdatePhase.installing;
notifyListeners();
case OtaStatus.INSTALLATION_DONE:
_phase = UpdatePhase.waitingForInstaller;
notifyListeners();
case OtaStatus.DOWNLOAD_ERROR:
_fail(UpdateFailure.download, event.value);
case OtaStatus.CHECKSUM_ERROR:
_fail(UpdateFailure.checksum, event.value);
case OtaStatus.PERMISSION_NOT_GRANTED_ERROR:
_fail(UpdateFailure.permission, event.value);
case OtaStatus.INSTALLATION_ERROR:
_fail(UpdateFailure.install, event.value);
case OtaStatus.ALREADY_RUNNING_ERROR:
_fail(UpdateFailure.alreadyRunning, event.value);
case OtaStatus.INTERNAL_ERROR:
_fail(UpdateFailure.unknown, event.value);
case OtaStatus.CANCELED:
break;
}
}
/// The plugin closes the stream once it has handed the APK to Android's
/// installer; closing earlier means it gave up without telling why.
void _onDone() {
_subscription = null;
switch (_phase) {
case UpdatePhase.installing:
_phase = UpdatePhase.waitingForInstaller;
notifyListeners();
case UpdatePhase.downloading:
_fail(UpdateFailure.unknown, 'Update stream closed while downloading');
case UpdatePhase.waitingForInstaller || UpdatePhase.failed:
break;
}
}
void _onError(Object error, StackTrace stackTrace) {
debugPrint('UpdateProgressViewModel: stream error: $error');
unawaited(Sentry.captureException(error, stackTrace: stackTrace));
_fail(UpdateFailure.unknown, null, report: false);
}
void _fail(UpdateFailure failure, String? detail, {bool report = true}) {
debugPrint('UpdateProgressViewModel: $failure: $detail');
if (report) {
unawaited(
Sentry.captureMessage(
'OTA update failed ($failure, ${update.version}): $detail',
),
);
}
cancel();
_phase = UpdatePhase.failed;
_failure = failure;
notifyListeners();
}
void _runSimulation() {
const totalSteps = 40;
var step = 0;
_simTimer = Timer.periodic(const Duration(milliseconds: 150), (timer) {
step++;
if (simulateError && step == totalSteps ~/ 2) {
_phase = UpdatePhase.failed;
_failure = UpdateFailure.simulated;
timer.cancel();
notifyListeners();
return;
}
if (step < totalSteps) {
_progress = step / totalSteps;
notifyListeners();
return;
}
timer.cancel();
_progress = 1;
_phase = UpdatePhase.installing;
notifyListeners();
_simTimer = Timer(const Duration(seconds: 2), () {
_phase = UpdatePhase.waitingForInstaller;
notifyListeners();
});
});
}
@override
void dispose() {
cancel();
super.dispose();
}
}