revert(player): MediaSession surface back to 5 advertised actions

Pixel Watch 2 stopped showing controls entirely after v2026.05.13.3's
MediaSession expansion. Reverting the additive pieces:

* systemActions back to the original 5 (play / pause / skipPrev /
  skipNext / seek). stop, skipToQueueItem, setShuffleMode,
  setRepeatMode, setRating removed.
* controls list back to skipPrev / play|pause / skipNext (no stop).
* stop() override removed — let BaseAudioHandler default apply
  (probably needs to be a no-op for the MediaSession to stay alive
  through certain lifecycle events that audio_service triggers
  internally; the override was actually halting the session).
* MediaItem.rating no longer set in _toMediaItem. The Android
  MediaSession.setRating() path requires setRatingType(RATING_HEART)
  to actually expose to controllers, and audio_service doesn't
  surface that config knob — broadcasting an unanchored rating
  appears to make Wear OS reject the session entirely.

Kept in place:
* skipToQueueItem override — still needed for QueueScreen's direct
  handler call (not routed through MediaSession actions).
* setRating override + LikeBridge wiring — harmless if never
  invoked, and lights up automatically if we figure out how to
  configure the rating type later.
* AlbumCoverCache.peekCached for sync artUri seed — that part
  worked, and the failure mode would be a missing cover, not a
  rejected session.

Watch should come back to its previous "sometimes works" state from
v2026.05.13.2 (basic controls only). Getting past that needs proper
MediaSession config that audio_service either doesn't expose or
requires platform-channel work.
This commit is contained in:
2026-05-14 15:44:14 -04:00
parent e38189470b
commit e59ccba961
+22 -34
View File
@@ -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<void> 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<void> 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,