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
235 lines
8.4 KiB
Dart
235 lines
8.4 KiB
Dart
import 'dart:async';
|
||
import 'package:flutter/material.dart';
|
||
import 'dart:io' show Platform;
|
||
import 'package:audio_service/audio_service.dart';
|
||
import 'package:permission_handler/permission_handler.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 'services/favoriten_service.dart';
|
||
import 'services/auth_service.dart';
|
||
import 'services/cloud_service.dart';
|
||
import 'services/melo_logger.dart';
|
||
import 'services/audio_handler.dart';
|
||
import 'services/musik_scanner.dart';
|
||
import 'services/sync_service.dart';
|
||
import 'utils/farb_theme.dart';
|
||
import 'utils/user_effekte.dart';
|
||
import 'screens/home_screen.dart';
|
||
import 'screens/login_screen.dart';
|
||
|
||
/// Globaler Notification-Plugin (wird in main() initialisiert)
|
||
final FlutterLocalNotificationsPlugin notificationsPlugin =
|
||
FlutterLocalNotificationsPlugin();
|
||
|
||
/// Globaler Timer für periodischen Auto-Scan (wird in MeloAppState.dispose gecancelt)
|
||
Timer? _autoScanTimer;
|
||
|
||
void main() async {
|
||
WidgetsFlutterBinding.ensureInitialized();
|
||
|
||
// Logger startet sofort – zeichnet ALLES auf
|
||
MeloLogger().init('2.54.1');
|
||
|
||
// ── Notifications initialisieren (Issue #9) ──
|
||
const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||
const initSettings = InitializationSettings(android: androidInit);
|
||
await notificationsPlugin.initialize(
|
||
settings: initSettings,
|
||
// Tipp auf die Sync-Abschluss-/Fehler-Notification → Cloud-Tab öffnen
|
||
onDidReceiveNotificationResponse: (response) {
|
||
if (response.payload == 'sync_fertig' ||
|
||
response.payload == 'sync_fehler') {
|
||
SyncService.syncBenachrichtigungGetippt.value++;
|
||
}
|
||
},
|
||
);
|
||
|
||
// Notification-Channel für Downloads (Fortschritt + Abschluss)
|
||
const downloadChannel = AndroidNotificationChannel(
|
||
'de.baka.melo.downloads',
|
||
'Melo Downloads',
|
||
description: 'Download-Fortschritt und Abschluss',
|
||
importance: Importance.low,
|
||
playSound: false,
|
||
enableVibration: false,
|
||
);
|
||
await notificationsPlugin
|
||
.resolvePlatformSpecificImplementation<
|
||
AndroidFlutterLocalNotificationsPlugin>()
|
||
?.createNotificationChannel(downloadChannel);
|
||
|
||
// Notification-Channel für den Cloud-Sync (Fortschritt + Abschluss)
|
||
// v2.55.2: Importance HIGH + Visibility public für Sperrbildschirm + Shadow.
|
||
// createNotificationChannel überschreibt bestehende Channel-Settings —
|
||
// ein alter Kanal mit low-Importance wird beim nächsten App-Start korrigiert.
|
||
const syncChannel = AndroidNotificationChannel(
|
||
'de.baka.melo.sync',
|
||
'Melo Sync',
|
||
description: 'Cloud-Sync-Fortschritt und Abschluss',
|
||
importance: Importance.high,
|
||
playSound: false,
|
||
enableVibration: false,
|
||
);
|
||
await notificationsPlugin
|
||
.resolvePlatformSpecificImplementation<
|
||
AndroidFlutterLocalNotificationsPlugin>()
|
||
?.createNotificationChannel(syncChannel);
|
||
|
||
// ── 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(
|
||
iosConfiguration: IosConfiguration(),
|
||
androidConfiguration: AndroidConfiguration(
|
||
onStart: _onServiceStart,
|
||
autoStart: false,
|
||
isForegroundMode: true,
|
||
notificationChannelId: 'de.baka.melo.fg_service',
|
||
initialNotificationTitle: 'Melo Sync',
|
||
initialNotificationContent: 'Synchronisiere Musik…',
|
||
foregroundServiceTypes: [AndroidForegroundType.dataSync],
|
||
),
|
||
);
|
||
|
||
try {
|
||
await DbHelper().db;
|
||
await FavoritenService().init();
|
||
// Hintergrund-Audio-Service starten
|
||
await AudioService.init(
|
||
builder: () => MeloAudioHandler(),
|
||
config: const AudioServiceConfig(
|
||
androidNotificationChannelId: 'de.baka.melo.audio',
|
||
androidNotificationChannelName: 'Melo Wiedergabe',
|
||
androidStopForegroundOnPause: false,
|
||
),
|
||
);
|
||
MeloLogger().zustand('start_ok', {'db': 'ok', 'audio': 'ok'});
|
||
} catch (e, stack) {
|
||
MeloLogger().fehler('App-Start', e, stack);
|
||
}
|
||
|
||
// Auth initialisieren (Token aus SharedPreferences laden + online validieren)
|
||
await AuthService().initialisieren();
|
||
|
||
// Favoriten-Toggle → Server-Push (Feuer-und-Vergessen, deterministisch via
|
||
// `set` statt Toggle — verhindert Doppel-Toggle). Songs ohne cloud_id
|
||
// (nicht hochgeladen) werden übersprungen; ein Fehlschlag wird beim
|
||
// nächsten vollständigen Sync korrigiert (favoritenMerge-Vereinigung).
|
||
FavoritenService().onStatusGeaendert = (songId, neuerStatus) async {
|
||
try {
|
||
final song = await DbHelper().songNachId(songId);
|
||
final cid = song?.cloudId;
|
||
if (cid == null || cid.isEmpty) return;
|
||
await CloudService().setFavorite(cid, favorit: neuerStatus);
|
||
} catch (e) {
|
||
MeloLogger().fehler('favoriten_server_push', e);
|
||
}
|
||
};
|
||
|
||
// Android-Berechtigungen beim Start abfragen (Issue #4)
|
||
// Speicherzugriff für Musik-Scan (READ_MEDIA_AUDIO / Storage)
|
||
await MusikScanner().frageSpeicherZugriff();
|
||
|
||
// Auto-Scan: Musikordner beim Start scannen + periodisch alle 3h (Issue #6)
|
||
_starteAutoScan();
|
||
|
||
// POST_NOTIFICATIONS für Android 13+ (Notifications für Downloads, Player)
|
||
if (Platform.isAndroid) {
|
||
await Permission.notification.request();
|
||
}
|
||
|
||
// Per-User-Akzent (überschreibbar in den Einstellungen) + Begrüßungs-Sound
|
||
// beim App-Start (wenn Token vorhanden). Fehler sind unkritisch.
|
||
await UserEffekt.anwenden(AuthService().benutzer);
|
||
|
||
// App-weiter Auto-Sync-Timer (F3): Intervall aus `cloud_interval`
|
||
// (0 = Aus), erster Tick erst NACH dem Intervall — kein Sync beim Start.
|
||
unawaited(SyncService.starteAutoSyncTimer());
|
||
|
||
runApp(const MeloApp());
|
||
}
|
||
|
||
/// Führt Musik-Scan beim Start aus + startet Timer.periodic alle 3 Stunden (Issue #6)
|
||
/// Scannt nur /Music, /Download, /Musik, /Downloads — nicht ganz /storage.
|
||
void _starteAutoScan() {
|
||
// Sofort beim Start scannen (asynchron, blockiert nicht)
|
||
MusikScanner().scanneMusikOrdner().then((songs) {
|
||
MeloLogger().zustand('auto_scan_start', {'gefunden': songs.length});
|
||
}).catchError((e) {
|
||
MeloLogger().fehler('auto_scan_start', e);
|
||
});
|
||
|
||
// Periodischer Scan alle 3 Stunden – Timer wird in MeloAppState.dispose() gecancelt
|
||
_autoScanTimer = Timer.periodic(const Duration(hours: 3), (_) {
|
||
MusikScanner().scanneMusikOrdner().then((songs) {
|
||
MeloLogger().zustand('auto_scan_periodisch', {'gefunden': songs.length});
|
||
}).catchError((e) {
|
||
MeloLogger().fehler('auto_scan_periodisch', e);
|
||
});
|
||
});
|
||
}
|
||
|
||
/// Background Service Callback – hält App im Vordergrund beim Download (Issue #3)
|
||
@pragma('vm:entry-point')
|
||
Future<bool> _onServiceStart(ServiceInstance service) async {
|
||
WidgetsFlutterBinding.ensureInitialized();
|
||
// Service läuft als Foreground-Service und hält den Prozess am Leben.
|
||
// Der eigentliche Download läuft im Haupt-Isolate – der Service dient nur
|
||
// als WakeLock / Foreground-Garantie.
|
||
service.on('stopService').listen((_) {
|
||
service.stopSelf();
|
||
});
|
||
return true;
|
||
}
|
||
|
||
class MeloApp extends StatefulWidget {
|
||
const MeloApp({super.key});
|
||
|
||
@override
|
||
State<MeloApp> createState() => _MeloAppState();
|
||
}
|
||
|
||
class _MeloAppState extends State<MeloApp> {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final auth = AuthService();
|
||
|
||
// ValueListenableBuilder: Akzentfarbe (per User, in Einstellungen wählbar)
|
||
// wechselt das komplette Theme live — kein App-Neustart nötig.
|
||
return ValueListenableBuilder<Color>(
|
||
valueListenable: MeloTheme.akzentNotifier,
|
||
builder: (_, akzent, __) => MaterialApp(
|
||
title: 'Melo',
|
||
debugShowCheckedModeBanner: false,
|
||
theme: MeloTheme.theme,
|
||
home: auth.istEingeloggt
|
||
? const Scaffold(body: MeloHome())
|
||
: const LoginScreen(),
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_autoScanTimer?.cancel();
|
||
_autoScanTimer = null;
|
||
super.dispose();
|
||
}
|
||
}
|