diff options
| author | Roman Nurik <romannurik@google.com> | 2011-09-30 18:02:10 -0700 |
|---|---|---|
| committer | Roman Nurik <romannurik@google.com> | 2011-10-05 16:50:26 -0700 |
| commit | 5986e12034e38fd165d2f100c7e404bbf61b2649 (patch) | |
| tree | 372fa846f2997efbbc41dd3a7310ad68f62f5619 /samples/RandomMusicPlayer/src | |
| parent | 8fbf284cc83263a6b5ce37afe9a14e8f06d8c317 (diff) | |
| download | android_development-5986e12034e38fd165d2f100c7e404bbf61b2649.tar.gz android_development-5986e12034e38fd165d2f100c7e404bbf61b2649.tar.bz2 android_development-5986e12034e38fd165d2f100c7e404bbf61b2649.zip | |
Update RandomMusicPlayer sample for new RemoteControlClient APIs, also add media button support
Change-Id: Ia6ad08dd0ec1e67f1cffa2d6cfca2120ee0a96c7
Diffstat (limited to 'samples/RandomMusicPlayer/src')
8 files changed, 755 insertions, 66 deletions
diff --git a/samples/RandomMusicPlayer/src/com/example/android/musicplayer/AudioFocusHelper.java b/samples/RandomMusicPlayer/src/com/example/android/musicplayer/AudioFocusHelper.java index 4b8b54abc..980bac808 100644 --- a/samples/RandomMusicPlayer/src/com/example/android/musicplayer/AudioFocusHelper.java +++ b/samples/RandomMusicPlayer/src/com/example/android/musicplayer/AudioFocusHelper.java @@ -51,7 +51,6 @@ public class AudioFocusHelper implements AudioManager.OnAudioFocusChangeListener * Called by AudioManager on audio focus changes. We implement this by calling our * MusicFocusable appropriately to relay the message. */ - @Override public void onAudioFocusChange(int focusChange) { if (mFocusable == null) return; switch (focusChange) { diff --git a/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MainActivity.java b/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MainActivity.java index 4974a218e..4d5224b2a 100644 --- a/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MainActivity.java +++ b/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MainActivity.java @@ -22,6 +22,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; @@ -71,7 +72,6 @@ public class MainActivity extends Activity implements OnClickListener { mEjectButton.setOnClickListener(this); } - @Override public void onClick(View target) { // Send the correct intent to the MusicService, according to the button that was clicked if (target == mPlayButton) @@ -119,4 +119,15 @@ public class MainActivity extends Activity implements OnClickListener { alertBuilder.show(); } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_HEADSETHOOK: + startService(new Intent(MusicService.ACTION_TOGGLE_PLAYBACK)); + return true; + } + return super.onKeyDown(keyCode, event); + } } diff --git a/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MediaButtonHelper.java b/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MediaButtonHelper.java new file mode 100644 index 000000000..1bacee5cf --- /dev/null +++ b/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MediaButtonHelper.java @@ -0,0 +1,85 @@ +package com.example.android.musicplayer; + +import android.content.ComponentName; +import android.media.AudioManager; +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Class that assists with handling new media button APIs available in API level 8. + */ +public class MediaButtonHelper { + // Backwards compatibility code (methods available as of API Level 8) + private static final String TAG = "MediaButtonHelper"; + + static { + initializeStaticCompatMethods(); + } + + static Method sMethodRegisterMediaButtonEventReceiver; + static Method sMethodUnregisterMediaButtonEventReceiver; + + static void initializeStaticCompatMethods() { + try { + sMethodRegisterMediaButtonEventReceiver = AudioManager.class.getMethod( + "registerMediaButtonEventReceiver", + new Class[] { ComponentName.class }); + sMethodUnregisterMediaButtonEventReceiver = AudioManager.class.getMethod( + "unregisterMediaButtonEventReceiver", + new Class[] { ComponentName.class }); + } catch (NoSuchMethodException e) { + // Silently fail when running on an OS before API level 8. + } + } + + public static void registerMediaButtonEventReceiverCompat(AudioManager audioManager, + ComponentName receiver) { + if (sMethodRegisterMediaButtonEventReceiver == null) + return; + + try { + sMethodRegisterMediaButtonEventReceiver.invoke(audioManager, receiver); + } catch (InvocationTargetException e) { + // Unpack original exception when possible + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else if (cause instanceof Error) { + throw (Error) cause; + } else { + // Unexpected checked exception; wrap and re-throw + throw new RuntimeException(e); + } + } catch (IllegalAccessException e) { + Log.e(TAG, "IllegalAccessException invoking registerMediaButtonEventReceiver."); + e.printStackTrace(); + } + } + + @SuppressWarnings("unused") + public static void unregisterMediaButtonEventReceiverCompat(AudioManager audioManager, + ComponentName receiver) { + if (sMethodUnregisterMediaButtonEventReceiver == null) + return; + + try { + sMethodUnregisterMediaButtonEventReceiver.invoke(audioManager, receiver); + } catch (InvocationTargetException e) { + // Unpack original exception when possible + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else if (cause instanceof Error) { + throw (Error) cause; + } else { + // Unexpected checked exception; wrap and re-throw + throw new RuntimeException(e); + } + } catch (IllegalAccessException e) { + Log.e(TAG, "IllegalAccessException invoking unregisterMediaButtonEventReceiver."); + e.printStackTrace(); + } + } +} diff --git a/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MusicIntentReceiver.java b/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MusicIntentReceiver.java index cc03d5e23..28f19d4af 100644 --- a/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MusicIntentReceiver.java +++ b/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MusicIntentReceiver.java @@ -19,22 +19,53 @@ package com.example.android.musicplayer; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.util.Log; +import android.view.KeyEvent; import android.widget.Toast; /** * Receives broadcasted intents. In particular, we are interested in the - * android.media.AUDIO_BECOMING_NOISY intent, which is broadcast, for example, when the user - * disconnects the headphones. This class works because we are declaring it in a <receiver> - * tag in AndroidManifest.xml. + * android.media.AUDIO_BECOMING_NOISY and android.intent.action.MEDIA_BUTTON intents, which is + * broadcast, for example, when the user disconnects the headphones. This class works because we are + * declaring it in a <receiver> tag in AndroidManifest.xml. */ public class MusicIntentReceiver extends BroadcastReceiver { @Override - public void onReceive(Context ctx, Intent intent) { + public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) { - Toast.makeText(ctx, "Headphones disconnected.", Toast.LENGTH_SHORT).show(); + Toast.makeText(context, "Headphones disconnected.", Toast.LENGTH_SHORT).show(); // send an intent to our MusicService to telling it to pause the audio - ctx.startService(new Intent(MusicService.ACTION_PAUSE)); + context.startService(new Intent(MusicService.ACTION_PAUSE)); + + } else if (intent.getAction().equals(Intent.ACTION_MEDIA_BUTTON)) { + KeyEvent keyEvent = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT); + if (keyEvent.getAction() != KeyEvent.ACTION_DOWN) + return; + + switch (keyEvent.getKeyCode()) { + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + context.startService(new Intent(MusicService.ACTION_TOGGLE_PLAYBACK)); + break; + case KeyEvent.KEYCODE_MEDIA_PLAY: + context.startService(new Intent(MusicService.ACTION_PLAY)); + break; + case KeyEvent.KEYCODE_MEDIA_PAUSE: + context.startService(new Intent(MusicService.ACTION_PAUSE)); + break; + case KeyEvent.KEYCODE_MEDIA_STOP: + context.startService(new Intent(MusicService.ACTION_STOP)); + break; + case KeyEvent.KEYCODE_MEDIA_NEXT: + context.startService(new Intent(MusicService.ACTION_SKIP)); + break; + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + // TODO: ensure that doing this in rapid succession actually plays the + // previous song + context.startService(new Intent(MusicService.ACTION_REWIND)); + break; + } } } } diff --git a/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MusicRetriever.java b/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MusicRetriever.java index 44d644756..5e6f2f142 100644 --- a/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MusicRetriever.java +++ b/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MusicRetriever.java @@ -16,16 +16,17 @@ package com.example.android.musicplayer; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; - import android.content.ContentResolver; import android.content.ContentUris; import android.database.Cursor; import android.net.Uri; +import android.provider.MediaStore; import android.util.Log; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + /** * Retrieves and organizes media to play. Before being used, you must call {@link #prepare()}, * which will retrieve all of the music on the user's device (by performing a query on a content @@ -57,7 +58,8 @@ public class MusicRetriever { // Perform a query on the content resolver. The URI we're passing specifies that we // want to query for all audio media on external storage (e.g. SD card) - Cursor cur = mContentResolver.query(uri, null, null, null, null); + Cursor cur = mContentResolver.query(uri, null, + MediaStore.Audio.Media.IS_MUSIC + " = 1", null, null); Log.i(TAG, "Query finished. " + (cur == null ? "Returned NULL." : "Returned a cursor.")); if (cur == null) { @@ -73,9 +75,12 @@ public class MusicRetriever { Log.i(TAG, "Listing..."); - // retrieve the indices of the columns where the ID and title of the song are - int titleColumn = cur.getColumnIndex(android.provider.MediaStore.Audio.Media.TITLE); - int idColumn = cur.getColumnIndex(android.provider.MediaStore.Audio.Media._ID); + // retrieve the indices of the columns where the ID, title, etc. of the song are + int artistColumn = cur.getColumnIndex(MediaStore.Audio.Media.ARTIST); + int titleColumn = cur.getColumnIndex(MediaStore.Audio.Media.TITLE); + int albumColumn = cur.getColumnIndex(MediaStore.Audio.Media.ALBUM); + int durationColumn = cur.getColumnIndex(MediaStore.Audio.Media.DURATION); + int idColumn = cur.getColumnIndex(MediaStore.Audio.Media._ID); Log.i(TAG, "Title column index: " + String.valueOf(titleColumn)); Log.i(TAG, "ID column index: " + String.valueOf(titleColumn)); @@ -83,7 +88,12 @@ public class MusicRetriever { // add each song to mItems do { Log.i(TAG, "ID: " + cur.getString(idColumn) + " Title: " + cur.getString(titleColumn)); - mItems.add(new Item(cur.getLong(idColumn), cur.getString(titleColumn))); + mItems.add(new Item( + cur.getLong(idColumn), + cur.getString(artistColumn), + cur.getString(titleColumn), + cur.getString(albumColumn), + cur.getLong(durationColumn))); } while (cur.moveToNext()); Log.i(TAG, "Done querying media. MusicRetriever is ready."); @@ -99,17 +109,41 @@ public class MusicRetriever { return mItems.get(mRandom.nextInt(mItems.size())); } - public class Item { + public static class Item { long id; + String artist; String title; + String album; + long duration; - public Item(long id, String title) { + public Item(long id, String artist, String title, String album, long duration) { this.id = id; + this.artist = artist; this.title = title; + this.album = album; + this.duration = duration; + } + + public long getId() { + return id; + } + + public String getArtist() { + return artist; + } + + public String getTitle() { + return title; + } + + public String getAlbum() { + return album; + } + + public long getDuration() { + return duration; } - public long getId() { return id; } - public String getTitle() { return title; } public Uri getURI() { return ContentUris.withAppendedId( android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id); diff --git a/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MusicService.java b/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MusicService.java index 9bd12512b..25f5d81fb 100644 --- a/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MusicService.java +++ b/samples/RandomMusicPlayer/src/com/example/android/musicplayer/MusicService.java @@ -16,19 +16,22 @@ package com.example.android.musicplayer; -import java.io.IOException; - import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.media.AudioManager; +import android.media.MediaMetadataRetriever; import android.media.MediaPlayer; import android.media.MediaPlayer.OnCompletionListener; import android.media.MediaPlayer.OnErrorListener; import android.media.MediaPlayer.OnPreparedListener; +import android.media.RemoteControlClient; import android.net.Uri; import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.WifiLock; @@ -37,9 +40,11 @@ import android.os.PowerManager; import android.util.Log; import android.widget.Toast; +import java.io.IOException; + /** * Service that handles media playback. This is the Service through which we perform all the media - * handling in our application. Upon initialization, it starts a {@link MediaRetriever} to scan + * handling in our application. Upon initialization, it starts a {@link MusicRetriever} to scan * the user's media. Then, it waits for Intents (which come from our main activity, * {@link MainActivity}, which signal the service to perform specific operations: Play, Pause, * Rewind, Skip, etc. @@ -48,7 +53,25 @@ public class MusicService extends Service implements OnCompletionListener, OnPre OnErrorListener, MusicFocusable, PrepareMusicRetrieverTask.MusicRetrieverPreparedListener { - NotificationManager mNotificationManager; + // The tag we put on debug messages + final static String TAG = "RandomMusicPlayer"; + + // These are the Intent actions that we are prepared to handle. Notice that the fact these + // constants exist in our class is a mere convenience: what really defines the actions our + // service can handle are the <action> tags in the <intent-filters> tag for our service in + // AndroidManifest.xml. + public static final String ACTION_TOGGLE_PLAYBACK = + "com.example.android.musicplayer.action.TOGGLE_PLAYBACK"; + public static final String ACTION_PLAY = "com.example.android.musicplayer.action.PLAY"; + public static final String ACTION_PAUSE = "com.example.android.musicplayer.action.PAUSE"; + public static final String ACTION_STOP = "com.example.android.musicplayer.action.STOP"; + public static final String ACTION_SKIP = "com.example.android.musicplayer.action.SKIP"; + public static final String ACTION_REWIND = "com.example.android.musicplayer.action.REWIND"; + public static final String ACTION_URL = "com.example.android.musicplayer.action.URL"; + + // The volume we set the media player to when we lose audio focus, but are allowed to reduce + // the volume instead of stopping playback. + public static final float DUCK_VOLUME = 0.1f; // our media player MediaPlayer mPlayer = null; @@ -104,24 +127,6 @@ public class MusicService extends Service implements OnCompletionListener, OnPre // device from shutting off the Wifi radio WifiLock mWifiLock; - // The tag we put on debug messages - final static String TAG = "RandomMusicPlayer"; - - // These are the Intent actions that we are prepared to handle. Notice that the fact these - // constants exist in our class is a mere convenience: what really defines the actions our - // service can handle are the <action> tags in the <intent-filters> tag for our service in - // AndroidManifest.xml. - public static final String ACTION_PLAY = "com.example.android.musicplayer.action.PLAY"; - public static final String ACTION_PAUSE = "com.example.android.musicplayer.action.PAUSE"; - public static final String ACTION_STOP = "com.example.android.musicplayer.action.STOP"; - public static final String ACTION_SKIP = "com.example.android.musicplayer.action.SKIP"; - public static final String ACTION_REWIND = "com.example.android.musicplayer.action.REWIND"; - public static final String ACTION_URL = "com.example.android.musicplayer.action.URL"; - - // The volume we set the media player to when we lose audio focus, but are allowed to reduce - // the volume instead of stopping playback. - public final float DUCK_VOLUME = 0.1f; - // The ID we use for the notification (the onscreen alert that appears at the notification // area at the top of the screen as an icon -- and as text as well if the user expands the // notification area). @@ -131,6 +136,20 @@ public class MusicService extends Service implements OnCompletionListener, OnPre // providing titles and URIs as we need. MusicRetriever mRetriever; + // our RemoteControlClient object, which will use remote control APIs available in + // SDK level >= 14, if they're available. + RemoteControlClientCompat mRemoteControlClientCompat; + + // Dummy album art we will pass to the remote control (if the APIs are available). + Bitmap mDummyAlbumArt; + + // The component name of MusicIntentReceiver, for use with media button and remote control + // APIs + ComponentName mMediaButtonReceiverComponent; + + AudioManager mAudioManager; + NotificationManager mNotificationManager; + Notification mNotification = null; /** @@ -167,6 +186,7 @@ public class MusicService extends Service implements OnCompletionListener, OnPre .createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock"); mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE); // Create the retriever and start an asynchronous task that will prepare it. mRetriever = new MusicRetriever(getContentResolver()); @@ -177,6 +197,10 @@ public class MusicService extends Service implements OnCompletionListener, OnPre mAudioFocusHelper = new AudioFocusHelper(getApplicationContext(), this); else mAudioFocus = AudioFocus.Focused; // no focus feature, so we always "have" audio focus + + mDummyAlbumArt = BitmapFactory.decodeResource(getResources(), R.drawable.dummy_album_art); + + mMediaButtonReceiverComponent = new ComponentName(this, MusicIntentReceiver.class); } /** @@ -187,7 +211,8 @@ public class MusicService extends Service implements OnCompletionListener, OnPre @Override public int onStartCommand(Intent intent, int flags, int startId) { String action = intent.getAction(); - if (action.equals(ACTION_PLAY)) processPlayRequest(); + if (action.equals(ACTION_TOGGLE_PLAYBACK)) processTogglePlaybackRequest(); + else if (action.equals(ACTION_PLAY)) processPlayRequest(); else if (action.equals(ACTION_PAUSE)) processPauseRequest(); else if (action.equals(ACTION_SKIP)) processSkipRequest(); else if (action.equals(ACTION_STOP)) processStopRequest(); @@ -198,6 +223,14 @@ public class MusicService extends Service implements OnCompletionListener, OnPre // restart in case it's killed. } + void processTogglePlaybackRequest() { + if (mState == State.Paused || mState == State.Stopped) { + processPlayRequest(); + } else { + processPauseRequest(); + } + } + void processPlayRequest() { if (mState == State.Retrieving) { // If we are still retrieving media, just set the flag to start playing when we're @@ -209,6 +242,8 @@ public class MusicService extends Service implements OnCompletionListener, OnPre tryToGetAudioFocus(); + // actually play the song + if (mState == State.Stopped) { // If we're stopped, just go ahead to the next song and start playing playNextSong(null); @@ -219,6 +254,12 @@ public class MusicService extends Service implements OnCompletionListener, OnPre setUpAsForeground(mSongTitle + " (playing)"); configAndStartMediaPlayer(); } + + // Tell any remote controls that our playback state is 'playing'. + if (mRemoteControlClientCompat != null) { + mRemoteControlClientCompat + .setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); + } } void processPauseRequest() { @@ -234,7 +275,13 @@ public class MusicService extends Service implements OnCompletionListener, OnPre mState = State.Paused; mPlayer.pause(); relaxResources(false); // while paused, we always retain the MediaPlayer - giveUpAudioFocus(); + // do not give up audio focus + } + + // Tell any remote controls that our playback state is 'paused'. + if (mRemoteControlClientCompat != null) { + mRemoteControlClientCompat + .setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); } } @@ -251,13 +298,23 @@ public class MusicService extends Service implements OnCompletionListener, OnPre } void processStopRequest() { - if (mState == State.Playing || mState == State.Paused) { + processStopRequest(false); + } + + void processStopRequest(boolean force) { + if (mState == State.Playing || mState == State.Paused || force) { mState = State.Stopped; // let go of all resources... relaxResources(true); giveUpAudioFocus(); + // Tell any remote controls that our playback state is 'paused'. + if (mRemoteControlClientCompat != null) { + mRemoteControlClientCompat + .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); + } + // service is no longer necessary. Will be started again if needed. stopSelf(); } @@ -330,14 +387,6 @@ public class MusicService extends Service implements OnCompletionListener, OnPre } } - /** - * Shortcut to making and displaying a toast. Seemed cleaner than repeating - * this code everywhere, at least for this sample. - */ - void say(String message) { - Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); - } - void tryToGetAudioFocus() { if (mAudioFocus != AudioFocus.Focused && mAudioFocusHelper != null && mAudioFocusHelper.requestFocus()) @@ -355,34 +404,80 @@ public class MusicService extends Service implements OnCompletionListener, OnPre relaxResources(false); // release everything except MediaPlayer try { + MusicRetriever.Item playingItem = null; if (manualUrl != null) { // set the source of the media player to a manual URL or path createMediaPlayerIfNeeded(); mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mPlayer.setDataSource(manualUrl); - mSongTitle = manualUrl; mIsStreaming = manualUrl.startsWith("http:") || manualUrl.startsWith("https:"); + + playingItem = new MusicRetriever.Item(0, null, manualUrl, null, 0); } else { mIsStreaming = false; // playing a locally available song - MusicRetriever.Item item = mRetriever.getRandomItem(); - if (item == null) { - say("No song to play :-("); + playingItem = mRetriever.getRandomItem(); + if (playingItem == null) { + Toast.makeText(this, + "No available music to play. Place some music on your external storage " + + "device (e.g. your SD card) and try again.", + Toast.LENGTH_LONG).show(); + processStopRequest(true); // stop everything! return; } // set the source of the media player a a content URI createMediaPlayerIfNeeded(); mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - mPlayer.setDataSource(getApplicationContext(), item.getURI()); - mSongTitle = item.getTitle(); + mPlayer.setDataSource(getApplicationContext(), playingItem.getURI()); } + mSongTitle = playingItem.getTitle(); mState = State.Preparing; setUpAsForeground(mSongTitle + " (loading)"); + // Use the media button APIs (if available) to register ourselves for media button + // events + + MediaButtonHelper.registerMediaButtonEventReceiverCompat( + mAudioManager, mMediaButtonReceiverComponent); + + // Use the remote control APIs (if available) to set the playback state + + if (mRemoteControlClientCompat == null) { + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); + intent.setComponent(mMediaButtonReceiverComponent); + mRemoteControlClientCompat = new RemoteControlClientCompat( + PendingIntent.getBroadcast(this /*context*/, + 0 /*requestCode, ignored*/, intent /*intent*/, 0 /*flags*/)); + RemoteControlHelper.registerRemoteControlClient(mAudioManager, + mRemoteControlClientCompat); + } + + mRemoteControlClientCompat.setPlaybackState( + RemoteControlClient.PLAYSTATE_PLAYING); + + mRemoteControlClientCompat.setTransportControlFlags( + RemoteControlClient.FLAG_KEY_MEDIA_PLAY | + RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | + RemoteControlClient.FLAG_KEY_MEDIA_NEXT | + RemoteControlClient.FLAG_KEY_MEDIA_STOP); + + // Update the remote controls + mRemoteControlClientCompat.editMetadata(true) + .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, playingItem.getArtist()) + .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, playingItem.getAlbum()) + .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, playingItem.getTitle()) + .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, + playingItem.getDuration()) + // TODO: fetch real item artwork + .putBitmap( + RemoteControlClientCompat.MetadataEditorCompat.METADATA_KEY_ARTWORK, + mDummyAlbumArt) + .apply(); + // starts preparing the media player in the background. When it's done, it will call // our OnPreparedListener (that is, the onPrepared() method on this class, since we set // the listener to 'this'). @@ -403,14 +498,12 @@ public class MusicService extends Service implements OnCompletionListener, OnPre } /** Called when media player is done playing current song. */ - @Override public void onCompletion(MediaPlayer player) { // The media player finished playing the current song, so we go ahead and start the next. playNextSong(null); } /** Called when media player is done preparing. */ - @Override public void onPrepared(MediaPlayer player) { // The media player is done preparing. That means we can start playing! mState = State.Playing; @@ -449,7 +542,6 @@ public class MusicService extends Service implements OnCompletionListener, OnPre * Called when there's an error playing media. When this happens, the media player goes to * the Error state. We warn the user about the error and reset the media player. */ - @Override public boolean onError(MediaPlayer mp, int what, int extra) { Toast.makeText(getApplicationContext(), "Media player error! Resetting.", Toast.LENGTH_SHORT).show(); @@ -461,7 +553,6 @@ public class MusicService extends Service implements OnCompletionListener, OnPre return true; // true indicates we handled the error } - @Override public void onGainedAudioFocus() { Toast.makeText(getApplicationContext(), "gained audio focus.", Toast.LENGTH_SHORT).show(); mAudioFocus = AudioFocus.Focused; @@ -471,7 +562,6 @@ public class MusicService extends Service implements OnCompletionListener, OnPre configAndStartMediaPlayer(); } - @Override public void onLostAudioFocus(boolean canDuck) { Toast.makeText(getApplicationContext(), "lost audio focus." + (canDuck ? "can duck" : "no duck"), Toast.LENGTH_SHORT).show(); @@ -482,7 +572,6 @@ public class MusicService extends Service implements OnCompletionListener, OnPre configAndStartMediaPlayer(); } - @Override public void onMusicRetrieverPrepared() { // Done retrieving! mState = State.Stopped; diff --git a/samples/RandomMusicPlayer/src/com/example/android/musicplayer/RemoteControlClientCompat.java b/samples/RandomMusicPlayer/src/com/example/android/musicplayer/RemoteControlClientCompat.java new file mode 100644 index 000000000..9541a91d0 --- /dev/null +++ b/samples/RandomMusicPlayer/src/com/example/android/musicplayer/RemoteControlClientCompat.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.musicplayer; + +import android.app.PendingIntent; +import android.graphics.Bitmap; +import android.os.Looper; +import android.util.Log; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * RemoteControlClient enables exposing information meant to be consumed by remote controls capable + * of displaying metadata, artwork and media transport control buttons. A remote control client + * object is associated with a media button event receiver. This event receiver must have been + * previously registered with + * {@link android.media.AudioManager#registerMediaButtonEventReceiver(android.content.ComponentName)} + * before the RemoteControlClient can be registered through + * {@link android.media.AudioManager#registerRemoteControlClient(android.media.RemoteControlClient)}. + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +public class RemoteControlClientCompat { + + private static final String TAG = "RemoteControlCompat"; + + private static Class sRemoteControlClientClass; + + // RCC short for RemoteControlClient + private static Method sRCCEditMetadataMethod; + private static Method sRCCSetPlayStateMethod; + private static Method sRCCSetTransportControlFlags; + + private static boolean sHasRemoteControlAPIs = false; + + static { + try { + ClassLoader classLoader = RemoteControlClientCompat.class.getClassLoader(); + sRemoteControlClientClass = getActualRemoteControlClientClass(classLoader); + // dynamically populate the playstate and flag values in case they change + // in future versions. + for (Field field : RemoteControlClientCompat.class.getFields()) { + try { + Field realField = sRemoteControlClientClass.getField(field.getName()); + Object realValue = realField.get(null); + field.set(null, realValue); + } catch (NoSuchFieldException e) { + Log.w(TAG, "Could not get real field: " + field.getName()); + } catch (IllegalArgumentException e) { + Log.w(TAG, "Error trying to pull field value for: " + field.getName() + + " " + e.getMessage()); + } catch (IllegalAccessException e) { + Log.w(TAG, "Error trying to pull field value for: " + field.getName() + + " " + e.getMessage()); + } + } + + // get the required public methods on RemoteControlClient + sRCCEditMetadataMethod = sRemoteControlClientClass.getMethod("editMetadata", + boolean.class); + sRCCSetPlayStateMethod = sRemoteControlClientClass.getMethod("setPlaybackState", + int.class); + sRCCSetTransportControlFlags = sRemoteControlClientClass.getMethod( + "setTransportControlFlags", int.class); + + sHasRemoteControlAPIs = true; + } catch (ClassNotFoundException e) { + // Silently fail when running on an OS before ICS. + } catch (NoSuchMethodException e) { + // Silently fail when running on an OS before ICS. + } catch (IllegalArgumentException e) { + // Silently fail when running on an OS before ICS. + } catch (SecurityException e) { + // Silently fail when running on an OS before ICS. + } + } + + public static Class getActualRemoteControlClientClass(ClassLoader classLoader) + throws ClassNotFoundException { + return classLoader.loadClass("android.media.RemoteControlClient"); + } + + private Object mActualRemoteControlClient; + + public RemoteControlClientCompat(PendingIntent pendingIntent) { + if (!sHasRemoteControlAPIs) { + return; + } + try { + mActualRemoteControlClient = + sRemoteControlClientClass.getConstructor(PendingIntent.class) + .newInstance(pendingIntent); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public RemoteControlClientCompat(PendingIntent pendingIntent, Looper looper) { + if (!sHasRemoteControlAPIs) { + return; + } + + try { + mActualRemoteControlClient = + sRemoteControlClientClass.getConstructor(PendingIntent.class, Looper.class) + .newInstance(pendingIntent, looper); + } catch (Exception e) { + Log.e(TAG, "Error creating new instance of " + sRemoteControlClientClass.getName(), e); + } + } + + /** + * Class used to modify metadata in a {@link android.media.RemoteControlClient} object. Use + * {@link android.media.RemoteControlClient#editMetadata(boolean)} to create an instance of an + * editor, on which you set the metadata for the RemoteControlClient instance. Once all the + * information has been set, use {@link #apply()} to make it the new metadata that should be + * displayed for the associated client. Once the metadata has been "applied", you cannot reuse + * this instance of the MetadataEditor. + */ + public class MetadataEditorCompat { + + private Method mPutStringMethod; + private Method mPutBitmapMethod; + private Method mPutLongMethod; + private Method mClearMethod; + private Method mApplyMethod; + + private Object mActualMetadataEditor; + + /** + * The metadata key for the content artwork / album art. + */ + public final static int METADATA_KEY_ARTWORK = 100; + + private MetadataEditorCompat(Object actualMetadataEditor) { + if (sHasRemoteControlAPIs && actualMetadataEditor == null) { + throw new IllegalArgumentException("Remote Control API's exist, " + + "should not be given a null MetadataEditor"); + } + if (sHasRemoteControlAPIs) { + Class metadataEditorClass = actualMetadataEditor.getClass(); + + try { + mPutStringMethod = metadataEditorClass.getMethod("putString", + int.class, String.class); + mPutBitmapMethod = metadataEditorClass.getMethod("putBitmap", + int.class, Bitmap.class); + mPutLongMethod = metadataEditorClass.getMethod("putLong", + int.class, long.class); + mClearMethod = metadataEditorClass.getMethod("clear", new Class[]{}); + mApplyMethod = metadataEditorClass.getMethod("apply", new Class[]{}); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + mActualMetadataEditor = actualMetadataEditor; + } + + /** + * Adds textual information to be displayed. + * Note that none of the information added after {@link #apply()} has been called, + * will be displayed. + * @param key The identifier of a the metadata field to set. Valid values are + * {@link android.media.MediaMetadataRetriever#METADATA_KEY_ALBUM}, + * {@link android.media.MediaMetadataRetriever#METADATA_KEY_ALBUMARTIST}, + * {@link android.media.MediaMetadataRetriever#METADATA_KEY_TITLE}, + * {@link android.media.MediaMetadataRetriever#METADATA_KEY_ARTIST}, + * {@link android.media.MediaMetadataRetriever#METADATA_KEY_AUTHOR}, + * {@link android.media.MediaMetadataRetriever#METADATA_KEY_COMPILATION}, + * {@link android.media.MediaMetadataRetriever#METADATA_KEY_COMPOSER}, + * {@link android.media.MediaMetadataRetriever#METADATA_KEY_DATE}, + * {@link android.media.MediaMetadataRetriever#METADATA_KEY_GENRE}, + * {@link android.media.MediaMetadataRetriever#METADATA_KEY_TITLE}, + * {@link android.media.MediaMetadataRetriever#METADATA_KEY_WRITER}. + * @param value The text for the given key, or {@code null} to signify there is no valid + * information for the field. + * @return Returns a reference to the same MetadataEditor object, so you can chain put + * calls together. + */ + public MetadataEditorCompat putString(int key, String value) { + if (sHasRemoteControlAPIs) { + try { + mPutStringMethod.invoke(mActualMetadataEditor, key, value); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + return this; + } + + /** + * Sets the album / artwork picture to be displayed on the remote control. + * @param key the identifier of the bitmap to set. The only valid value is + * {@link #METADATA_KEY_ARTWORK} + * @param bitmap The bitmap for the artwork, or null if there isn't any. + * @return Returns a reference to the same MetadataEditor object, so you can chain put + * calls together. + * @throws IllegalArgumentException + * @see android.graphics.Bitmap + */ + public MetadataEditorCompat putBitmap(int key, Bitmap bitmap) { + if (sHasRemoteControlAPIs) { + try { + mPutBitmapMethod.invoke(mActualMetadataEditor, key, bitmap); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + return this; + } + + /** + * Adds numerical information to be displayed. + * Note that none of the information added after {@link #apply()} has been called, + * will be displayed. + * @param key the identifier of a the metadata field to set. Valid values are + * {@link android.media.MediaMetadataRetriever#METADATA_KEY_CD_TRACK_NUMBER}, + * {@link android.media.MediaMetadataRetriever#METADATA_KEY_DISC_NUMBER}, + * {@link android.media.MediaMetadataRetriever#METADATA_KEY_DURATION} (with a value + * expressed in milliseconds), + * {@link android.media.MediaMetadataRetriever#METADATA_KEY_YEAR}. + * @param value The long value for the given key + * @return Returns a reference to the same MetadataEditor object, so you can chain put + * calls together. + * @throws IllegalArgumentException + */ + public MetadataEditorCompat putLong(int key, long value) { + if (sHasRemoteControlAPIs) { + try { + mPutLongMethod.invoke(mActualMetadataEditor, key, value); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + return this; + } + + /** + * Clears all the metadata that has been set since the MetadataEditor instance was + * created with {@link android.media.RemoteControlClient#editMetadata(boolean)}. + */ + public void clear() { + if (sHasRemoteControlAPIs) { + try { + mClearMethod.invoke(mActualMetadataEditor, (Object[]) null); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + } + + /** + * Associates all the metadata that has been set since the MetadataEditor instance was + * created with {@link android.media.RemoteControlClient#editMetadata(boolean)}, or since + * {@link #clear()} was called, with the RemoteControlClient. Once "applied", this + * MetadataEditor cannot be reused to edit the RemoteControlClient's metadata. + */ + public void apply() { + if (sHasRemoteControlAPIs) { + try { + mApplyMethod.invoke(mActualMetadataEditor, (Object[]) null); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + } + } + + /** + * Creates a {@link android.media.RemoteControlClient.MetadataEditor}. + * @param startEmpty Set to false if you want the MetadataEditor to contain the metadata that + * was previously applied to the RemoteControlClient, or true if it is to be created empty. + * @return a new MetadataEditor instance. + */ + public MetadataEditorCompat editMetadata(boolean startEmpty) { + Object metadataEditor; + if (sHasRemoteControlAPIs) { + try { + metadataEditor = sRCCEditMetadataMethod.invoke(mActualRemoteControlClient, + startEmpty); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + metadataEditor = null; + } + return new MetadataEditorCompat(metadataEditor); + } + + /** + * Sets the current playback state. + * @param state The current playback state, one of the following values: + * {@link android.media.RemoteControlClient#PLAYSTATE_STOPPED}, + * {@link android.media.RemoteControlClient#PLAYSTATE_PAUSED}, + * {@link android.media.RemoteControlClient#PLAYSTATE_PLAYING}, + * {@link android.media.RemoteControlClient#PLAYSTATE_FAST_FORWARDING}, + * {@link android.media.RemoteControlClient#PLAYSTATE_REWINDING}, + * {@link android.media.RemoteControlClient#PLAYSTATE_SKIPPING_FORWARDS}, + * {@link android.media.RemoteControlClient#PLAYSTATE_SKIPPING_BACKWARDS}, + * {@link android.media.RemoteControlClient#PLAYSTATE_BUFFERING}, + * {@link android.media.RemoteControlClient#PLAYSTATE_ERROR}. + */ + public void setPlaybackState(int state) { + if (sHasRemoteControlAPIs) { + try { + sRCCSetPlayStateMethod.invoke(mActualRemoteControlClient, state); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + /** + * Sets the flags for the media transport control buttons that this client supports. + * @param transportControlFlags A combination of the following flags: + * {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PREVIOUS}, + * {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_REWIND}, + * {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY}, + * {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY_PAUSE}, + * {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PAUSE}, + * {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_STOP}, + * {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_FAST_FORWARD}, + * {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_NEXT} + */ + public void setTransportControlFlags(int transportControlFlags) { + if (sHasRemoteControlAPIs) { + try { + sRCCSetTransportControlFlags.invoke(mActualRemoteControlClient, + transportControlFlags); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + public final Object getActualRemoteControlClientObject() { + return mActualRemoteControlClient; + } +} diff --git a/samples/RandomMusicPlayer/src/com/example/android/musicplayer/RemoteControlHelper.java b/samples/RandomMusicPlayer/src/com/example/android/musicplayer/RemoteControlHelper.java new file mode 100644 index 000000000..714d3c524 --- /dev/null +++ b/samples/RandomMusicPlayer/src/com/example/android/musicplayer/RemoteControlHelper.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.musicplayer; + +import android.media.AudioManager; +import android.util.Log; + +import java.lang.reflect.Method; + +/** + * Contains methods to handle registering/unregistering remote control clients. These methods only + * run on ICS devices. On previous devices, all methods are no-ops. + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class RemoteControlHelper { + private static final String TAG = "RemoteControlHelper"; + + private static boolean sHasRemoteControlAPIs = false; + + private static Method sRegisterRemoteControlClientMethod; + private static Method sUnregisterRemoteControlClientMethod; + + static { + try { + ClassLoader classLoader = RemoteControlHelper.class.getClassLoader(); + Class sRemoteControlClientClass = + RemoteControlClientCompat.getActualRemoteControlClientClass(classLoader); + sRegisterRemoteControlClientMethod = AudioManager.class.getMethod( + "registerRemoteControlClient", new Class[]{sRemoteControlClientClass}); + sUnregisterRemoteControlClientMethod = AudioManager.class.getMethod( + "unregisterRemoteControlClient", new Class[]{sRemoteControlClientClass}); + sHasRemoteControlAPIs = true; + } catch (ClassNotFoundException e) { + // Silently fail when running on an OS before ICS. + } catch (NoSuchMethodException e) { + // Silently fail when running on an OS before ICS. + } catch (IllegalArgumentException e) { + // Silently fail when running on an OS before ICS. + } catch (SecurityException e) { + // Silently fail when running on an OS before ICS. + } + } + + public static void registerRemoteControlClient(AudioManager audioManager, + RemoteControlClientCompat remoteControlClient) { + if (!sHasRemoteControlAPIs) { + return; + } + + try { + sRegisterRemoteControlClientMethod.invoke(audioManager, + remoteControlClient.getActualRemoteControlClientObject()); + } catch (Exception e) { + Log.e(TAG, e.getMessage(), e); + } + } + + + public static void unregisterRemoteControlClient(AudioManager audioManager, + RemoteControlClientCompat remoteControlClient) { + if (!sHasRemoteControlAPIs) { + return; + } + + try { + sUnregisterRemoteControlClientMethod.invoke(audioManager, + remoteControlClient.getActualRemoteControlClientObject()); + } catch (Exception e) { + Log.e(TAG, e.getMessage(), e); + } + } +} + |
