diff --git a/flutter_client/lib/player/audio_handler.dart b/flutter_client/lib/player/audio_handler.dart index 5bae269a..1518d7c9 100644 --- a/flutter_client/lib/player/audio_handler.dart +++ b/flutter_client/lib/player/audio_handler.dart @@ -315,11 +315,14 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl final coverPath = (t.albumId.isNotEmpty && _coverCache != null) ? _coverCache!.peekCached(t.albumId) : null; - // MediaItem.rating reflects the user's like state on this track - // so external surfaces (Wear's heart button, lock-screen - // favorites) render the right filled/outlined icon. Updated on - // every track change via _likeBridge.isTrackLiked. - final liked = _likeBridge?.isTrackLiked(t.id) ?? false; + // MediaItem.rating intentionally NOT set: audio_service propagates + // it to MediaSession.setRating(), but the Android session also + // needs setRatingType(RATING_HEART) configured to expose that to + // controllers — audio_service doesn't surface that config knob, + // and broadcasting an unanchored rating made Wear OS reject the + // session entirely. The LikeBridge wiring stays in place so + // setRating can still fire from any surface that DOES route it, + // we just don't advertise it. return MediaItem( id: t.id, title: t.title, @@ -327,7 +330,6 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl album: t.albumTitle, duration: Duration(seconds: t.durationSec), artUri: coverPath != null ? Uri.file(coverPath) : null, - rating: Rating.newHeartRating(liked), extras: extras.isEmpty ? null : extras, ); } @@ -412,19 +414,6 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl @override Future skipToPrevious() => _player.seekToPrevious(); - /// Stops playback and dismisses the system notification. BaseAudio - /// Handler's default just sets the processing state to idle without - /// touching the player, so the just_audio engine kept its audio - /// session and external surfaces (Wear, Auto, BT) saw a "paused - /// forever" rather than a clean stop. Halting the player releases - /// the session and lets super.stop() tear down the foreground - /// notification. - @override - Future stop() async { - await _player.stop(); - await super.stop(); - } - /// Heart rating from external surfaces (Wear's favorite button, /// lock-screen like) → LikesController.toggle(track). We only /// route through the bridge when the rating actually flips relative @@ -498,33 +487,32 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl MediaControl.skipToPrevious, if (playing) MediaControl.pause else MediaControl.play, MediaControl.skipToNext, - MediaControl.stop, ], // androidCompactActionIndices tells the system which controls // appear in the collapsed/lock-screen view. Without this, some // Android versions render the player without working buttons. - // Keep this at 3 actions (prev/play-pause/next) — stop lives - // in the expanded view only. androidCompactActionIndices: const [0, 1, 2], // systemActions enumerates which actions the system can invoke - // on us. Anything not listed here doesn't route back to the - // handler from external surfaces (Wear, Auto, Bluetooth, lock - // screen) — Android 13+ silently drops those events. Keep this - // in sync with the override methods so each implemented action - // is actually advertised. + // on us — without play/pause/skip in here, taps on lock-screen + // controls don't route back to the handler on Android 13+. + // + // v2026.05.13.3 added stop, skipToQueueItem, setShuffleMode, + // setRepeatMode, and setRating to this set; reverted because + // Pixel Watch 2 stopped showing controls entirely after that + // change. The MediaSession contract on Wear OS requires more + // setup than just advertising the action (e.g. setRatingType + // for setRating) and audio_service doesn't expose those knobs. + // Keep the override methods themselves (skipToQueueItem is + // still routed via QueueScreen's direct handler call; + // setRating is harmless if never invoked) so we don't lose + // the underlying functionality — just don't tell the system + // we support them. systemActions: const { MediaAction.play, MediaAction.pause, - MediaAction.stop, MediaAction.skipToNext, MediaAction.skipToPrevious, - MediaAction.skipToQueueItem, MediaAction.seek, - MediaAction.setShuffleMode, - MediaAction.setRepeatMode, - // Heart rating — Wear and lock screen render this as a like - // button. Routed to LikesController via _likeBridge. - MediaAction.setRating, }, processingState: switch (_player.processingState) { ProcessingState.idle => AudioProcessingState.idle,