v2.55.3 — Code-Review-Fixes: foregroundServiceType (Android 14+), POST_NOTIFICATIONS-Check, FG-Service-Notification-Channel, try/catch _zeigeSyncNotification

CRITICAL:
- AndroidManifest: foregroundServiceType="dataSync" für BackgroundService (Android 14+ MissingForegroundServiceTypeException)
- main.dart: notificationChannelId + initialNotificationTitle + foregroundServiceTypes für FlutterBackgroundService (fehlende FG-Notification → ANR/crash nach 5s)

MEDIUM:
- sync_service.dart: POST_NOTIFICATIONS-Permission-Check vor _starteForegroundService (Android 13+)
- sync_service.dart: try/catch um _zeigeSyncNotification (plugin-dispose-Sicherheit)

flutter analyze: clean | flutter test: 194/194 passed
This commit is contained in:
Dustin
2026-08-06 18:17:25 +02:00
parent 7777297073
commit 85615d253c
3 changed files with 53 additions and 11 deletions
+7
View File
@@ -36,6 +36,13 @@
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<!-- v2.55.3: Foreground-Service-Type für Android 14+ (compileSdk 36).
Ohne dieses Attribut crasht der Service-Start mit
MissingForegroundServiceTypeException. -->
<service
android:name="id.flutter.flutter_background_service.BackgroundService"
android:foregroundServiceType="dataSync" />
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data
+21
View File
@@ -77,12 +77,33 @@ void main() async {
?.createNotificationChannel(syncChannel); ?.createNotificationChannel(syncChannel);
// ── Background Service initialisieren (Issue #3) ── // ── Background Service initialisieren (Issue #3) ──
// v2.55.3: Foreground-Service-Notification-Channel konfigurieren.
// Ohne notificationChannelId zeigt der Service eine Default-Notification
// ohne eigenen Channel — auf Android 14+ Pflicht.
const fgChannel = AndroidNotificationChannel(
'de.baka.melo.fg_service',
'Melo Hintergrunddienst',
description: 'Hält Melo im Hintergrund aktiv (Sync, Downloads)',
importance: Importance.low,
playSound: false,
enableVibration: false,
showBadge: false,
);
await notificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(fgChannel);
await FlutterBackgroundService().configure( await FlutterBackgroundService().configure(
iosConfiguration: IosConfiguration(), iosConfiguration: IosConfiguration(),
androidConfiguration: AndroidConfiguration( androidConfiguration: AndroidConfiguration(
onStart: _onServiceStart, onStart: _onServiceStart,
autoStart: false, autoStart: false,
isForegroundMode: true, isForegroundMode: true,
notificationChannelId: 'de.baka.melo.fg_service',
initialNotificationTitle: 'Melo Sync',
initialNotificationContent: 'Synchronisiere Musik…',
foregroundServiceTypes: [AndroidForegroundType.dataSync],
), ),
); );
+25 -11
View File
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_background_service/flutter_background_service.dart'; import 'package:flutter_background_service/flutter_background_service.dart';
import '../database/db_helper.dart'; import '../database/db_helper.dart';
import '../models/song.dart'; import '../models/song.dart';
@@ -577,18 +578,22 @@ class SyncService {
if (!Platform.isAndroid) return; if (!Platform.isAndroid) return;
final maxP = gesamt > 0 ? gesamt : 1; final maxP = gesamt > 0 ? gesamt : 1;
final p = aktuell > maxP ? maxP : aktuell; final p = aktuell > maxP ? maxP : aktuell;
notificationsPlugin.show( try {
id: _syncNotifyId, notificationsPlugin.show(
title: 'Synchronisiere…', id: _syncNotifyId,
body: gesamt > 0 ? '$aktuell / $gesamt Songs' : body, title: 'Synchronisiere…',
notificationDetails: NotificationDetails( body: gesamt > 0 ? '$aktuell / $gesamt Songs' : body,
android: syncNotificationDetails( notificationDetails: NotificationDetails(
ongoing: true, android: syncNotificationDetails(
progress: p, ongoing: true,
maxProgress: maxP, progress: p,
maxProgress: maxP,
),
), ),
), );
); } catch (_) {
// Plugin kann beim App-Exit bereits disposed sein
}
} }
/// Abschluss-Notification — tippbar (öffnet den Cloud-Tab, payload /// Abschluss-Notification — tippbar (öffnet den Cloud-Tab, payload
@@ -630,9 +635,18 @@ class SyncService {
/// Startet den Foreground-Service, damit der Sync auch bei Display-aus /// Startet den Foreground-Service, damit der Sync auch bei Display-aus
/// und minimierter App weiterläuft (Android-Prozess-Garantie). /// und minimierter App weiterläuft (Android-Prozess-Garantie).
/// v2.55.3: Prüft POST_NOTIFICATIONS-Permission (Android 13+).
/// Ohne diese Permission crasht der Foreground-Service innerhalb von
/// 5 Sekunden (Android killt Services ohne sichtbare Notification).
Future<void> _starteForegroundService() async { Future<void> _starteForegroundService() async {
if (!Platform.isAndroid) return; if (!Platform.isAndroid) return;
try { try {
// Android 13+: Benachrichtigungs-Berechtigung erforderlich
final status = await Permission.notification.status;
if (status.isDenied || status.isPermanentlyDenied) {
MeloLogger().aktion('foreground_sync_skip', {'grund': 'notification_denied'});
return;
}
final svc = FlutterBackgroundService(); final svc = FlutterBackgroundService();
final laeuft = await svc.isRunning(); final laeuft = await svc.isRunning();
if (!laeuft) { if (!laeuft) {