diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index bb2b619..c7a8161 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + + + + + + + + + diff --git a/lib/app_version.dart b/lib/app_version.dart index 4ce0727..975b6cb 100644 --- a/lib/app_version.dart +++ b/lib/app_version.dart @@ -1,4 +1,4 @@ /// Manuell mit `pubspec.yaml` synchron halten (kein package_info_plus, um /// keine zusätzliche Native-Plugin-Fläche in dieser Debugging-Phase /// einzuführen). Bei jedem Release beide Stellen bumpen. -const String kAppVersion = '0.1.6+7'; +const String kAppVersion = '0.1.7+8'; diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 28980ce..a51030c 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -26,6 +26,9 @@ class _HomeScreenState extends State { String? _error; Timer? _ticker; String? _newerVersion; + final _updateService = UpdateCheckService(); + bool _updating = false; + double? _updateProgress; @override void initState() { @@ -34,11 +37,37 @@ class _HomeScreenState extends State { _ticker = Timer.periodic(const Duration(seconds: 1), (_) { if (_openEntry != null) setState(() {}); }); - UpdateCheckService().checkForNewerVersion().then((v) { + _updateService.checkForNewerVersion().then((v) { if (mounted && v != null) setState(() => _newerVersion = v); }); } + Future _installUpdate() async { + setState(() { + _updating = true; + _updateProgress = null; + _error = null; + }); + try { + await _updateService.downloadAndInstall( + onProgress: (p) { + if (mounted) setState(() => _updateProgress = p); + }, + ); + } catch (e) { + if (mounted) { + setState(() => _error = 'Update fehlgeschlagen: $e'); + } + } finally { + if (mounted) { + setState(() { + _updating = false; + _updateProgress = null; + }); + } + } + } + @override void dispose() { _ticker?.cancel(); @@ -185,17 +214,33 @@ class _HomeScreenState extends State { color: Colors.blue[50], child: Padding( padding: const EdgeInsets.all(12), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Icon(Icons.system_update, color: Colors.blue), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Neue Version $_newerVersion verfügbar (installiert: $kAppVersion). ' - 'Im Portal unter Profil herunterladen.', - style: const TextStyle(color: Colors.blue), - ), + Row( + children: [ + const Icon(Icons.system_update, color: Colors.blue), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Neue Version $_newerVersion verfügbar (installiert: $kAppVersion).', + style: const TextStyle(color: Colors.blue), + ), + ), + ], ), + const SizedBox(height: 8), + if (_updating) + LinearProgressIndicator(value: _updateProgress) + else + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: _installUpdate, + icon: const Icon(Icons.download), + label: const Text('Jetzt aktualisieren'), + ), + ), ], ), ), diff --git a/lib/services/update_check_service.dart b/lib/services/update_check_service.dart index 77f869c..2965be9 100644 --- a/lib/services/update_check_service.dart +++ b/lib/services/update_check_service.dart @@ -1,18 +1,24 @@ import 'package:dio/dio.dart'; +import 'package:open_filex/open_filex.dart'; +import 'package:path_provider/path_provider.dart'; import '../app_version.dart'; import 'api_client.dart'; /// Prüft `/static/downloads/version.txt` auf dem konfigurierten Server gegen -/// die eigene kAppVersion. Rein informativ – kein Erzwingen, kein Auto-Update -/// (APK-Sideloading, kein Play Store). Fehler beim Check sind egal (offline, -/// Server down) – dann einfach kein Hinweis. +/// die eigene kAppVersion und kann das neue APK direkt herunterladen + +/// den System-Installer öffnen (REQUEST_INSTALL_PACKAGES + FileProvider, +/// siehe AndroidManifest.xml). Rein informativ/manuell – kein automatischer +/// Silent-Install (kein Play Store, App ist nicht Geräteeigentümer/MDM). class UpdateCheckService { + String get _origin { + final apiBase = ApiClient.instance.baseUrl; // endet auf .../api/v1 + return apiBase.replaceFirst(RegExp(r'/api/v1/?$'), ''); + } + Future checkForNewerVersion() async { try { - final apiBase = ApiClient.instance.baseUrl; // endet auf .../api/v1 - final origin = apiBase.replaceFirst(RegExp(r'/api/v1/?$'), ''); final response = await Dio(BaseOptions( - baseUrl: origin, + baseUrl: _origin, connectTimeout: const Duration(seconds: 5), receiveTimeout: const Duration(seconds: 5), )).get('/static/downloads/version.txt'); @@ -26,4 +32,24 @@ class UpdateCheckService { return null; } } + + /// Lädt die APK herunter und öffnet den Android-Paketinstaller. + /// [onProgress] liefert 0.0–1.0, falls die Antwort ein Content-Length hat. + Future downloadAndInstall({void Function(double)? onProgress}) async { + final dir = await getExternalStorageDirectory() ?? await getTemporaryDirectory(); + final filePath = '${dir.path}/timemaster-update.apk'; + + await Dio(BaseOptions(baseUrl: _origin)).download( + '/static/downloads/timemaster.apk', + filePath, + onReceiveProgress: (received, total) { + if (total > 0 && onProgress != null) onProgress(received / total); + }, + ); + + final result = await OpenFilex.open(filePath, type: 'application/vnd.android.package-archive'); + if (result.type != ResultType.done) { + throw Exception('Installer konnte nicht geöffnet werden: ${result.message}'); + } + } } diff --git a/pubspec.yaml b/pubspec.yaml index 80586e4..1e4828e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: timemaster_mobile description: TimeMaster Zeiterfassung – Employee App publish_to: 'none' -version: 0.1.6+7 +version: 0.1.7+8 environment: sdk: '>=3.3.0 <4.0.0' @@ -16,6 +16,7 @@ dependencies: path_provider: ^2.1.4 flutter_secure_storage: ^9.2.2 local_auth: ^2.3.0 + open_filex: ^4.5.0 provider: ^6.1.2 intl: ^0.19.0