From c8db107f0bbbe4e9e64d8c6791d2ca1c3a874b53 Mon Sep 17 00:00:00 2001 From: Joshua Bahnsen Date: Thu, 10 Jan 2013 09:23:19 -0700 Subject: [PATCH] Added lockscreen music controls --- AndroidManifest.xml | 17 +- .../androidapp/activity/DownloadActivity.java | 16 +- .../activity/SubsonicTabActivity.java | 61 ++- .../androidapp/service/DownloadService.java | 2 + .../service/DownloadServiceImpl.java | 112 +++++- .../DownloadServiceLifecycleSupport.java | 2 +- .../util/RemoteControlClientCompat.java | 353 ++++++++++++++++++ .../androidapp/util/RemoteControlHelper.java | 86 +++++ 8 files changed, 634 insertions(+), 15 deletions(-) create mode 100644 src/net/sourceforge/subsonic/androidapp/util/RemoteControlClientCompat.java create mode 100644 src/net/sourceforge/subsonic/androidapp/util/RemoteControlHelper.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index e95ca63b..eca35f2b 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2,7 +2,7 @@ + a:versionName="3.9.9.6" a:installLocation="auto"> @@ -12,7 +12,7 @@ - + @@ -85,9 +85,16 @@ - - + + + + + + + + + + diff --git a/src/net/sourceforge/subsonic/androidapp/activity/DownloadActivity.java b/src/net/sourceforge/subsonic/androidapp/activity/DownloadActivity.java index e01248af..e42e9e53 100644 --- a/src/net/sourceforge/subsonic/androidapp/activity/DownloadActivity.java +++ b/src/net/sourceforge/subsonic/androidapp/activity/DownloadActivity.java @@ -46,7 +46,6 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.view.Window; import android.view.WindowManager; import android.view.animation.AnimationUtils; import android.widget.AdapterView; @@ -363,7 +362,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi visualizerButton.setTypeface(typeface); jukeboxButton.setTypeface(typeface); } - + @Override protected void onResume() { super.onResume(); @@ -511,10 +510,15 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi savePlaylist.setEnabled(savePlaylistEnabled); savePlaylist.setVisible(savePlaylistEnabled); MenuItem screenOption = menu.findItem(R.id.menu_screen_on_off); - if (getDownloadService().getKeepScreenOn()) { - screenOption.setTitle(R.string.download_menu_screen_off); - } else { - screenOption.setTitle(R.string.download_menu_screen_on); + + DownloadService downloadService = getDownloadService(); + + if (downloadService != null) { + if (getDownloadService().getKeepScreenOn()) { + screenOption.setTitle(R.string.download_menu_screen_off); + } else { + screenOption.setTitle(R.string.download_menu_screen_on); + } } return super.onPrepareOptionsMenu(menu); } diff --git a/src/net/sourceforge/subsonic/androidapp/activity/SubsonicTabActivity.java b/src/net/sourceforge/subsonic/androidapp/activity/SubsonicTabActivity.java index fc411305..5209d572 100644 --- a/src/net/sourceforge/subsonic/androidapp/activity/SubsonicTabActivity.java +++ b/src/net/sourceforge/subsonic/androidapp/activity/SubsonicTabActivity.java @@ -71,6 +71,8 @@ public class SubsonicTabActivity extends Activity implements OnClickListener{ private View playlistButton; private View nowPlayingButton; + //private boolean shortPress = false; + private GestureDetector gestureDetector; View.OnTouchListener gestureListener; @@ -209,7 +211,6 @@ public class SubsonicTabActivity extends Activity implements OnClickListener{ getImageLoader().clear(); } - @Override public boolean onKeyDown(int keyCode, KeyEvent event) { boolean isVolumeDown = keyCode == KeyEvent.KEYCODE_VOLUME_DOWN; @@ -221,6 +222,19 @@ public class SubsonicTabActivity extends Activity implements OnClickListener{ getDownloadService().adjustJukeboxVolume(isVolumeUp); return true; } + +// if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) { +// if(event.getAction() == KeyEvent.ACTION_DOWN){ +// event.startTracking(); +// +// if(event.getRepeatCount() == 0){ +// shortPress = true; +// } +// +// return true; +// } +// } + return super.onKeyDown(keyCode, event); } @@ -487,7 +501,50 @@ public class SubsonicTabActivity extends Activity implements OnClickListener{ return false; } - } + +// @Override +// public boolean onKeyLongPress(int keyCode, KeyEvent event) { +// DownloadService service = getDownloadService(); +// int current = service.getCurrentPlayingIndex(); +// +// switch(keyCode){ +// case KeyEvent.KEYCODE_VOLUME_UP: +// shortPress = false; +// +// if (current == -1) { +// service.play(0); +// } else { +// current++; +// service.play(current); +// } +// return true; +// case KeyEvent.KEYCODE_VOLUME_DOWN: +// shortPress = false; +// +// if (current == -1 || current == 0) { +// service.play(0); +// } else { +// current--; +// service.play(current); +// } +// return true; +// } +// +// return super.onKeyLongPress(keyCode, event); +// } +// +// @Override +// public boolean onKeyUp(int keyCode, KeyEvent event) { +// if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { +// shortPress = false; +// return true; +// } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP){ +// shortPress = false; +// return true; +// } +// +// return super.onKeyUp(keyCode, event); +// } } diff --git a/src/net/sourceforge/subsonic/androidapp/service/DownloadService.java b/src/net/sourceforge/subsonic/androidapp/service/DownloadService.java index 50e455ba..397cacd9 100644 --- a/src/net/sourceforge/subsonic/androidapp/service/DownloadService.java +++ b/src/net/sourceforge/subsonic/androidapp/service/DownloadService.java @@ -77,6 +77,8 @@ public interface DownloadService { void next(); void pause(); + + void stop(); void start(); diff --git a/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceImpl.java b/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceImpl.java index 03657634..7566f2fd 100644 --- a/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceImpl.java +++ b/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceImpl.java @@ -21,13 +21,19 @@ package net.sourceforge.subsonic.androidapp.service; import android.app.Notification; 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.media.AudioManager; +import android.media.AudioManager.OnAudioFocusChangeListener; +import android.media.MediaMetadataRetriever; import android.media.MediaPlayer; +import android.media.RemoteControlClient; import android.os.Handler; import android.os.IBinder; import android.os.PowerManager; +import android.util.DisplayMetrics; import android.util.Log; import android.widget.RemoteViews; import net.sourceforge.subsonic.androidapp.R; @@ -37,11 +43,14 @@ import net.sourceforge.subsonic.androidapp.audiofx.VisualizerController; import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; import net.sourceforge.subsonic.androidapp.domain.PlayerState; import net.sourceforge.subsonic.androidapp.domain.RepeatMode; +import net.sourceforge.subsonic.androidapp.receiver.MediaButtonIntentReceiver; import net.sourceforge.subsonic.androidapp.util.CancellableTask; import net.sourceforge.subsonic.androidapp.util.LRUCache; import net.sourceforge.subsonic.androidapp.util.ShufflePlayBuffer; import net.sourceforge.subsonic.androidapp.util.SimpleServiceBinder; import net.sourceforge.subsonic.androidapp.util.Util; +import net.sourceforge.subsonic.androidapp.util.RemoteControlHelper; +import net.sourceforge.subsonic.androidapp.util.RemoteControlClientCompat; import java.io.File; import java.util.ArrayList; @@ -96,6 +105,10 @@ public class DownloadServiceImpl extends Service implements DownloadService { private VisualizerController visualizerController; private boolean showVisualization; private boolean jukeboxEnabled; + + RemoteControlClientCompat remoteControlClientCompat; + AudioManager audioManager; + ComponentName mediaButtonReceiverComponent; static { try { @@ -113,6 +126,16 @@ public class DownloadServiceImpl extends Service implements DownloadService { visualizerAvailable = false; } } + + private OnAudioFocusChangeListener _afChangeListener = new OnAudioFocusChangeListener() { + public void onAudioFocusChange(int focusChange) { + if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { + start(); + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { + stop(); + } + } + }; @Override public void onCreate() { @@ -149,6 +172,9 @@ public class DownloadServiceImpl extends Service implements DownloadService { visualizerController = null; } } + + audioManager = (AudioManager) getSystemService(AUDIO_SERVICE); + mediaButtonReceiverComponent = new ComponentName(this, MediaButtonIntentReceiver.class); PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName()); @@ -549,9 +575,40 @@ public class DownloadServiceImpl extends Service implements DownloadService { handleError(x); } } + + @Override + public synchronized void stop() { + AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + am.abandonAudioFocus(_afChangeListener); + + try { + if (playerState == STARTED) { + if (jukeboxEnabled) { + jukeboxService.stop(); + } else { + mediaPlayer.pause(); + } + setPlayerState(PAUSED); + } + } catch (Exception x) { + handleError(x); + } + + //seekTo(0); + } @Override public synchronized void start() { + AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + int result = am.requestAudioFocus(_afChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); + + if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) + stop(); + + // grab the media button when we have audio focus + AudioManager audioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE); + audioManager.registerMediaButtonEventReceiver(new ComponentName(this, MediaButtonIntentReceiver.class)); + try { if (jukeboxEnabled) { jukeboxService.start(); @@ -630,6 +687,59 @@ public class DownloadServiceImpl extends Service implements DownloadService { this.playerState = playerState; + if (remoteControlClientCompat == null) { + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); + intent.setComponent(mediaButtonReceiverComponent); + remoteControlClientCompat = new RemoteControlClientCompat(PendingIntent.getBroadcast(this, 0, intent, 0)); + RemoteControlHelper.registerRemoteControlClient(audioManager, remoteControlClientCompat); + } + + switch (playerState) + { + case STARTED: + remoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); + break; + case PAUSED: + remoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); + break; + case IDLE: + case STOPPED: + remoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); + break; + } + + remoteControlClientCompat.setTransportControlFlags( + RemoteControlClient.FLAG_KEY_MEDIA_PLAY | + RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | + RemoteControlClient.FLAG_KEY_MEDIA_NEXT | + RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS | + RemoteControlClient.FLAG_KEY_MEDIA_STOP); + + try { + + String artist = currentPlaying.getSong().getArtist(); + String album = currentPlaying.getSong().getAlbum(); + String title = currentPlaying.getSong().getTitle(); + Integer duration = currentPlaying.getSong().getDuration(); + + MusicService musicService = MusicServiceFactory.getMusicService(this); + DisplayMetrics metrics = this.getResources().getDisplayMetrics(); + int size = Math.min(metrics.widthPixels, metrics.heightPixels); + Bitmap bitmap = musicService.getCoverArt(this, currentPlaying.getSong(), size, true, null); + + // Update the remote controls + remoteControlClientCompat.editMetadata(true) + .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, artist) + .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, title) + .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, album) + .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration) + .putBitmap(RemoteControlClientCompat.MetadataEditorCompat.METADATA_KEY_ARTWORK, bitmap) + .apply(); + } + catch (Exception e) { + // + } + if (Util.isNotificationEnabled(this)) { if (show) { Util.showPlayingNotification(this, this, handler, currentPlaying.getSong(), this.notification, this.playerState); @@ -639,7 +749,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { } else { Util.hidePlayingNotification(this, this, handler); } - + if (playerState == STARTED) { scrobbler.scrobble(this, currentPlaying, false); } else if (playerState == COMPLETED) { diff --git a/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceLifecycleSupport.java b/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceLifecycleSupport.java index d319a76a..fdbc47a2 100644 --- a/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceLifecycleSupport.java +++ b/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceLifecycleSupport.java @@ -73,7 +73,7 @@ public class DownloadServiceLifecycleSupport { } else if (DownloadServiceImpl.CMD_PAUSE.equals(action)) { downloadService.pause(); } else if (DownloadServiceImpl.CMD_STOP.equals(action)) { - downloadService.pause(); + downloadService.stop(); downloadService.seekTo(0); } } diff --git a/src/net/sourceforge/subsonic/androidapp/util/RemoteControlClientCompat.java b/src/net/sourceforge/subsonic/androidapp/util/RemoteControlClientCompat.java new file mode 100644 index 00000000..152367e2 --- /dev/null +++ b/src/net/sourceforge/subsonic/androidapp/util/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 net.sourceforge.subsonic.androidapp.util; + +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; + } +} \ No newline at end of file diff --git a/src/net/sourceforge/subsonic/androidapp/util/RemoteControlHelper.java b/src/net/sourceforge/subsonic/androidapp/util/RemoteControlHelper.java new file mode 100644 index 00000000..6675862d --- /dev/null +++ b/src/net/sourceforge/subsonic/androidapp/util/RemoteControlHelper.java @@ -0,0 +1,86 @@ +/* + * 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 net.sourceforge.subsonic.androidapp.util; + +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); + } + } +}