feat: In-App-Update (Download + Installer öffnen)
Portiert von timemaster-mobile v0.1.7+8: UpdateCheckService prüft kiosk-version.txt, LoginScreen zeigt Update-Banner mit Fortschrittsbalken + "Jetzt aktualisieren"-Button, öffnet Android-Paketinstaller direkt (open_filex + FileProvider). Banner sitzt im LoginScreen statt StampScreen, da das der Idle-/Ruhezustand des Kiosks ist – Stempel-Sessions sind nur 900s aktiv. REQUEST_INSTALL_PACKAGES-Permission ergänzt. v0.1.5+6. Diese Version selbst muss noch einmal manuell installiert werden (Feature existiert ja erst danach), ab hier sollte der Update-Button reichen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
This commit is contained in:
1 parent
be36786913
commit
6f30e11e4e
6 files changed
+155
-2
No files matched your search
@@ -5,6 +5,7 @@
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<application
|
||||
android:label="timemaster_kiosk"
|
||||
android:name="${applicationName}"
|
||||
@@ -36,6 +37,18 @@
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
<!-- FileProvider für open_filex – Android 7+ verlangt content://
|
||||
statt file:// beim Öffnen heruntergeladener APKs. -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Deckt getExternalFilesDir()/... ab, wo wir das APK zwischenspeichern -->
|
||||
<external-files-path name="downloads" path="." />
|
||||
<cache-path name="cache" path="." />
|
||||
</paths>
|
||||
@@ -1,3 +1,3 @@
|
||||
/// Manuell mit pubspec.yaml synchron halten (siehe timemaster-mobile,
|
||||
/// gleiche Konvention – kein package_info_plus während aktiver Feldtests).
|
||||
const String kAppVersion = '0.1.4+5';
|
||||
const String kAppVersion = '0.1.5+6';
|
||||
@@ -7,6 +7,7 @@ import '../app_version.dart';
|
||||
import '../models/kiosk_session.dart';
|
||||
import '../services/api_client.dart';
|
||||
import '../services/kiosk_service.dart';
|
||||
import '../services/update_check_service.dart';
|
||||
import 'enrollment_screen.dart';
|
||||
import 'stamp_screen.dart';
|
||||
|
||||
@@ -32,12 +33,49 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
String? _scannedUid;
|
||||
List<KioskEmployee>? _employees;
|
||||
|
||||
final _updateService = UpdateCheckService();
|
||||
String? _newerVersion;
|
||||
bool _updating = false;
|
||||
double? _updateProgress;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateService.checkForNewerVersion().then((v) {
|
||||
if (mounted && v != null) setState(() => _newerVersion = v);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_nfcScanning) NfcManager.instance.stopSession();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _dioMessage(DioException e) {
|
||||
final detail = e.response?.data is Map ? e.response?.data['detail'] : null;
|
||||
if (detail is String) return detail;
|
||||
@@ -223,6 +261,44 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_newerVersion != null) ...[
|
||||
Card(
|
||||
color: Colors.blue[50],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
SegmentedButton<_LoginMode>(
|
||||
segments: const [
|
||||
ButtonSegment(value: _LoginMode.pin, label: Text('PIN'), icon: Icon(Icons.pin)),
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
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/kiosk-version.txt` auf dem konfigurierten Server
|
||||
/// gegen die eigene kAppVersion und kann das neue APK direkt herunterladen +
|
||||
/// den System-Installer öffnen (REQUEST_INSTALL_PACKAGES + FileProvider,
|
||||
/// siehe AndroidManifest.xml). Analog timemaster-mobile, eigene Dateinamen
|
||||
/// (kiosk-version.txt / timemaster-kiosk.apk – geteiltes Downloads-
|
||||
/// Verzeichnis mit der Employee-App auf dem Server, andere Dateien).
|
||||
class UpdateCheckService {
|
||||
String get _origin {
|
||||
final apiBase = ApiClient.instance.baseUrl; // endet auf .../api/v1
|
||||
return apiBase.replaceFirst(RegExp(r'/api/v1/?$'), '');
|
||||
}
|
||||
|
||||
Future<String?> checkForNewerVersion() async {
|
||||
try {
|
||||
final response = await Dio(BaseOptions(
|
||||
baseUrl: _origin,
|
||||
connectTimeout: const Duration(seconds: 5),
|
||||
receiveTimeout: const Duration(seconds: 5),
|
||||
)).get<String>('/static/downloads/kiosk-version.txt');
|
||||
|
||||
final remote = (response.data ?? '').trim();
|
||||
if (remote.isEmpty || remote == kAppVersion) return null;
|
||||
return remote;
|
||||
} on DioException {
|
||||
return null;
|
||||
} catch (_) {
|
||||
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<void> downloadAndInstall({void Function(double)? onProgress}) async {
|
||||
final dir = await getExternalStorageDirectory() ?? await getTemporaryDirectory();
|
||||
final filePath = '${dir.path}/timemaster-kiosk-update.apk';
|
||||
|
||||
await Dio(BaseOptions(baseUrl: _origin)).download(
|
||||
'/static/downloads/timemaster-kiosk.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}');
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -1,7 +1,7 @@
|
||||
name: timemaster_kiosk
|
||||
description: TimeMaster Kiosk – Stempeluhr für Android-Tablets hinter VPN
|
||||
publish_to: 'none'
|
||||
version: 0.1.4+5
|
||||
version: 0.1.5+6
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
@@ -15,6 +15,8 @@ dependencies:
|
||||
nfc_manager: ^4.2.1
|
||||
wakelock_plus: ^1.2.8
|
||||
mobile_scanner: ^6.0.2
|
||||
open_filex: ^4.5.0
|
||||
path_provider: ^2.1.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in new issue
Block a user