diff --git a/app/src/main/java/ch/blinkenlights/android/vanilla/MediaButtonReceiver.java b/app/src/main/java/ch/blinkenlights/android/vanilla/MediaButtonReceiver.java index a47de374..7a6f8d24 100644 --- a/app/src/main/java/ch/blinkenlights/android/vanilla/MediaButtonReceiver.java +++ b/app/src/main/java/ch/blinkenlights/android/vanilla/MediaButtonReceiver.java @@ -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 diff --git a/app/src/main/java/ch/blinkenlights/android/vanilla/MediaSessionTracker.java b/app/src/main/java/ch/blinkenlights/android/vanilla/MediaSessionTracker.java index a5368956..b4ea72b9 100644 --- a/app/src/main/java/ch/blinkenlights/android/vanilla/MediaSessionTracker.java +++ b/app/src/main/java/ch/blinkenlights/android/vanilla/MediaSessionTracker.java @@ -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); - } - }); + @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)); + } + @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() - .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration) - .build()); + 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) + .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); } } diff --git a/app/src/main/java/ch/blinkenlights/android/vanilla/PlaybackService.java b/app/src/main/java/ch/blinkenlights/android/vanilla/PlaybackService.java index 37a09b9e..8e81f8ed 100644 --- a/app/src/main/java/ch/blinkenlights/android/vanilla/PlaybackService.java +++ b/app/src/main/java/ch/blinkenlights/android/vanilla/PlaybackService.java @@ -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() { diff --git a/app/src/main/java/ch/blinkenlights/android/vanilla/RemoteControlImplLp.java b/app/src/main/java/ch/blinkenlights/android/vanilla/RemoteControlImplLp.java index 2bb89787..e2e7987b 100644 --- a/app/src/main/java/ch/blinkenlights/android/vanilla/RemoteControlImplLp.java +++ b/app/src/main/java/ch/blinkenlights/android/vanilla/RemoteControlImplLp.java @@ -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); } }