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
* 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 {
/**
@ -202,6 +205,7 @@ public class MediaButtonReceiver extends BroadcastReceiver {
/**
* Runable to run a delayed action
* 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 serial the value of sLastClickTime during creation, used to identify stale events

View File

@ -22,33 +22,57 @@
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.PlaybackStateCompat;
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 {
/**
* Instance of Vanillas PlaybackService
* Context we are working with.
*/
private PlaybackService mPlaybackService;
private Context mContext;
/**
* Our generic MediaSession
*/
private MediaSessionCompat mMediaSession;
MediaSessionTracker(PlaybackService service) {
mPlaybackService = service;
mMediaSession = new MediaSessionCompat(service, "Vanilla Music Media Session");
MediaSessionTracker(Context context) {
mContext = context;
mMediaSession = new MediaSessionCompat(mContext, "Vanilla Music Media Session");
mMediaSession.setCallback(new MediaSessionCompat.Callback() {
@Override
public void onSeekTo(long pos) {
mPlaybackService.seekToPosition((int)pos);
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));
}
@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.
*
* @param song The song containing the new metadata.
* @param state PlaybackService state, used to determine playback 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()
.setActions(PlaybackStateCompat.ACTION_SEEK_TO)
.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();
mMediaSession.setPlaybackState(playbackState);
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)
.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; )
list.get(i).onPositionInfoChanged();
mRemoteControlClient.updateRemote(mCurrentSong, mState, mForceNotificationVisible);
mMediaSessionTracker.updateSession(mCurrentSong, mState);
}
private final LibraryObserver mObserver = new LibraryObserver() {

View File

@ -18,156 +18,25 @@
package ch.blinkenlights.android.vanilla;
import android.annotation.TargetApi;
import android.app.PendingIntent;
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)
public class RemoteControlImplLp implements RemoteControl.Client {
/**
* Context of this instance
*/
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
* This is just a placeholder implementation: On API 21, media buttons are handled in MediaSessionTracker.
*/
public RemoteControlImplLp(Context context) {
mContext = context;
}
/**
* Registers a new MediaSession on the device
*/
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() {
if (mMediaSession != null) {
mMediaSession.setActive(false);
mMediaSession.release();
mMediaSession = null;
}
}
/**
* Uninitializes our cached preferences, forcing a reload
*/
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) {
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);
}
}