import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:sync_relay_core/core.dart'; import 'updater.dart'; // In-app update flow behind the Home update indicator. Same code path as // `--upgrade` (downloadAndApply); this adds state for the UI and the // restart. Check failures are logged only — the banner shows up just when // there is something to install. final _log = createLogger('update'); enum UpdatePhase { idle, available, downloading, applying, restarting, failed } class UpdateStatus { final UpdatePhase phase; final UpdateManifest? manifest; final double? progress; final String? error; // False for dev runs / unpacked builds: banner tells the user to update // by hand instead of offering the button. final bool canInstall; const UpdateStatus(this.phase, {this.manifest, this.progress, this.error, this.canInstall = false}); } typedef RestartFn = Future Function(String executable, List args); // Release Hive's box locks before the new instance opens the same boxes. Future _closeAndRestart(String executable, List args) async { try { await Hive.close(); } catch (e) { _log.warning('closing hive before restart: $e'); } await restartApp(executable, args); } class UpdateController { static final instance = UpdateController(); final String currentVersion; final String manifestUrl; final String? executablePath; final Directory? installRoot; final RestartFn restart; final bool skipDevBuilds; UpdateController( {this.currentVersion = appVersion, this.manifestUrl = updateManifestUrl, this.executablePath, this.installRoot, this.restart = _closeAndRestart, this.skipDevBuilds = true}); final status = ValueNotifier(const UpdateStatus(UpdatePhase.idle)); /// Outcome of the last finished check, for Settings/About wording /// ("Up to date", "Check failed: ..."); null until one finishes. final lastCheck = ValueNotifier(null); Future? _check; late final String _exe = executablePath ?? currentExecutablePath(); late final Directory? _root = installRoot ?? findInstallRoot(_exe); String get _exeName => _exe.replaceAll('\\', '/').split('/').last; // Once per process; later calls return the same future. Future checkOnce() => _check ??= _checkNow(); // User-triggered re-check ("Check now"); joins one already in flight. Future checkNow() { final inFlight = _check; if (inFlight != null && _checking) return inFlight; return _check = _checkNow(); } bool _checking = false; Future _checkNow() async { _checking = true; try { await _runCheck(); } finally { _checking = false; } } Future _runCheck() async { if (skipDevBuilds && currentVersion.endsWith('-dev')) { _log.info('dev build ($currentVersion), skipping update check'); lastCheck.value = 'Dev build — update checks are off'; return; } final root = _root; if (root != null) await cleanupUpdateLeftovers(root); try { final m = await fetchManifest(url: manifestUrl); if (!isNewerVersion(currentVersion, m.version)) { _log.info('up to date ($currentVersion, latest ${m.version})'); lastCheck.value = 'Up to date'; return; } _log.info('update available: ${m.version} (proto ${m.protocol})'); status.value = UpdateStatus(UpdatePhase.available, manifest: m, canInstall: root != null); lastCheck.value = 'Update v${m.version} available'; } catch (e) { _log.warning('update check failed: $e'); lastCheck.value = 'Update check failed: $e'; } } void dismiss() { final s = status.value; if ((s.phase == UpdatePhase.available || s.phase == UpdatePhase.failed) && !(s.manifest?.isRequired ?? false)) { status.value = const UpdateStatus(UpdatePhase.idle); } } Future install() async { final s = status.value; final m = s.manifest; final root = _root; if (m == null || root == null) return; if (s.phase != UpdatePhase.available && s.phase != UpdatePhase.failed) { return; } status.value = UpdateStatus(UpdatePhase.downloading, manifest: m, progress: 0, canInstall: true); var lastPct = -1; try { await downloadAndApply(m, root, executableName: installRoot == null ? _exeName : null, onProgress: (got, total) { if (total == null || total == 0) return; final pct = got * 100 ~/ total; if (pct == lastPct) return; lastPct = pct; status.value = UpdateStatus(UpdatePhase.downloading, manifest: m, progress: got / total, canInstall: true); if (pct == 100) { status.value = UpdateStatus(UpdatePhase.applying, manifest: m, canInstall: true); } }, say: (line) => _log.info(line)); } catch (e) { _log.warning('update failed: $e'); status.value = UpdateStatus(UpdatePhase.failed, manifest: m, error: '$e', canInstall: true); return; } _log.info('updated to ${m.version}, restarting'); status.value = UpdateStatus(UpdatePhase.restarting, manifest: m, canInstall: true); await restart(_exe, launchArgs); } }