import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; import 'installer.dart'; /// 强制更新页:检测到新版本时的全屏阻塞页。 /// PopScope 禁返回(Android 系统返回 / iOS 边缘滑动均不可退出)。 /// 点「立即更新」在 App 内流式下载 APK(显示下载进度)→ PackageInstaller /// 会话安装(显示安装进度),不再跳浏览器。 /// 点击更新时回调 onUpdateAccepted(调用方持久化服务器版本号, /// 使 APK 版本号不递增时也不反复提示)。 class UpdateScreen extends StatefulWidget { final String version; final String url; final String notes; final VoidCallback? onUpdateAccepted; const UpdateScreen({ super.key, required this.version, required this.url, this.notes = '', this.onUpdateAccepted, }); @override State createState() => _UpdateScreenState(); } enum _Stage { idle, downloading, installing, finished, failed } class _UpdateScreenState extends State { final http.Client _client = http.Client(); final File _apkFile = File('${Directory.systemTemp.path}/observer-latest.apk'); final File _apkPart = File('${Directory.systemTemp.path}/observer-latest.apk.part'); _Stage _stage = _Stage.idle; /// 进度百分比 0-100;null = 总量未知(不确定进度条) double? _progress; String? _message; StreamSubscription? _installSub; @override void dispose() { _installSub?.cancel(); _client.close(); super.dispose(); } Future _launch() async { if (_stage == _Stage.downloading || _stage == _Stage.installing || _stage == _Stage.finished) { return; } setState(() { _stage = _Stage.idle; _message = null; }); widget.onUpdateAccepted?.call(); if (!Platform.isAndroid) { // 更新检查本就仅 Android 触发,这里兜底非 Android 走浏览器 final uri = Uri.tryParse(widget.url); if (uri == null) return; try { await launchUrl(uri, mode: LaunchMode.externalApplication); } catch (_) {} return; } // APK 已下载完成(下载是原子落盘,.part 改名后文件才存在)→ 跳过下载直接安装 if (_apkFile.existsSync()) { await _install(); return; } await _download(); } Future _download() async { setState(() { _stage = _Stage.downloading; _progress = 0; }); try { if (_apkPart.existsSync()) _apkPart.deleteSync(); final res = await _client.send(http.Request('GET', Uri.parse(widget.url))); if (res.statusCode != 200) { throw HttpException('HTTP ${res.statusCode}'); } final total = res.contentLength ?? -1; final sink = _apkPart.openWrite(); var received = 0; await for (final chunk in res.stream) { sink.add(chunk); received += chunk.length; if (mounted && total > 0) { setState(() => _progress = received / total * 100); } } await sink.close(); // 原子落盘:下载完成后才重命名为正式文件,避免残留半包被当成完整 APK _apkPart.renameSync(_apkFile.path); if (!mounted) return; await _install(); } catch (_) { if (_apkPart.existsSync()) _apkPart.deleteSync(); if (!mounted) return; setState(() { _stage = _Stage.failed; _progress = null; _message = '下载失败,请检查网络后重试'; }); } } Future _install() async { setState(() { _stage = _Stage.installing; _progress = 0; _message = null; }); await _installSub?.cancel(); _installSub = ApkInstaller.progress().listen((e) { if (!mounted) return; switch (e.event) { case 'progress': setState(() => _progress = e.progress.toDouble()); break; case 'finished': final ok = e.success == true; setState(() { _stage = ok ? _Stage.finished : _Stage.failed; _message = ok ? '安装完成,请从桌面打开新版应用' : '安装失败,请重试'; }); break; case 'failed': setState(() { _stage = _Stage.failed; _message = e.error ?? '安装失败,请重试'; }); break; } }, onError: (Object _) { if (!mounted) return; setState(() { _stage = _Stage.failed; _message = '安装失败,请重试'; }); }); final result = await ApkInstaller.install(_apkFile.path); if (!mounted) return; if (result == 'permission_required') { // 原生侧已拉起系统设置页;APK 已缓存,用户开启后返回再点直达安装 setState(() { _stage = _Stage.failed; _message = '请在系统设置中允许「安装未知应用」,返回后再次点击「立即更新」(APK 已缓存,无需重新下载)'; }); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final busy = _stage == _Stage.downloading || _stage == _Stage.installing; final progressText = _stage == _Stage.downloading ? (_progress == null ? '下载中…' : '下载中 ${_progress!.round()}%') : (_progress == null ? '安装中…' : '安装中 ${_progress!.round()}%'); return PopScope( canPop: false, child: Scaffold( body: Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.system_update_alt, size: 64, color: theme.colorScheme.primary), const SizedBox(height: 16), Text('发现新版本 ${widget.version}', style: theme.textTheme.headlineSmall), const SizedBox(height: 12), if (widget.notes.isNotEmpty) Text(widget.notes, textAlign: TextAlign.center, style: const TextStyle(height: 1.6)), const SizedBox(height: 24), if (busy) ...[ LinearProgressIndicator( value: _progress == null ? null : _progress! / 100, minHeight: 6, ), const SizedBox(height: 12), Text(progressText), const SizedBox(height: 24), ], FilledButton.icon( onPressed: busy || _stage == _Stage.finished ? null : _launch, icon: const Icon(Icons.download), label: Text(_stage == _Stage.finished ? '已完成' : '立即更新'), style: FilledButton.styleFrom( minimumSize: const Size(200, 48), textStyle: const TextStyle(fontSize: 16), ), ), if (_message != null) ...[ const SizedBox(height: 12), Text( _message!, textAlign: TextAlign.center, style: TextStyle( height: 1.5, color: _stage == _Stage.failed ? theme.colorScheme.error : Colors.green, ), ), ], const SizedBox(height: 12), Text('不更新将无法继续使用', style: theme.textTheme.bodySmall ?.copyWith(color: theme.colorScheme.error)), ], ), ), ), ), ); } }