From 81c6527e73618c0d90a78b32d62a235f00532777 Mon Sep 17 00:00:00 2001 From: Adrian Ulrich Date: Sun, 20 Dec 2015 16:37:56 +0100 Subject: [PATCH] Use new MediaSession API on 5.x --- .../android/vanilla/PlaybackService.java | 14 +- .../android/vanilla/RemoteControl.java | 129 +++------------- .../vanilla/RemoteControlImplKitKat.java | 144 ++++++++++++++++++ .../android/vanilla/RemoteControlImplLp.java | 122 +++++++++++++++ 4 files changed, 300 insertions(+), 109 deletions(-) create mode 100644 src/ch/blinkenlights/android/vanilla/RemoteControlImplKitKat.java create mode 100644 src/ch/blinkenlights/android/vanilla/RemoteControlImplLp.java diff --git a/src/ch/blinkenlights/android/vanilla/PlaybackService.java b/src/ch/blinkenlights/android/vanilla/PlaybackService.java index 9179eb48..60429efa 100644 --- a/src/ch/blinkenlights/android/vanilla/PlaybackService.java +++ b/src/ch/blinkenlights/android/vanilla/PlaybackService.java @@ -337,6 +337,10 @@ public final class PlaybackService extends Service * The SensorManager service. */ private SensorManager mSensorManager; + /** + * A remote control client implementation + */ + private RemoteControl.Client mRemoteControlClient; SongTimeline mTimeline; private Song mCurrentSong; @@ -495,7 +499,8 @@ public final class PlaybackService extends Service getContentResolver().registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mObserver); - RemoteControl.registerRemote(this, mAudioManager); + mRemoteControlClient = new RemoteControl().getClient(this); + mRemoteControlClient.registerRemote(mAudioManager); mLooper = thread.getLooper(); mHandler = new Handler(mLooper, this); @@ -614,6 +619,9 @@ public final class PlaybackService extends Service if (mSensorManager != null) mSensorManager.unregisterListener(this); + if (mRemoteControlClient != null) + mRemoteControlClient.unregisterRemote(); + super.onDestroy(); } @@ -833,7 +841,7 @@ public final class PlaybackService extends Service } else if (PrefKeys.MEDIA_BUTTON.equals(key) || PrefKeys.MEDIA_BUTTON_BEEP.equals(key)) { MediaButtonReceiver.reloadPreference(this); } else if (PrefKeys.COVER_ON_LOCKSCREEN.equals(key)) { - RemoteControl.reloadPreference(); + mRemoteControlClient.reloadPreference(); } else if (PrefKeys.USE_IDLE_TIMEOUT.equals(key) || PrefKeys.IDLE_TIMEOUT.equals(key)) { mIdleTimeout = settings.getBoolean(PrefKeys.USE_IDLE_TIMEOUT, PrefDefaults.USE_IDLE_TIMEOUT) ? settings.getInt(PrefKeys.IDLE_TIMEOUT, PrefDefaults.IDLE_TIMEOUT) : 0; userActionTriggered(); @@ -1055,7 +1063,7 @@ public final class PlaybackService extends Service if (mReadaheadEnabled) triggerReadAhead(); - RemoteControl.updateRemote(this, mCurrentSong, mState, mForceNotificationVisible); + mRemoteControlClient.updateRemote(mCurrentSong, mState, mForceNotificationVisible); if (mStockBroadcast) stockMusicBroadcast(); diff --git a/src/ch/blinkenlights/android/vanilla/RemoteControl.java b/src/ch/blinkenlights/android/vanilla/RemoteControl.java index e40a8d7e..7c9004aa 100644 --- a/src/ch/blinkenlights/android/vanilla/RemoteControl.java +++ b/src/ch/blinkenlights/android/vanilla/RemoteControl.java @@ -1,129 +1,46 @@ /* * Copyright (C) 2015 Adrian Ulrich - * Copyright (C) 2012 Christopher Eby * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . */ package ch.blinkenlights.android.vanilla; -import android.app.PendingIntent; -import android.content.ComponentName; import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.graphics.Bitmap; import android.media.AudioManager; -import android.media.MediaMetadataRetriever; -import android.media.RemoteControlClient; +import android.os.Build; public class RemoteControl { - /** - * Used with updateRemote method. - */ - private static RemoteControlClient sRemote; - /** - * Whether the cover should be shown. 1 for yes, 0 for no, -1 for - * uninitialized. - */ - private static int sShowCover = -1; /** - * Perform initialization required for RemoteControlClient. - * - * @param context A context to use. - * @param am The AudioManager service. + * Returns a RemoteControl.Client implementation */ - public static void registerRemote(Context context, AudioManager am) - { - if (!MediaButtonReceiver.useHeadsetControls(context)) { - // RemoteControlClient requires MEDIA_BUTTON intent - return; - } - - MediaButtonReceiver.registerMediaButton(context); - - Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); - mediaButtonIntent.setComponent(new ComponentName(context.getPackageName(), MediaButtonReceiver.class.getName())); - PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(context, 0, mediaButtonIntent, 0); - RemoteControlClient remote = new RemoteControlClient(mediaPendingIntent); - int flags = RemoteControlClient.FLAG_KEY_MEDIA_NEXT - | RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS - | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE - | RemoteControlClient.FLAG_KEY_MEDIA_PLAY - | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE; - remote.setTransportControlFlags(flags); - am.registerRemoteControlClient(remote); - sRemote = remote; + public RemoteControl.Client getClient(Context context) { + return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? + new RemoteControlImplLp(context) : + new RemoteControlImplKitKat(context) // legacy implementation, kept until we drop 4.x support + ); } /** - * Uninitializes our cached preferences, forcing a reload + * Interface definition of our RemoteControl API */ - public static void reloadPreference() - { - sShowCover = -1; - } - - /** - * Update the remote with new metadata. - * {@link #registerRemote(Context, AudioManager)} must have been called - * first. - * - * @param context A context to use. - * @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 static void updateRemote(Context context, Song song, int state, boolean keepPaused) - { - RemoteControlClient remote = sRemote; - if (remote == null) - return; - - boolean isPlaying = ((state & PlaybackService.FLAG_PLAYING) != 0); - - if (sShowCover == -1) { - SharedPreferences settings = PlaybackService.getSettings(context); - sShowCover = settings.getBoolean(PrefKeys.COVER_ON_LOCKSCREEN, PrefDefaults.COVER_ON_LOCKSCREEN) ? 1 : 0; - } - - remote.setPlaybackState(isPlaying ? RemoteControlClient.PLAYSTATE_PLAYING : RemoteControlClient.PLAYSTATE_PAUSED); - RemoteControlClient.MetadataEditor editor = remote.editMetadata(true); - if (song != null) { - editor.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, song.artist); - editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, song.album); - editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, song.title); - Bitmap bitmap = song.getCover(context); - if (bitmap != null && sShowCover == 1 && (isPlaying || keepPaused)) { - // Create a copy of the cover art, since RemoteControlClient likes - // to recycle what we give it. - bitmap = bitmap.copy(Bitmap.Config.RGB_565, false); - } else { - // Some lockscreen implementations fail to clear the cover artwork - // if we send a null bitmap. We are creating a 16x16 transparent - // bitmap to work around this limitation. - bitmap = Bitmap.createBitmap(16, 16, Bitmap.Config.ARGB_8888); - } - editor.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, bitmap); - } - editor.apply(); + public interface Client { + public void registerRemote(AudioManager am); + public void unregisterRemote(); + public void reloadPreference(); + public void updateRemote(Song song, int state, boolean keepPaused); } } diff --git a/src/ch/blinkenlights/android/vanilla/RemoteControlImplKitKat.java b/src/ch/blinkenlights/android/vanilla/RemoteControlImplKitKat.java new file mode 100644 index 00000000..fdc22105 --- /dev/null +++ b/src/ch/blinkenlights/android/vanilla/RemoteControlImplKitKat.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2015 Adrian Ulrich + * Copyright (C) 2012 Christopher Eby + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package ch.blinkenlights.android.vanilla; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.media.AudioManager; +import android.media.MediaMetadataRetriever; +import android.media.RemoteControlClient; + + +public class RemoteControlImplKitKat implements RemoteControl.Client { + /** + * Context of this instance + */ + private Context mContext; + /** + * Used with updateRemote method. + */ + private RemoteControlClient mRemote; + /** + * 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 RemoteControlImplKitKat(Context context) { + mContext = context; + } + + /** + * Perform initialization required for RemoteControlClient. + * + * @param am The AudioManager service. + */ + public void registerRemote(AudioManager am) + { + if (!MediaButtonReceiver.useHeadsetControls(mContext)) { + // RemoteControlClient requires MEDIA_BUTTON intent + return; + } + + MediaButtonReceiver.registerMediaButton(mContext); + + Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + mediaButtonIntent.setComponent(new ComponentName(mContext.getPackageName(), MediaButtonReceiver.class.getName())); + PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(mContext, 0, mediaButtonIntent, 0); + RemoteControlClient remote = new RemoteControlClient(mediaPendingIntent); + int flags = RemoteControlClient.FLAG_KEY_MEDIA_NEXT + | RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS + | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE + | RemoteControlClient.FLAG_KEY_MEDIA_PLAY + | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE; + remote.setTransportControlFlags(flags); + am.registerRemoteControlClient(remote); + mRemote = remote; + } + + public void unregisterRemote() { + // we should probably call am.unregisterRemoteControlClient but we never did and i'm not touching the legacy implementation. + } + + /** + * Uninitializes our cached preferences, forcing a reload + */ + public void reloadPreference() + { + mShowCover = -1; + } + + /** + * Update the remote with new metadata. + * {@link #registerRemote(Context, AudioManager)} 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) + { + RemoteControlClient remote = mRemote; + if (remote == null) + return; + + boolean isPlaying = ((state & PlaybackService.FLAG_PLAYING) != 0); + + if (mShowCover == -1) { + SharedPreferences settings = PlaybackService.getSettings(mContext); + mShowCover = settings.getBoolean(PrefKeys.COVER_ON_LOCKSCREEN, PrefDefaults.COVER_ON_LOCKSCREEN) ? 1 : 0; + } + + remote.setPlaybackState(isPlaying ? RemoteControlClient.PLAYSTATE_PLAYING : RemoteControlClient.PLAYSTATE_PAUSED); + RemoteControlClient.MetadataEditor editor = remote.editMetadata(true); + if (song != null) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, song.artist); + editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, song.album); + editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, song.title); + Bitmap bitmap = song.getCover(mContext); + if (bitmap != null && mShowCover == 1 && (isPlaying || keepPaused)) { + // Create a copy of the cover art, since RemoteControlClient likes + // to recycle what we give it. + bitmap = bitmap.copy(Bitmap.Config.RGB_565, false); + } else { + // Some lockscreen implementations fail to clear the cover artwork + // if we send a null bitmap. We are creating a 16x16 transparent + // bitmap to work around this limitation. + bitmap = Bitmap.createBitmap(16, 16, Bitmap.Config.ARGB_8888); + } + editor.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, bitmap); + } + editor.apply(); + } +} diff --git a/src/ch/blinkenlights/android/vanilla/RemoteControlImplLp.java b/src/ch/blinkenlights/android/vanilla/RemoteControlImplLp.java new file mode 100644 index 00000000..17731f9b --- /dev/null +++ b/src/ch/blinkenlights/android/vanilla/RemoteControlImplLp.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2015 Adrian Ulrich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ch.blinkenlights.android.vanilla; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.media.AudioManager; +import android.media.MediaMetadata; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; + + + +public class RemoteControlImplLp implements RemoteControl.Client { + /** + * Context of this instance + */ + private 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) { + mContext = context; + } + + /** + * Registers a new MediaSession on the device + * + * @param am The AudioManager service. (unused) + */ + public void registerRemote(AudioManager am) { + mMediaSession = new MediaSession(mContext, "VanillaMusic"); + mMediaSession.setFlags(MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); + mMediaSession.setActive(true); + } + + /** + * Unregisters a registered media session + */ + public void unregisterRemote() { + mMediaSession.setActive(false); + mMediaSession.release(); + } + + /** + * Uninitializes our cached preferences, forcing a reload + */ + public void reloadPreference() { + mShowCover = -1; + } + + /** + * Update the remote with new metadata. + * {@link #registerRemote(AudioManager)} 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 = PlaybackService.getSettings(mContext); + mShowCover = settings.getBoolean(PrefKeys.COVER_ON_LOCKSCREEN, PrefDefaults.COVER_ON_LOCKSCREEN) ? 1 : 0; + } + + if (song != null) { + Bitmap bitmap = null; + if (mShowCover == 1 && (isPlaying || keepPaused)) { + bitmap = song.getCover(mContext); + if (bitmap != null) + bitmap.copy(Bitmap.Config.ARGB_8888, false); + } + + session.setMetadata(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) + .build()); + } + + int playbackState = (isPlaying ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_PAUSED); + session.setPlaybackState(new PlaybackState.Builder() + .setState(playbackState, PlaybackState.PLAYBACK_POSITION_UNKNOWN , 1.0f).build()); + } +}