v2.55.2 — Manueller Sync: Status-Popup, Hintergrund-Weiterlauf, Sperrbildschirm-Notification

- cloud_screen.dart: Sync-Popup mit Live-Fortschritt („12/325 synchronisiert"),
  „Im Hintergrund fortsetzen"-Button, Ergebnis-Snackbar nach Abschluss
- sync_service.dart: Notification-Kanal auf HIGH + Visibility.public,
  Foreground-Service starten/stoppen während Sync (Display-aus-Garantie),
  statische syncNotificationDetails() für Tests
- main.dart: Sync-Channel-Importance auf HIGH (überschreibt alte low-Einstellung)
- Tests: +7 (syncNotificationDetails HIGH/public, Lifecycle-Unabhängigkeit)
This commit is contained in:
Dustin
2026-08-06 18:11:41 +02:00
parent b4d63509d1
commit 7777297073
4 changed files with 323 additions and 32 deletions
+78 -28
View File
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import '../database/db_helper.dart';
import '../models/song.dart';
import '../utils/sanitize.dart';
@@ -253,11 +254,19 @@ class SyncService {
/// NICHT interaktiv gelöst — die Server-Metadaten gewinnen still.
/// Rückgabe: true bei Erfolg, false bei Fehler oder wenn bereits ein
/// Sync läuft (globaler Guard).
///
/// v2.55.2: Startet den Foreground-Service (FlutterBackgroundService),
/// damit der Sync auch bei Display-aus und minimierter App weiterläuft.
Future<bool> syncAlles({bool automatisch = false}) async {
if (_laeuftGlobal) return false;
_laeuftGlobal = true;
laeuftNotifier.value = true;
String? konfliktBatch;
// v2.55.2: Foreground-Service starten — hält den Prozess im Hintergrund
// aktiv, auch wenn das Display aus ist oder die App minimiert wird.
await _starteForegroundService();
try {
_zeigeSyncNotification('Verbinde…', 0, 0);
onFortschritt?.call('Verbinde...', 0);
@@ -527,11 +536,41 @@ class SyncService {
} catch (_) {
// Plugin kann beim App-Exit bereits disposed sein
}
// v2.55.2: Foreground-Service stoppen, sobald der Sync beendet ist.
await _stoppeForegroundService();
onSyncEnde?.call();
}
}
// ─── Notifications ───
// ─── Notifications (v2.55.2: HIGH + public für Sperrbildschirm) ───
/// Erzeugt die [AndroidNotificationDetails] für Sync-Notifications.
/// Statisch → testbar ohne Platform-/Plugin-Zugriff.
/// [ongoing]=true für Fortschritts-Notification (nicht wegwischbar),
/// [ongoing]=false für Abschluss-/Fehler-Notification (automatisch gelöscht).
@visibleForTesting
static AndroidNotificationDetails syncNotificationDetails({
required bool ongoing,
int progress = 0,
int maxProgress = 0,
}) {
return AndroidNotificationDetails(
_syncChannelId,
'Melo Sync',
channelDescription: ongoing
? 'Cloud-Sync-Fortschritt'
: 'Cloud-Sync-Abschluss',
importance: Importance.high,
priority: Priority.high,
visibility: NotificationVisibility.public,
onlyAlertOnce: true,
showProgress: ongoing,
maxProgress: ongoing ? (maxProgress > 0 ? maxProgress : 1) : 0,
progress: ongoing ? progress : 0,
ongoing: ongoing,
autoCancel: !ongoing,
);
}
/// Persistente Fortschritts-Notification (nicht wegwischbar, ongoing).
void _zeigeSyncNotification(String body, int aktuell, int gesamt) {
@@ -543,18 +582,10 @@ class SyncService {
title: 'Synchronisiere…',
body: gesamt > 0 ? '$aktuell / $gesamt Songs' : body,
notificationDetails: NotificationDetails(
android: AndroidNotificationDetails(
_syncChannelId,
'Melo Sync',
channelDescription: 'Cloud-Sync-Fortschritt',
importance: Importance.low,
priority: Priority.low,
onlyAlertOnce: true,
showProgress: true,
maxProgress: maxP,
progress: p,
android: syncNotificationDetails(
ongoing: true,
autoCancel: false,
progress: p,
maxProgress: maxP,
),
),
);
@@ -573,14 +604,7 @@ class SyncService {
body: '$songs Songs, $favoriten Favoriten synchronisiert ✅',
payload: 'sync_fertig',
notificationDetails: NotificationDetails(
android: AndroidNotificationDetails(
_syncChannelId,
'Melo Sync',
channelDescription: 'Cloud-Sync-Abschluss',
importance: Importance.defaultImportance,
priority: Priority.defaultPriority,
autoCancel: true,
),
android: syncNotificationDetails(ongoing: false),
),
);
}
@@ -597,18 +621,44 @@ class SyncService {
body: 'Bitte erneut versuchen ❌',
payload: 'sync_fehler',
notificationDetails: NotificationDetails(
android: AndroidNotificationDetails(
_syncChannelId,
'Melo Sync',
channelDescription: 'Cloud-Sync-Abschluss',
importance: Importance.defaultImportance,
priority: Priority.defaultPriority,
autoCancel: true,
),
android: syncNotificationDetails(ongoing: false),
),
);
}
// ─── Foreground-Service (v2.55.2) ───
/// Startet den Foreground-Service, damit der Sync auch bei Display-aus
/// und minimierter App weiterläuft (Android-Prozess-Garantie).
Future<void> _starteForegroundService() async {
if (!Platform.isAndroid) return;
try {
final svc = FlutterBackgroundService();
final laeuft = await svc.isRunning();
if (!laeuft) {
await svc.startService();
MeloLogger().aktion('foreground_sync_start', {});
}
} catch (e) {
MeloLogger().fehler('foreground_sync_start', e);
}
}
/// Stoppt den Foreground-Service nach Sync-Ende.
Future<void> _stoppeForegroundService() async {
if (!Platform.isAndroid) return;
try {
final svc = FlutterBackgroundService();
final laeuft = await svc.isRunning();
if (laeuft) {
svc.invoke('stopService'); // Feuer-und-vergessen (Isolate-Nachricht)
MeloLogger().aktion('foreground_sync_stop', {});
}
} catch (e) {
MeloLogger().fehler('foreground_sync_stop', e);
}
}
String _formatZeit(DateTime? dt) {
if (dt == null) return 'Nie';
return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';