summaryrefslogtreecommitdiff
path: root/player-android/lib/main.dart
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-21 18:35:26 +0300
committerPaul Buetow <paul@buetow.org>2026-05-21 18:35:26 +0300
commite58a12ee773da39d639ebb0e26fdb1b782941c56 (patch)
tree60dbf99bc2daf8f9c0b0b0709a7fa0fa106eb8f1 /player-android/lib/main.dart
parent627f44e105926e5ad4963336cdbfd9f9522bcdb1 (diff)
Wrap AudioPlayerScreen in audio_service for background playback (1b)
Creates PlayerAudioHandler (BaseAudioHandler + SeekHandler) that wraps just_audio's AudioPlayer and bridges it to the Android media session: background foreground-service playback, lock-screen / notification controls (play/pause/seek/skip ±15 s), audio focus, and Bluetooth headset events all handled by audio_service. Key design decisions: - Handler registered once via AudioService.init in main() and injected into ProviderScope via overrideWithValue (DIP: no global mutable var). - Progress-sync timer stays in the screen so PlayerApiClient is never imported by the handler (SRP boundary preserved). - Seek bar onChanged routes through handler.seek() so the notification position updates on slider drags (Law of Demeter fix). - _initPlayer refactored into _buildAuthHeaders / _loadSource / _resumeFromSavedPosition helpers (each ≤30 lines, SoC). - Tests override audioHandlerProvider with _FakePlayerAudioHandler to avoid platform-channel calls; all 221 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'player-android/lib/main.dart')
-rw-r--r--player-android/lib/main.dart56
1 files changed, 53 insertions, 3 deletions
diff --git a/player-android/lib/main.dart b/player-android/lib/main.dart
index 156b8f8..b32c8e7 100644
--- a/player-android/lib/main.dart
+++ b/player-android/lib/main.dart
@@ -1,11 +1,61 @@
+import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:just_audio/just_audio.dart';
+import 'providers/audio_handler_provider.dart';
import 'router.dart';
+import 'services/audio_handler.dart';
-/// Entry point — wraps the whole widget tree in a [ProviderScope] so every
-/// widget and provider has access to the Riverpod container.
-void main() => runApp(const ProviderScope(child: PlayerAndroidApp()));
+// ---------------------------------------------------------------------------
+// Entry point
+// ---------------------------------------------------------------------------
+
+/// Entry point.
+///
+/// [AudioService.init] must be called before [runApp] so that the handler is
+/// registered before any widget tries to obtain it via [audioHandlerProvider].
+///
+/// The handler is injected into [ProviderScope] via an override rather than
+/// stored in a global variable, keeping the dependency explicit and testable
+/// (Dependency Inversion Principle: the provider file does not need to import
+/// `main.dart`; the composition root wires the graph).
+void main() async {
+ // Ensure platform channels are ready before calling AudioService.init.
+ WidgetsFlutterBinding.ensureInitialized();
+
+ // Register the PlayerAudioHandler as the singleton media session handler.
+ // AudioService.init<T> returns the exact T from the builder. We pass it
+ // directly into ProviderScope so [audioHandlerProvider] is resolved without
+ // any global mutable state.
+ final handler = await AudioService.init<PlayerAudioHandler>(
+ builder: () => PlayerAudioHandler(AudioPlayer()),
+ config: const AudioServiceConfig(
+ // Notification channel name shown in Android Settings → App info.
+ androidNotificationChannelName: 'Player Audio',
+ // Keep the service alive while the notification is visible so the OS
+ // does not kill the process when the user swipes the app away.
+ androidStopForegroundOnPause: false,
+ // Allow the user to swipe away the notification to stop playback (UX
+ // expectation on Android — mirrors Spotify / Podcast Addict behaviour).
+ androidNotificationOngoing: false,
+ ),
+ );
+
+ runApp(
+ ProviderScope(
+ // Override the provider with the concrete handler instance so that every
+ // widget and provider that reads [audioHandlerProvider] gets the same
+ // singleton without going through a global variable.
+ overrides: [audioHandlerProvider.overrideWithValue(handler)],
+ child: const PlayerAndroidApp(),
+ ),
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Root widget
+// ---------------------------------------------------------------------------
/// Root application widget.
///