diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 6a99972..8b1adf1 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -65,6 +65,13 @@
+
+
+
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ecfdd95..d52c120 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -29,6 +29,9 @@
Please select a route
Wifi needs to be connected for playback.
Do you want to enable it now?
+
+
+ Local Device
Album Art
diff --git a/src/com/github/nutomic/controldlna/localroute/Controller.java b/src/com/github/nutomic/controldlna/localroute/Controller.java
new file mode 100644
index 0000000..efa2208
--- /dev/null
+++ b/src/com/github/nutomic/controldlna/localroute/Controller.java
@@ -0,0 +1,197 @@
+/*
+Copyright (c) 2013, Felix Ableitner
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.nutomic.controldlna.localroute;
+
+import java.io.IOException;
+
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaItemStatus;
+import android.support.v7.media.MediaRouteProvider;
+import android.support.v7.media.MediaRouter.ControlRequestCallback;
+import android.util.Log;
+
+/**
+ * Receives control intents through media route and executes them on a MediaPlayer.
+ *
+ * @author felix
+ *
+ */
+public class Controller extends MediaRouteProvider.RouteController implements
+ MediaPlayer.OnCompletionListener, MediaPlayer.OnPreparedListener {
+
+ private static final String TAG = "Controller";
+
+ private Context mContext;
+
+ private AudioManager mAudio;
+
+ AudioManager.OnAudioFocusChangeListener mFocusListener;
+
+ private final String mRouteId;
+
+ private String mItemId;
+
+ private int mState;
+
+ private MediaPlayer mPlayer = new MediaPlayer();
+
+ public Controller(String routeId, Context context) {
+ mContext = context;
+ mRouteId = routeId;
+ mAudio = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+ mPlayer.setOnPreparedListener(this);
+ }
+
+ @Override
+ public void onRelease() {
+ mPlayer.release();
+ }
+
+ @Override
+ public void onSelect() {
+ mAudio.requestAudioFocus(mFocusListener, AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN);
+ }
+
+ @Override
+ public void onUnselect() {
+ mAudio.abandonAudioFocus(mFocusListener);
+ }
+
+ @Override
+ public void onSetVolume(int volume) {
+ mAudio.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0);
+ }
+
+ @Override
+ public void onUpdateVolume(int delta) {
+ int currentVolume = mAudio.getStreamVolume(AudioManager.STREAM_MUSIC);
+ mAudio.setStreamVolume(AudioManager.STREAM_MUSIC, currentVolume + delta, 0);
+ }
+
+ @Override
+ public boolean onControlRequest(Intent intent, ControlRequestCallback callback) {
+ String sessionId = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+ String itemId = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
+ if (intent.getAction().equals(MediaControlIntent.ACTION_PLAY)) {
+ try {
+ mPlayer.reset();
+ mPlayer.setDataSource(mContext, intent.getData());
+ mPlayer.prepareAsync();
+ mItemId = intent.getDataString();
+ mState = MediaItemStatus.PLAYBACK_STATE_BUFFERING;
+ getStatus(mItemId, mRouteId, callback);
+ return true;
+ } catch (IllegalArgumentException e) {
+ mState = MediaItemStatus.PLAYBACK_STATE_ERROR;
+ Log.d(TAG, "Failed to start playback", e);
+ } catch (IOException e) {
+ mState = MediaItemStatus.PLAYBACK_STATE_ERROR;
+ Log.d(TAG, "Failed to start playback", e);
+ }
+ }
+ else if (intent.getAction().equals(MediaControlIntent.ACTION_PAUSE)) {
+ mPlayer.pause();
+ mState = MediaItemStatus.PLAYBACK_STATE_PAUSED;
+ return true;
+ }
+ else if (intent.getAction().equals(MediaControlIntent.ACTION_RESUME)) {
+ mPlayer.start();
+ mState = MediaItemStatus.PLAYBACK_STATE_PLAYING;
+ return true;
+ }
+ else if (intent.getAction().equals(MediaControlIntent.ACTION_STOP)) {
+ mPlayer.stop();
+ mState = MediaItemStatus.PLAYBACK_STATE_CANCELED;
+ return true;
+ }
+ else if (intent.getAction().equals(MediaControlIntent.ACTION_SEEK)) {
+ mPlayer.seekTo((int) intent.getLongExtra(
+ MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0));
+ getStatus(itemId, sessionId, callback);
+ return true;
+ }
+ else if(intent.getAction().equals(MediaControlIntent.ACTION_GET_STATUS)) {
+ getStatus(itemId, sessionId, callback);
+ return true;
+ }
+ return false;
+ }
+
+ private void getStatus(String itemId, String sessionId, ControlRequestCallback callback) {
+ if (callback == null)
+ return;
+
+ Bundle status = null;
+
+ if (mItemId.equals(itemId)) {
+ status = new MediaItemStatus.Builder(mState)
+ .setContentPosition(mPlayer.getCurrentPosition())
+ .setContentDuration(mPlayer.getDuration())
+ .setTimestamp(SystemClock.elapsedRealtime())
+ .build().asBundle();
+
+ status.putString(MediaControlIntent.EXTRA_SESSION_ID, mRouteId);
+ status.putString(MediaControlIntent.EXTRA_ITEM_ID, mItemId);
+ }
+ else
+ status = new MediaItemStatus.Builder(MediaItemStatus.PLAYBACK_STATE_INVALIDATED)
+ .build().asBundle();
+
+ callback.onResult(status);
+ }
+
+ /**
+ * Sets state to finished.
+ *
+ * Note: Do not set the listener before play() is called
+ * (or this will be called immediately).
+ */
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ mState = MediaItemStatus.PLAYBACK_STATE_FINISHED;
+ mPlayer.setOnCompletionListener(null);
+ }
+
+ /**
+ * Starts playback and sets completion listener.
+ */
+ @Override
+ public void onPrepared(MediaPlayer mp) {
+ mPlayer.start();
+ mState = MediaItemStatus.PLAYBACK_STATE_PLAYING;
+ mPlayer.setOnCompletionListener(this);
+ }
+}
diff --git a/src/com/github/nutomic/controldlna/localroute/Provider.java b/src/com/github/nutomic/controldlna/localroute/Provider.java
new file mode 100644
index 0000000..b4171d7
--- /dev/null
+++ b/src/com/github/nutomic/controldlna/localroute/Provider.java
@@ -0,0 +1,105 @@
+/*
+Copyright (c) 2013, Felix Ableitner
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.nutomic.controldlna.localroute;
+
+import java.util.ArrayList;
+
+import android.content.Context;
+import android.content.IntentFilter;
+import android.content.IntentFilter.MalformedMimeTypeException;
+import android.content.res.Resources;
+import android.media.AudioManager;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaRouteDescriptor;
+import android.support.v7.media.MediaRouteProvider;
+import android.support.v7.media.MediaRouteProviderDescriptor;
+import android.support.v7.media.MediaRouter;
+
+import com.github.nutomic.controldlna.R;
+
+/**
+ * MediaRouteProvider that details the local audio route with its
+ * controls to the system.
+ *
+ * @author felix
+ *
+ */
+final class Provider extends MediaRouteProvider {
+
+ private static final String ROUTE_ID = "local_route";
+
+ AudioManager mAudio = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
+
+ private static final ArrayList CONTROL_FILTERS;
+ static {
+ IntentFilter f = new IntentFilter();
+ f.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ f.addAction(MediaControlIntent.ACTION_PLAY);
+ f.addAction(MediaControlIntent.ACTION_PAUSE);
+ f.addAction(MediaControlIntent.ACTION_SEEK);
+ f.addAction(MediaControlIntent.ACTION_STOP);
+ f.addDataScheme("http");
+ f.addDataScheme("https");
+ addDataTypeUnchecked(f, "audio/*");
+
+ CONTROL_FILTERS = new ArrayList();
+ CONTROL_FILTERS.add(f);
+ }
+
+ private static void addDataTypeUnchecked(IntentFilter filter, String type) {
+ try {
+ filter.addDataType(type);
+ } catch (MalformedMimeTypeException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public Provider(Context context) {
+ super(context);
+
+ MediaRouteDescriptor routeDescriptor = new MediaRouteDescriptor.Builder(
+ ROUTE_ID, context.getResources().getString(R.string.local_device))
+ .addControlFilters(CONTROL_FILTERS)
+ .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
+ .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE)
+ .setVolume(mAudio.getStreamMaxVolume(AudioManager.STREAM_MUSIC))
+ .build();
+
+
+ MediaRouteProviderDescriptor providerDescriptor =
+ new MediaRouteProviderDescriptor.Builder()
+ .addRoute(routeDescriptor)
+ .build();
+ setDescriptor(providerDescriptor);
+ }
+
+ @Override
+ public RouteController onCreateRouteController(String routeId) {
+ return new Controller(routeId, getContext());
+ }
+}
\ No newline at end of file
diff --git a/src/com/github/nutomic/controldlna/localroute/ProviderService.java b/src/com/github/nutomic/controldlna/localroute/ProviderService.java
new file mode 100644
index 0000000..53eaa3b
--- /dev/null
+++ b/src/com/github/nutomic/controldlna/localroute/ProviderService.java
@@ -0,0 +1,49 @@
+/*
+Copyright (c) 2013, Felix Ableitner
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.nutomic.controldlna.localroute;
+
+import android.support.v7.media.MediaRouteProvider;
+import android.support.v7.media.MediaRouteProviderService;
+
+public class ProviderService extends MediaRouteProviderService {
+
+ private Provider mProvider;
+
+ @Override
+ public MediaRouteProvider onCreateMediaRouteProvider() {
+ if (mProvider == null)
+ mProvider = new Provider(this);
+ return mProvider;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mProvider = null;
+ }
+}