diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8c88729
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+.classpath
+.project
+bin
+gen
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..7344eb6
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,10 @@
+
+ * If the player is already playing, it will continue to play with the new + * track. If it is not playing, it will move to the next track without + * playing. + *
+ * If the player has loaded last track, the player will be moved to the + * first song in the queue and playback will be stopped, regardless of + * whether or not the player is already playing. + */ + abstract public void skip(); + + /** + * Moves back in the play queue. + *
+ * If the player is already playing, it will continue to play with the new + * track. If it is not playing, it will skip without playing. + *
+ * If the playhead is currently fewer than 2 seconds from the beginning of + * the track, it will skip to the previous track. Otherwise, it will skip to + * the beginning of the currently loaded track. + *
+ * If the player has loaded the first track in the queue, it will skip to + * the beginning of that track regardless of the playhead's position. + */ + abstract public void skipBack(); + + /** + * Removes all songs from the queue. + *
+ * If the player is currently playing, only the currently playing song will + * be preserved. All tracks, including those before and after the current + * track, will be removed from the queue. + *
+ * If the player is not playing, all tracks, including the currently loaded + * one, will be removed. + */ + abstract public void emptyQueue(); + + /** + * Gets the location of the playhead in milliseconds. + * + * @return the current position of the play head in milliseconds + */ + abstract public int getCurrentPosition(); + + /** + * Gets the duration of the currently loaded Song in milliseconds. + * + * @return the duration of the currently loaded Song in milliseconds. + */ + abstract public int getDuration(); + + /** + * Gets the {@linkplain Song} representation of the track that is currently + * loaded in the player. + * + * @return the {@linkplain Song} of the track that is currently loaded in + * the player. + */ + abstract public Song nowPlaying(); + + /** + * Checks to see if the player is currently playing back audio. + * + * @return {@code true} if the player is currently playing, {@code false} + * otherwise. + */ + abstract public boolean isPlaying(); + + /** + * Checks to see if the player is currently loading audio. + * + * @return {@code true} if the player is currently loading, {@code false} + * otherwise. + */ + abstract public boolean isLoading(); + + /** + * Gets the state of the currently loaded + * {@linkplain android.media.MediaPlayer MediaPlayer}, represented as an + * int. + * + * @return One of: {@link MediaPlayerWithState#IDLE IDLE}, + * {@link MediaPlayerWithState#INITIALIZED INITIALIZED}, + * {@link MediaPlayerWithState#PREPARING PREPARING}, + * {@link MediaPlayerWithState#PREPARED PREPARED}, + * {@link MediaPlayerWithState#STARTED STARTED}, + * {@link MediaPlayerWithState#PAUSED PAUSED}, + * {@link MediaPlayerWithState#PLAYBACK_COMPLETED + * PLAYBACK_COMPLETED}, {@link MediaPlayerWithState#STOPPED STOPPED} + * , {@link MediaPlayerWithState#END END}, or + * {@link MediaPlayerWithState#ERROR ERROR} + */ + abstract public int getState(); + + /** + * Sets the visible buttons for plugins. It is up to the implementation of + * the plugin to honor these settings. + * + * @see {@link android.media.RemoteControlClient#setTransportControlFlags(int) } + * @param transportControlFlags + */ + abstract public void setTransportControlFlags(int transportControlFlags); + + /** + * Returns the number of items in the queue. + * + * @return the number of items currently in the queue. + */ + abstract public int getQueueLength(); + + /** + * Returns the number of clips in the queue which are at least partially + * behind the playhead. + *
+ * If the currently enqueued track is stopped at the beginning of the track, + * it is not considered in this calculation. If the player is paused or + * playing or the playhead is partially over the "now playing" track, it + * will be considered as part of this calculation. + * + * @return the number of clips in the queue with are at least partially + * behind the playhead. + */ + abstract public int getQueuePosition(); + + /** + * Removes the element at {@code position} from the play queue. + * + * @param position + * the (non-zero-indexed) position of the item in the queue to + * remove. + * @return true if the operation was successful, false if there are fewer + * than {@code position} items in the play queue. + */ + abstract public boolean removeFromQueue(int position); + + /** + * Constructs an {@linkplain Intent} which will start the appropriate + * {@linkplain PlayerHaterService} as configured in the project's + * AndroidManifest.xml file. + * + * @param context + * @return An {@link Intent} which will start the correct service. + * @throws IllegalArgumentException + * if there is no appropriate service configured in + * AndroidManifest.xml + */ + public static Intent buildServiceIntent(Context context) { + Intent intent = new Intent("org.prx.playerhater.SERVICE"); + intent.setPackage(context.getPackageName()); + Config.attachToIntent(intent); + + if (context.getPackageManager().queryIntentServices(intent, 0).size() == 0) { + intent = new Intent(context, PlaybackService.class); + Config.attachToIntent(intent); + if (context.getPackageManager().queryIntentServices(intent, 0) + .size() == 0) { + IllegalArgumentException e = new IllegalArgumentException( + "No usable service found."); + String tag = context.getPackageName() + "/PlayerHater"; + String message = "Please define your Playback Service. For help, refer to: https://github.com/PRX/PlayerHater/wiki/Setting-Up-Your-Manifest"; + Log.e(tag, message, e); + throw e; + } + } + + return intent; + } +} diff --git a/src/org/prx/playerhater/PlayerHaterPlugin.java b/src/org/prx/playerhater/PlayerHaterPlugin.java new file mode 100644 index 0000000..368808e --- /dev/null +++ b/src/org/prx/playerhater/PlayerHaterPlugin.java @@ -0,0 +1,317 @@ +/******************************************************************************* + * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange + * + * 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 org.prx.playerhater; + +import org.prx.playerhater.PlayerHater; +import org.prx.playerhater.Song; + +import android.app.PendingIntent; +import android.content.Context; +import android.net.Uri; + +/** + * An interface that plugins for PlayerHater must implement. + *
+ * Plugins must implement a default constructor that takes no arguments. + *
+ * Most implementations should consider extending {@linkplain AbstractPlugin} + * instead of implementing this interface. + * + * @since 2.0.0 + * @version 2.1.0 + * + * @author Chris Rhoden + */ +public interface PlayerHaterPlugin { + + /** + * Called when the plugin can start doing work against PlayerHater. + * + * @param context + * A {@link Context} that the plugin can use to interact with the + * application. This will either be the + * {@link ApplicationContext} associated with the + * {@link Application} running the plugin, or it will be the + * context that is bound when this plugin was attached using + * {@link BoundPlayerHater#setBoundPlugin(PlayerHaterPlugin)} + * @param playerHater + * A instance of one of the subclasses of {@link PlayerHater}. + * This is the plugin's handle to PlayerHater, and it should not + * attempt to access it in any other way. + *
+ * If this plugin is loaded in the standard way, this will be an + * instance of {@link ServicePlayerHater}. Otherwise, it will be + * a {@link BoundPlayerHater} or a {@link BinderPlayerHater}, + * depending on the current status of the Service. + */ + void onPlayerHaterLoaded(Context context, PlayerHater playerHater); + + /** + * Called when the song that is loaded into the player has changed. + *
+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *
+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + * + * @param song + * The Song which is now loaded into the player. + */ + void onSongChanged(Song song); + + /** + * Called when the song that was loaded into the player has completed. + *
+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *
+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + * + * @param song + * The Song that has finished playing. + * @param reason + * The reason that the song reached the end. One of + * {@link PlayerHater#SKIP_BUTTON } or + * {@link PlayerHater#TRACK_END } + */ + void onSongFinished(Song song, int reason); + + /** + * Called when the duration of the currently loaded track has changed. + * Typically called whenever {@link PlayerHaterPlugin#onSongChanged(Song)} + * has been called, but provided as a convenience for plugins that do not + * care about other metdata. + *
+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *
+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + * + * @param duration + * The new duration of the currently loaded Song in milliseconds. + */ + void onDurationChanged(int duration); + + /** + * Called when the player has started preparing or buffering the Song loaded + * into it. + *
+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *
+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + */ + void onAudioLoading(); + + /** + * Called when playback has been paused. + *
+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *
+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + */ + void onAudioPaused(); + + /** + * Called when playback has been resumed after being paused. + *
+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *
+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + */ + void onAudioResumed(); + + /** + * Called when audio begins playback for the first time. + *
+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *
+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + */ + void onAudioStarted(); + + /** + * Called when the audio has been stopped without the possibility of + * resuming. + *
+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *
+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + */ + void onAudioStopped(); + + /** + * Called when the title of the currently playing track has changed. + *
+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *
+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + * + * @param title + * The new title of the currently playing track. + */ + void onTitleChanged(String title); + + /** + * Called when the artist of the currently playing track has changed. + *
+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *
+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + * + * @param artist + * The new artist of the currently playing track. + */ + void onArtistChanged(String artist); + + /** + * Called when the album art of the currently playing track has changed. + *
+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *
+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + * + * @param url + * The URI of the new album art for the currently playing track. + */ + void onAlbumArtChanged(Uri url); + + /** + * Called when there is a song "on deck" to play next. + *
+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *
+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + * + * @param nextTrack + * The next song which will be played. + */ + void onNextSongAvailable(Song nextTrack); + + /** + * Called when there is no song "on deck" to play next. + *
+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *
+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + */ + void onNextSongUnavailable(); + + /** + * Called when the requested Transport Control Flags have changed. + *
+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *
+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + * + * @see {@link android.media.RemoteControlClient#setTransportControlFlags(int)} + * + * @param transportControlFlags + * A bitmask representing the transport control flags requested + */ + void onTransportControlFlagsChanged(int transportControlFlags); + + /** + * Called when the requested pending intent for resuming the host + * application has changed. + *
+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *
+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + * + * @param intent + * A pending intent that the plugin should use if it wants to + * resume the hosting application. + */ + void onIntentActivityChanged(PendingIntent intent); + + /** + * Called after one or more state-change callbacks have completed. This + * method is guaranteed, by default, to run on the UI thread. + */ + void onChangesComplete(); +} diff --git a/src/org/prx/playerhater/Song.java b/src/org/prx/playerhater/Song.java new file mode 100644 index 0000000..e90415a --- /dev/null +++ b/src/org/prx/playerhater/Song.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange + * + * 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 org.prx.playerhater; + +import android.net.Uri; +import android.os.Bundle; + +/** + * An interface which can be used for playback in {@linkplain PlayerHater}. This + * interface is primarily for use by {@linkplain PlayerHaterPlugin}s, which use + * the metadata provided to display notifications, widgets, and lock screen + * controls. + * + * @see PlayerHater#play(Song) + * @version 3.0.0 + * @author Chris Rhoden + * @since 2.0.0 + */ +public interface Song { + + /** + * An accessor for the title attribute of a {@linkplain Song} playable + * entity in PlayerHater + * + * @return The title of the {@linkplain Song} + */ + String getTitle(); + + /** + * An accessor for the artist attribute of a {@linkplain Song} playable + * entity in PlayerHater + * + * @return The name of the artist performing the {@linkplain Song} + */ + String getArtist(); + + /** + * An accessor for the album attribute of a {@linkplain Song} playable + * entity in PlayerHater + * + * @return The name of the album on which the {@linkplain Song} is performed + */ + String getAlbumTitle(); + + /** + * An accessor for the the album art attribute of a {@linkplain Song} + * playable entity in PlayerHater + * + * @return The Uri representing the album art for this {@linkplain Song} + */ + Uri getAlbumArt(); + + /** + * @see android.media.MediaPlayer#setDataSource(android.content.Context, + * Uri) + * @return A Uri which resolves to the {@linkplain Song}'s file, which can + * be played using Android's + * {@linkplain android.media.MediaPlayer#setDataSource(android.content.Context, Uri) + * MediaPlayer} class. + *
+ * Ex. + *
+ * {@code http://www.example.com/track.mp3 } + *
+ * {@code content://com.example.app/clips/21 } + */ + Uri getUri(); + + /** + * @return A Bundle whose meaning is user-defined. This is to enable easy + * inter-process communication of additional data about Songs. + *
+ * PlayerHater will not do anything with this.
+ */
+ Bundle getExtra();
+}
diff --git a/src/org/prx/playerhater/broadcast/HeadphoneButtonGestureHelper.java b/src/org/prx/playerhater/broadcast/HeadphoneButtonGestureHelper.java
new file mode 100644
index 0000000..353d549
--- /dev/null
+++ b/src/org/prx/playerhater/broadcast/HeadphoneButtonGestureHelper.java
@@ -0,0 +1,87 @@
+/*******************************************************************************
+ * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange
+ *
+ * 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 org.prx.playerhater.broadcast;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.view.KeyEvent;
+
+class HeadphoneButtonGestureHelper {
+
+ private static final int BUTTON_PRESSED = 0;
+ private static final int PLAY_PAUSE = 1;
+ private static final int NEXT = 2;
+ private static final int PREV = 3;
+ private static final int MILISECONDS_DELAY = 250;
+ public static final String TAG = "GESTURES";
+
+
+ private long mLastEventTime = 0;
+ private int mCurrentAction = 1;
+ private static Context lastContext;
+
+ private final Handler mHandler = new ButtonHandler(this);
+ private RemoteControlButtonReceiver mMediaButtonReceiver;
+
+ public void onHeadsetButtonPressed(long eventTime, Context context) {
+ if (eventTime - mLastEventTime <= MILISECONDS_DELAY + 100) {
+ mCurrentAction += 1;
+ if (mCurrentAction > 3) {
+ mCurrentAction = 1;
+ }
+ mHandler.removeMessages(BUTTON_PRESSED);
+ }
+ lastContext = context;
+ mLastEventTime = eventTime;
+ mHandler.sendEmptyMessageDelayed(BUTTON_PRESSED, MILISECONDS_DELAY);
+ }
+
+ public void setReceiver(RemoteControlButtonReceiver receiver) {
+ mMediaButtonReceiver = receiver;
+ }
+
+ private static class ButtonHandler extends Handler {
+
+ private final HeadphoneButtonGestureHelper mButtonGestureHelper;
+
+ private ButtonHandler(HeadphoneButtonGestureHelper ctx) {
+ mButtonGestureHelper = ctx;
+ }
+
+ @Override
+ public void dispatchMessage(Message message) {
+ switch (mButtonGestureHelper.mCurrentAction) {
+
+ case PLAY_PAUSE:
+ mButtonGestureHelper.mMediaButtonReceiver
+ .onRemoteControlButtonPressed(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, lastContext);
+ break;
+ case NEXT:
+ mButtonGestureHelper.mMediaButtonReceiver
+ .onRemoteControlButtonPressed(KeyEvent.KEYCODE_MEDIA_NEXT, lastContext);
+ break;
+ case PREV:
+ mButtonGestureHelper.mMediaButtonReceiver
+ .onRemoteControlButtonPressed(KeyEvent.KEYCODE_MEDIA_PREVIOUS, lastContext);
+ break;
+ }
+ mButtonGestureHelper.mLastEventTime = 0;
+ mButtonGestureHelper.mCurrentAction = 1;
+ }
+ }
+
+}
diff --git a/src/org/prx/playerhater/broadcast/Receiver.java b/src/org/prx/playerhater/broadcast/Receiver.java
new file mode 100644
index 0000000..8bde716
--- /dev/null
+++ b/src/org/prx/playerhater/broadcast/Receiver.java
@@ -0,0 +1,107 @@
+package org.prx.playerhater.broadcast;
+
+import org.prx.playerhater.PlayerHater;
+import org.prx.playerhater.ipc.IPlayerHaterServer;
+
+import android.annotation.SuppressLint;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.view.KeyEvent;
+
+public class Receiver extends BroadcastReceiver implements
+ RemoteControlButtonReceiver {
+
+ public static final String REMOTE_CONTROL_BUTTON = "org.prx.playerhater.REMOTE_CONTROL";
+
+ private static final HeadphoneButtonGestureHelper sGestureHelper = new HeadphoneButtonGestureHelper();
+
+ public Receiver() {
+ super();
+ }
+
+ public Receiver(Context context) {
+ super();
+ sGestureHelper.setReceiver(this);
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_HEADSET_PLUG);
+ filter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
+ filter.addAction(Intent.ACTION_MEDIA_BUTTON);
+ filter.setPriority(10000);
+ context.registerReceiver(this, filter);
+ }
+
+ @Override
+ @SuppressLint("InlinedApi")
+ public void onRemoteControlButtonPressed(int keyCode, Context context) {
+ sendKeyCode(context, keyCode, keyCode != KeyEvent.KEYCODE_MEDIA_PAUSE);
+ }
+
+ @Override
+ @SuppressLint("InlinedApi")
+ public void onReceive(Context context, Intent intent) {
+ int keyCode = -1;
+ if (intent.getAction() != null) {
+ if (intent.getIntExtra("state", 0) == 0) {
+ if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) {
+ keyCode = KeyEvent.KEYCODE_MEDIA_PAUSE;
+ } else if (intent.getAction().equals(
+ AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
+ keyCode = KeyEvent.KEYCODE_MEDIA_PAUSE;
+ }
+ }
+ if (intent.getAction().equals(Intent.ACTION_MEDIA_BUTTON)) {
+ KeyEvent event = (KeyEvent) intent
+ .getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ return;
+ }
+
+ keyCode = event.getKeyCode();
+
+ if (KeyEvent.KEYCODE_HEADSETHOOK == keyCode) {
+ sGestureHelper.onHeadsetButtonPressed(event.getEventTime(),
+ context);
+ }
+ }
+
+ if (keyCode != -1) {
+ boolean autoStart = keyCode != KeyEvent.KEYCODE_MEDIA_PAUSE
+ && keyCode != KeyEvent.KEYCODE_MEDIA_STOP;
+ sendKeyCode(context, keyCode, autoStart);
+ }
+ }
+ }
+
+ private IPlayerHaterServer getService(Context context) {
+ Intent intent = PlayerHater.buildServiceIntent(context);
+ return IPlayerHaterServer.Stub
+ .asInterface(peekService(context, intent));
+ }
+
+ private void sendKeyCode(Context context, int keyCode,
+ boolean startIfNecessary) {
+ if (getService(context) != null) {
+ try {
+ getService(context).onRemoteControlButtonPressed(keyCode);
+ } catch (Exception e) {
+ if (startIfNecessary && context != null) {
+ Intent intent = context
+ .getPackageManager()
+ .getLaunchIntentForPackage(context.getPackageName());
+ intent.putExtra(REMOTE_CONTROL_BUTTON, keyCode);
+ context.startActivity(intent);
+ }
+ }
+
+ } else if (startIfNecessary && context != null) {
+ Intent intent = new Intent(PlayerHater.buildServiceIntent(context));
+ intent.putExtra(REMOTE_CONTROL_BUTTON, keyCode);
+ context.startService(intent);
+ }
+ }
+
+}
diff --git a/src/org/prx/playerhater/broadcast/RemoteControlButtonReceiver.java b/src/org/prx/playerhater/broadcast/RemoteControlButtonReceiver.java
new file mode 100644
index 0000000..1b76fa2
--- /dev/null
+++ b/src/org/prx/playerhater/broadcast/RemoteControlButtonReceiver.java
@@ -0,0 +1,22 @@
+/*******************************************************************************
+ * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange
+ *
+ * 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 org.prx.playerhater.broadcast;
+
+import android.content.Context;
+
+interface RemoteControlButtonReceiver {
+ public void onRemoteControlButtonPressed(int keyCode, Context context);
+}
diff --git a/src/org/prx/playerhater/ipc/Client.java b/src/org/prx/playerhater/ipc/Client.java
new file mode 100644
index 0000000..4fd8cf0
--- /dev/null
+++ b/src/org/prx/playerhater/ipc/Client.java
@@ -0,0 +1,187 @@
+package org.prx.playerhater.ipc;
+
+import org.prx.playerhater.PlayerHater;
+import org.prx.playerhater.PlayerHaterPlugin;
+import org.prx.playerhater.Song;
+import org.prx.playerhater.songs.SongHost;
+import org.prx.playerhater.util.Log;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.net.Uri;
+import android.os.RemoteException;
+
+public class Client implements PlayerHaterPlugin {
+
+ private static final String CLIENT_ERROR = "Client has gone away...";
+
+ private final IPlayerHaterClient mClient;
+
+ public Client(IPlayerHaterClient client) {
+ mClient = client;
+ }
+
+ @Override
+ public void onPlayerHaterLoaded(Context context, PlayerHater playerHater) {
+ }
+
+ @Override
+ public void onSongChanged(Song song) {
+ try {
+ mClient.onSongChanged(SongHost.getTag(song));
+ } catch (RemoteException e) {
+ Log.e(CLIENT_ERROR, e);
+ throw new IllegalStateException(CLIENT_ERROR, e);
+ }
+ }
+
+ @Override
+ public void onSongFinished(Song song, int reason) {
+ try {
+ mClient.onSongFinished(SongHost.getTag(song), reason);
+ } catch (RemoteException e) {
+ Log.e(CLIENT_ERROR, e);
+ throw new IllegalStateException(CLIENT_ERROR, e);
+ }
+ }
+
+ @Override
+ public void onDurationChanged(int duration) {
+ try {
+ mClient.onDurationChanged(duration);
+ } catch (RemoteException e) {
+ Log.e(CLIENT_ERROR, e);
+ throw new IllegalStateException(CLIENT_ERROR, e);
+ }
+ }
+
+ @Override
+ public void onAudioLoading() {
+ try {
+ mClient.onAudioLoading();
+ } catch (RemoteException e) {
+ Log.e(CLIENT_ERROR, e);
+ throw new IllegalStateException(CLIENT_ERROR, e);
+ }
+ }
+
+ @Override
+ public void onAudioPaused() {
+ try {
+ mClient.onAudioPaused();
+ } catch (RemoteException e) {
+ Log.e(CLIENT_ERROR, e);
+ throw new IllegalStateException(CLIENT_ERROR, e);
+ }
+ }
+
+ @Override
+ public void onAudioResumed() {
+ try {
+ mClient.onAudioResumed();
+ } catch (RemoteException e) {
+ Log.e(CLIENT_ERROR, e);
+ throw new IllegalStateException(CLIENT_ERROR, e);
+ }
+ }
+
+ @Override
+ public void onAudioStarted() {
+ try {
+ mClient.onAudioStarted();
+ } catch (RemoteException e) {
+ Log.e(CLIENT_ERROR, e);
+ throw new IllegalStateException(CLIENT_ERROR, e);
+ }
+ }
+
+ @Override
+ public void onAudioStopped() {
+ try {
+ mClient.onAudioStopped();
+ } catch (RemoteException e) {
+ Log.e(CLIENT_ERROR, e);
+ throw new IllegalStateException(CLIENT_ERROR, e);
+ }
+ }
+
+ @Override
+ public void onTitleChanged(String title) {
+ try {
+ mClient.onTitleChanged(title);
+ } catch (RemoteException e) {
+ Log.e(CLIENT_ERROR, e);
+ throw new IllegalStateException(CLIENT_ERROR, e);
+ }
+ }
+
+ @Override
+ public void onArtistChanged(String artist) {
+ try {
+ mClient.onArtistChanged(artist);
+ } catch (RemoteException e) {
+ Log.e(CLIENT_ERROR, e);
+ throw new IllegalStateException(CLIENT_ERROR, e);
+ }
+ }
+
+ @Override
+ public void onAlbumArtChanged(Uri url) {
+ try {
+ mClient.onAlbumArtChanged(url);
+ } catch (RemoteException e) {
+ Log.e(CLIENT_ERROR, e);
+ throw new IllegalStateException(CLIENT_ERROR, e);
+ }
+ }
+
+ @Override
+ public void onNextSongAvailable(Song nextTrack) {
+ try {
+ mClient.onNextSongAvailable(SongHost.getTag(nextTrack));
+ } catch (RemoteException e) {
+ Log.e(CLIENT_ERROR, e);
+ throw new IllegalStateException(CLIENT_ERROR, e);
+ }
+ }
+
+ @Override
+ public void onNextSongUnavailable() {
+ try {
+ mClient.onNextSongUnavailable();
+ } catch (RemoteException e) {
+ Log.e(CLIENT_ERROR, e);
+ throw new IllegalStateException(CLIENT_ERROR, e);
+ }
+ }
+
+ @Override
+ public void onTransportControlFlagsChanged(int transportControlFlags) {
+ try {
+ mClient.onTransportControlFlagsChanged(transportControlFlags);
+ } catch (RemoteException e) {
+ Log.e(CLIENT_ERROR, e);
+ throw new IllegalStateException(CLIENT_ERROR, e);
+ }
+ }
+
+ @Override
+ public void onIntentActivityChanged(PendingIntent intent) {
+ try {
+ mClient.onIntentActivityChanged(intent);
+ } catch (RemoteException e) {
+ Log.e(CLIENT_ERROR, e);
+ throw new IllegalStateException(CLIENT_ERROR, e);
+ }
+ }
+
+ @Override
+ public void onChangesComplete() {
+ try {
+ mClient.onChangesComplete();
+ } catch (RemoteException e) {
+ Log.e(CLIENT_ERROR, e);
+ throw new IllegalStateException(CLIENT_ERROR, e);
+ }
+ }
+}
diff --git a/src/org/prx/playerhater/ipc/IPlayerHaterClient.aidl b/src/org/prx/playerhater/ipc/IPlayerHaterClient.aidl
new file mode 100644
index 0000000..e3d126e
--- /dev/null
+++ b/src/org/prx/playerhater/ipc/IPlayerHaterClient.aidl
@@ -0,0 +1,38 @@
+package org.prx.playerhater.ipc;
+
+import android.net.Uri;
+import android.app.PendingIntent;
+
+interface IPlayerHaterClient {
+
+ /**
+ * Plugin Methods
+ */
+ void onSongChanged(int songTag);
+ void onSongFinished(int songTag, int reason);
+ void onDurationChanged(int duration);
+ void onAudioLoading();
+ void onAudioPaused();
+ void onAudioResumed();
+ void onAudioStarted();
+ void onAudioStopped();
+ void onTitleChanged(String title);
+ void onArtistChanged(String artist);
+ void onAlbumTitleChanged(String albumTitle);
+ void onAlbumArtChanged(in Uri uri);
+ void onTransportControlFlagsChanged(int transportControlFlags);
+ void onNextSongAvailable(int songTag);
+ void onNextSongUnavailable();
+ void onChangesComplete();
+ void onIntentActivityChanged(in PendingIntent intent);
+
+ /**
+ * SongHost Methods
+ */
+ String getSongTitle(int songTag);
+ String getSongArtist(int songTag);
+ String getSongAlbumTitle(int songTag);
+ Uri getSongAlbumArt(int songTag);
+ Uri getSongUri(int songTag);
+ Bundle getSongExtra(int songTag);
+}
\ No newline at end of file
diff --git a/src/org/prx/playerhater/ipc/IPlayerHaterServer.aidl b/src/org/prx/playerhater/ipc/IPlayerHaterServer.aidl
new file mode 100644
index 0000000..a45f08c
--- /dev/null
+++ b/src/org/prx/playerhater/ipc/IPlayerHaterServer.aidl
@@ -0,0 +1,53 @@
+package org.prx.playerhater.ipc;
+
+import android.net.Uri;
+import org.prx.playerhater.ipc.IPlayerHaterClient;
+import android.app.Notification;
+
+interface IPlayerHaterServer {
+
+ /**
+ * Server-specific methods
+ */
+ void setClient(IPlayerHaterClient client);
+ void onRemoteControlButtonPressed(int keyCode);
+ void startForeground(int notificationNu, in Notification notification);
+ void stopForeground(boolean fact);
+ void duck();
+ void unduck();
+
+ /**
+ * PlayerHater Methods
+ */
+ boolean pause();
+ boolean stop();
+ boolean resume();
+ boolean playAtTime(int startTime);
+ boolean play(int songTag, int startTime);
+ boolean seekTo(int startTime);
+ int enqueue(int songTag);
+ boolean skipTo(int position);
+ void skip();
+ void skipBack();
+ void emptyQueue();
+ int getCurrentPosition();
+ int getDuration();
+ int nowPlaying();
+ boolean isPlaying();
+ boolean isLoading();
+ int getState();
+ void setTransportControlFlags(int transportControlFlags);
+ int getQueueLength();
+ int getQueuePosition();
+ boolean removeFromQueue(int position);
+
+ /**
+ * SongHost Methods
+ */
+ String getSongTitle(int songTag);
+ String getSongArtist(int songTag);
+ String getSongAlbumTitle(int songTag);
+ Uri getSongAlbumArt(int songTag);
+ Uri getSongUri(int songTag);
+ Bundle getSongExtra(int songTag);
+}
\ No newline at end of file
diff --git a/src/org/prx/playerhater/ipc/Server.java b/src/org/prx/playerhater/ipc/Server.java
new file mode 100644
index 0000000..5264255
--- /dev/null
+++ b/src/org/prx/playerhater/ipc/Server.java
@@ -0,0 +1,240 @@
+package org.prx.playerhater.ipc;
+
+import org.prx.playerhater.PlayerHater;
+import org.prx.playerhater.Song;
+import org.prx.playerhater.songs.SongHost;
+import org.prx.playerhater.util.Log;
+
+import android.os.RemoteException;
+
+public class Server extends PlayerHater {
+
+ private static final String SERVER_ERROR = "Server has gone away...";
+
+ private final IPlayerHaterServer mServer;
+
+ public Server(IPlayerHaterServer server) {
+ mServer = server;
+ }
+
+ @Override
+ public boolean pause() {
+ try {
+ return mServer.pause();
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public boolean stop() {
+ try {
+ return mServer.stop();
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public boolean play() {
+ try {
+ return mServer.resume();
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public boolean play(int startTime) {
+ try {
+ return mServer.playAtTime(startTime);
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public boolean play(Song song) {
+ try {
+ return mServer.play(SongHost.getTag(song), 0);
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public boolean play(Song song, int startTime) {
+ try {
+ return mServer.play(SongHost.getTag(song), 0);
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public boolean seekTo(int startTime) {
+ try {
+ return mServer.seekTo(startTime);
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public int enqueue(Song song) {
+ try {
+ return mServer.enqueue(SongHost.getTag(song));
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public boolean skipTo(int position) {
+ try {
+ return mServer.skipTo(position);
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public void skip() {
+ try {
+ mServer.skip();
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public void skipBack() {
+ try {
+ mServer.skipBack();
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public void emptyQueue() {
+ try {
+ mServer.emptyQueue();
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public int getCurrentPosition() {
+ try {
+ return mServer.getCurrentPosition();
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public int getDuration() {
+ try {
+ return mServer.getDuration();
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public Song nowPlaying() {
+ try {
+ return SongHost.getSong(mServer.nowPlaying());
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public boolean isPlaying() {
+ try {
+ return mServer.isPlaying();
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public boolean isLoading() {
+ try {
+ return mServer.isLoading();
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public int getState() {
+ try {
+ return mServer.getState();
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public void setTransportControlFlags(int transportControlFlags) {
+ try {
+ mServer.setTransportControlFlags(transportControlFlags);
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public int getQueueLength() {
+ try {
+ return mServer.getQueueLength();
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public int getQueuePosition() {
+ try {
+ return mServer.getQueuePosition();
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ public boolean removeFromQueue(int position) {
+ try {
+ return mServer.removeFromQueue(position);
+ } catch (RemoteException e) {
+ Log.e(SERVER_ERROR, e);
+ throw new IllegalStateException(SERVER_ERROR, e);
+ }
+ }
+
+}
diff --git a/src/org/prx/playerhater/songs/RemoteSong.java b/src/org/prx/playerhater/songs/RemoteSong.java
new file mode 100644
index 0000000..adfb023
--- /dev/null
+++ b/src/org/prx/playerhater/songs/RemoteSong.java
@@ -0,0 +1,201 @@
+/*******************************************************************************
+ * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange
+ *
+ * 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 org.prx.playerhater.songs;
+
+import org.prx.playerhater.Song;
+import org.prx.playerhater.ipc.IPlayerHaterClient;
+import org.prx.playerhater.ipc.IPlayerHaterServer;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+
+public class RemoteSong implements Song {
+
+ private static SongHost sSongHost;
+
+ public static interface SongHost {
+ Uri getSongAlbumArt(int tag) throws RemoteException;
+
+ Uri getSongUri(int tag) throws RemoteException;
+
+ String getSongAlbumTitle(int tag) throws RemoteException;
+
+ String getSongTitle(int tag) throws RemoteException;
+
+ String getSongArtist(int tag) throws RemoteException;
+
+ Bundle getSongExtra(int tag) throws RemoteException;
+ }
+
+ private static final class ClientSongHost implements SongHost {
+ private final IPlayerHaterClient mClient;
+
+ private ClientSongHost(IPlayerHaterClient client) {
+ mClient = client;
+ }
+
+ @Override
+ public Uri getSongAlbumArt(int tag) throws RemoteException {
+ return mClient.getSongAlbumArt(tag);
+ }
+
+ @Override
+ public Uri getSongUri(int tag) throws RemoteException {
+ return mClient.getSongUri(tag);
+ }
+
+ @Override
+ public String getSongTitle(int tag) throws RemoteException {
+ return mClient.getSongTitle(tag);
+ }
+
+ @Override
+ public String getSongArtist(int tag) throws RemoteException {
+ return mClient.getSongArtist(tag);
+ }
+
+ @Override
+ public Bundle getSongExtra(int tag) throws RemoteException {
+ return mClient.getSongExtra(tag);
+ }
+
+ @Override
+ public String getSongAlbumTitle(int tag) throws RemoteException {
+ return mClient.getSongAlbumTitle(tag);
+ }
+ }
+
+ private static class ServerSongHost implements SongHost {
+ private final IPlayerHaterServer mServer;
+
+ private ServerSongHost(IPlayerHaterServer server) {
+ mServer = server;
+ }
+
+ @Override
+ public Uri getSongAlbumArt(int tag) throws RemoteException {
+ return mServer.getSongAlbumArt(tag);
+ }
+
+ @Override
+ public Uri getSongUri(int tag) throws RemoteException {
+ return mServer.getSongUri(tag);
+ }
+
+ @Override
+ public String getSongTitle(int tag) throws RemoteException {
+ return mServer.getSongTitle(tag);
+ }
+
+ @Override
+ public String getSongArtist(int tag) throws RemoteException {
+ return mServer.getSongArtist(tag);
+ }
+
+ @Override
+ public Bundle getSongExtra(int tag) throws RemoteException {
+ return mServer.getSongExtra(tag);
+ }
+
+ @Override
+ public String getSongAlbumTitle(int tag) throws RemoteException {
+ return mServer.getSongAlbumTitle(tag);
+ }
+ }
+
+ public static void setSongHost(SongHost host) {
+ sSongHost = host;
+ }
+
+ public static void setSongHost(IPlayerHaterClient client) {
+ sSongHost = new ClientSongHost(client);
+ }
+
+ public static void setSongHost(IPlayerHaterServer server) {
+ sSongHost = new ServerSongHost(server);
+ }
+
+ private static SongHost getSongHost() {
+ return sSongHost;
+ }
+
+ private final int mTag;
+
+ RemoteSong(int tag) {
+ mTag = tag;
+ }
+
+ @Override
+ public String getTitle() {
+ try {
+ return getSongHost().getSongTitle(mTag);
+ } catch (RemoteException e) {
+ throw new IllegalStateException(
+ "Remote Process has died or become disconnected", e);
+ }
+ }
+
+ @Override
+ public String getArtist() {
+ try {
+ return getSongHost().getSongArtist(mTag);
+ } catch (RemoteException e) {
+ throw new IllegalStateException(
+ "Remote Process has died or become disconnected", e);
+ }
+ }
+
+ @Override
+ public Uri getAlbumArt() {
+ try {
+ return getSongHost().getSongAlbumArt(mTag);
+ } catch (RemoteException e) {
+ throw new IllegalStateException(
+ "Remote Process has died or become disconnected", e);
+ }
+ }
+
+ @Override
+ public Uri getUri() {
+ try {
+ return getSongHost().getSongUri(mTag);
+ } catch (RemoteException e) {
+ throw new IllegalStateException(
+ "Remote Process has died or become disconnected", e);
+ }
+ }
+
+ @Override
+ public Bundle getExtra() {
+ try {
+ return getSongHost().getSongExtra(mTag);
+ } catch (RemoteException e) {
+ throw new IllegalStateException(
+ "Remote Process has died or become disconnected", e);
+ }
+ }
+
+ @Override
+ public String getAlbumTitle() {
+ try {
+ return getSongHost().getSongAlbumTitle(mTag);
+ } catch (RemoteException e) {
+ throw new IllegalStateException(
+ "Remote Process has died or become disconnected", e);
+ }
+ }
+}
diff --git a/src/org/prx/playerhater/songs/SongHost.java b/src/org/prx/playerhater/songs/SongHost.java
new file mode 100644
index 0000000..e172e8c
--- /dev/null
+++ b/src/org/prx/playerhater/songs/SongHost.java
@@ -0,0 +1,39 @@
+package org.prx.playerhater.songs;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.prx.playerhater.Song;
+
+import android.util.SparseArray;
+
+public class SongHost {
+
+ private static final SparseArray