unify media sessions.

Android doesn't like having 2 media sessions sticking around, hence we move the button
handling code into MediaSessionTracker on lollipop.
This commit is contained in:
Adrian Ulrich 2021-03-03 19:35:02 +01:00
parent 1a13ca753a
commit 0f5a229df3
4 changed files with 73 additions and 152 deletions

View File

@ -38,6 +38,9 @@ import android.view.KeyEvent;
/** /**
* Receives media button events and calls to PlaybackService to respond * Receives media button events and calls to PlaybackService to respond
* appropriately. * appropriately.
*
* Most of this logic is only needed for RemoteControlImplICS (like double-click)
* as >= LP devices handle this using the MediaSessionCompat.
*/ */
public class MediaButtonReceiver extends BroadcastReceiver { public class MediaButtonReceiver extends BroadcastReceiver {
/** /**
@ -202,6 +205,7 @@ public class MediaButtonReceiver extends BroadcastReceiver {
/** /**
* Runable to run a delayed action * Runable to run a delayed action
* Inspects sLastClickTime and sDelayedClicks to guess what to do * Inspects sLastClickTime and sDelayedClicks to guess what to do
* Note: this is only used on pre-lolipop devices.
* *
* @param context the context to use * @param context the context to use
* @param serial the value of sLastClickTime during creation, used to identify stale events * @param serial the value of sLastClickTime during creation, used to identify stale events

View File

@ -22,33 +22,57 @@
package ch.blinkenlights.android.vanilla; package ch.blinkenlights.android.vanilla;
import android.content.Context;
import android.graphics.Bitmap;
import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat; import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.MediaMetadataCompat;
import android.view.KeyEvent;
/**
// Helper class used to show the notification seekbar. * Manages our active media session which is responsible for keeping
* the notification seek bar up to date and handling key events.
*/
public class MediaSessionTracker { public class MediaSessionTracker {
/** /**
* Instance of Vanillas PlaybackService * Context we are working with.
*/ */
private PlaybackService mPlaybackService; private Context mContext;
/** /**
* Our generic MediaSession * Our generic MediaSession
*/ */
private MediaSessionCompat mMediaSession; private MediaSessionCompat mMediaSession;
MediaSessionTracker(PlaybackService service) { MediaSessionTracker(Context context) {
mPlaybackService = service; mContext = context;
mMediaSession = new MediaSessionCompat(mContext, "Vanilla Music Media Session");
mMediaSession = new MediaSessionCompat(service, "Vanilla Music Media Session");
mMediaSession.setCallback(new MediaSessionCompat.Callback() { mMediaSession.setCallback(new MediaSessionCompat.Callback() {
@Override @Override
public void onSeekTo(long pos) { public void onPause() {
mPlaybackService.seekToPosition((int)pos); MediaButtonReceiver.processKey(mContext, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK));
} }
}); @Override
public void onPlay() {
MediaButtonReceiver.processKey(mContext, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK));
}
@Override
public void onSkipToNext() {
MediaButtonReceiver.processKey(mContext, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT));
}
@Override
public void onSkipToPrevious() {
MediaButtonReceiver.processKey(mContext, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS));
}
@Override
public void onStop() {
// We will behave the same as Google Play Music: for "Stop" we unconditionally Pause instead
MediaButtonReceiver.processKey(mContext, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE));
}
@Override
public void onSeekTo(long pos) {
PlaybackService.get(mContext).seekToPosition((int)pos);
}});
} }
/** /**
@ -67,20 +91,43 @@ public class MediaSessionTracker {
/** /**
* Populates the active media session with new info. * Populates the active media session with new info.
*
* @param song The song containing the new metadata.
* @param state PlaybackService state, used to determine playback state.
*/ */
public void updateSession(Song song, int state) { public void updateSession(Song song, int state) {
boolean playing = (state & PlaybackService.FLAG_PLAYING) != 0; final boolean playing = (state & PlaybackService.FLAG_PLAYING) != 0;
final PlaybackService service = PlaybackService.get(mContext);
PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder() PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder()
.setActions(PlaybackStateCompat.ACTION_SEEK_TO)
.setState(playing ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED, .setState(playing ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED,
mPlaybackService.getPosition(), 1.0f) service.getPosition(), 1.0f)
.setActions(PlaybackStateCompat.ACTION_PLAY |
PlaybackStateCompat.ACTION_STOP |
PlaybackStateCompat.ACTION_PAUSE |
PlaybackStateCompat.ACTION_PLAY_PAUSE |
PlaybackStateCompat.ACTION_SEEK_TO |
PlaybackStateCompat.ACTION_SKIP_TO_NEXT |
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)
.build(); .build();
mMediaSession.setPlaybackState(playbackState);
if (song != null) { if (song != null) {
mMediaSession.setMetadata(new MediaMetadataCompat.Builder() final Bitmap cover = song.getCover(mContext);
MediaMetadataCompat.Builder metadataBuilder = new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.artist)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.album)
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.title)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration) .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration)
.build()); .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, cover);
// logic copied from FullPlaybackActivity.updateQueuePosition()
if (PlaybackService.finishAction(service.getState()) != SongTimeline.FINISH_RANDOM) {
metadataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, service.getTimelinePosition() + 1);
metadataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, service.getTimelineLength());
} }
mMediaSession.setMetadata(metadataBuilder.build());
}
mMediaSession.setPlaybackState(playbackState);
mMediaSession.setActive(true);
} }
} }

View File

@ -1965,6 +1965,7 @@ public final class PlaybackService extends Service
for (int i = list.size(); --i != -1; ) for (int i = list.size(); --i != -1; )
list.get(i).onPositionInfoChanged(); list.get(i).onPositionInfoChanged();
mRemoteControlClient.updateRemote(mCurrentSong, mState, mForceNotificationVisible); mRemoteControlClient.updateRemote(mCurrentSong, mState, mForceNotificationVisible);
mMediaSessionTracker.updateSession(mCurrentSong, mState);
} }
private final LibraryObserver mObserver = new LibraryObserver() { private final LibraryObserver mObserver = new LibraryObserver() {

View File

@ -18,156 +18,25 @@
package ch.blinkenlights.android.vanilla; package ch.blinkenlights.android.vanilla;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.ComponentName;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.media.MediaMetadata;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.view.KeyEvent;
@TargetApi(21) @TargetApi(21)
public class RemoteControlImplLp implements RemoteControl.Client { public class RemoteControlImplLp implements RemoteControl.Client {
/** /**
* Context of this instance * This is just a placeholder implementation: On API 21, media buttons are handled in MediaSessionTracker.
*/
private final Context mContext;
/**
* Objects MediaSession handle
*/
private MediaSession mMediaSession;
/**
* Whether the cover should be shown. 1 for yes, 0 for no, -1 for
* uninitialized.
*/
private int mShowCover = -1;
/**
* Creates a new instance
*
* @param context The context to use
*/ */
public RemoteControlImplLp(Context context) { public RemoteControlImplLp(Context context) {
mContext = context;
} }
/**
* Registers a new MediaSession on the device
*/
public void initializeRemote() { public void initializeRemote() {
// make sure there is only one registered remote
unregisterRemote();
if (MediaButtonReceiver.useHeadsetControls(mContext) == false)
return;
mMediaSession = new MediaSession(mContext, "Vanilla Music");
mMediaSession.setCallback(new MediaSession.Callback() {
@Override
public void onPause() {
MediaButtonReceiver.processKey(mContext, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK));
}
@Override
public void onPlay() {
MediaButtonReceiver.processKey(mContext, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK));
}
@Override
public void onSkipToNext() {
MediaButtonReceiver.processKey(mContext, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT));
}
@Override
public void onSkipToPrevious() {
MediaButtonReceiver.processKey(mContext, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS));
}
@Override
public void onStop() {
// We will behave the same as Google Play Music: for "Stop" we unconditionally Pause instead
MediaButtonReceiver.processKey(mContext, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE));
}
});
Intent intent = new Intent();
intent.setComponent(new ComponentName(mContext.getPackageName(), MediaButtonReceiver.class.getName()));
PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
// This Seems to overwrite our MEDIA_BUTTON intent filter and there seems to be no way to unregister it
// Well: We intent to keep this around as long as possible anyway. But WHY ANDROID?!
mMediaSession.setMediaButtonReceiver(pendingIntent);
mMediaSession.setFlags(MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS | MediaSession.FLAG_HANDLES_MEDIA_BUTTONS);
} }
/**
* Unregisters a registered media session
*/
public void unregisterRemote() { public void unregisterRemote() {
if (mMediaSession != null) {
mMediaSession.setActive(false);
mMediaSession.release();
mMediaSession = null;
}
} }
/**
* Uninitializes our cached preferences, forcing a reload
*/
public void reloadPreference() { public void reloadPreference() {
mShowCover = -1;
} }
/**
* Update the remote with new metadata.
* {@link #initializeRemote()} must have been called
* first.
*
* @param song The song containing the new metadata.
* @param state PlaybackService state, used to determine playback state.
* @param keepPaused whether or not to keep the remote updated in paused mode
*/
public void updateRemote(Song song, int state, boolean keepPaused) { public void updateRemote(Song song, int state, boolean keepPaused) {
MediaSession session = mMediaSession;
if (session == null)
return;
boolean isPlaying = ((state & PlaybackService.FLAG_PLAYING) != 0);
if (mShowCover == -1) {
SharedPreferences settings = SharedPrefHelper.getSettings(mContext);
mShowCover = settings.getBoolean(PrefKeys.COVER_ON_LOCKSCREEN, PrefDefaults.COVER_ON_LOCKSCREEN) ? 1 : 0;
}
PlaybackService service = PlaybackService.get(mContext);
if (song != null) {
Bitmap bitmap = null;
if (mShowCover == 1 && (isPlaying || keepPaused)) {
bitmap = song.getCover(mContext);
}
MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder()
.putString(MediaMetadata.METADATA_KEY_ARTIST, song.artist)
.putString(MediaMetadata.METADATA_KEY_ALBUM, song.album)
.putString(MediaMetadata.METADATA_KEY_TITLE, song.title)
.putLong(MediaMetadata.METADATA_KEY_DURATION, song.duration)
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap);
// logic copied from FullPlaybackActivity.updateQueuePosition()
if (PlaybackService.finishAction(service.getState()) != SongTimeline.FINISH_RANDOM) {
metadataBuilder.putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, service.getTimelinePosition() + 1);
metadataBuilder.putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, service.getTimelineLength());
}
session.setMetadata(metadataBuilder.build());
}
int playbackState = (isPlaying ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_PAUSED);
session.setPlaybackState(new PlaybackState.Builder()
.setState(playbackState, service.getPosition(), 1.0f)
.setActions(PlaybackState.ACTION_PLAY | PlaybackState.ACTION_STOP | PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_PLAY_PAUSE |
PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS)
.build());
mMediaSession.setActive(true);
} }
} }