From 5edbd2886fe4f8366d9a6427b6f4acac9e9f9091 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Thu, 26 Sep 2013 18:29:26 +0200 Subject: [PATCH] Added MediaRouter support. --- AndroidManifest.xml | 10 + project.properties | 1 + .../controldlna/gui/RendererFragment.java | 5 +- .../controldlna/mediarouter/Provider.java | 358 ++++++++++++++++++ .../mediarouter/ProviderService.java | 50 +++ .../mediarouter/RemotePlayService.java | 247 ++++++++++++ .../controldlna/upnp/DeviceListener.java | 13 +- .../nutomic/controldlna/upnp/PlayService.java | 53 ++- .../controldlna/upnp/UpnpController.java | 2 +- .../nutomic/controldlna/upnp/UpnpPlayer.java | 10 +- 10 files changed, 719 insertions(+), 30 deletions(-) create mode 100644 src/com/github/nutomic/controldlna/mediarouter/Provider.java create mode 100644 src/com/github/nutomic/controldlna/mediarouter/ProviderService.java create mode 100644 src/com/github/nutomic/controldlna/mediarouter/RemotePlayService.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index f30a9dd..a0eeb53 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -35,6 +35,16 @@ + + + + + + + + diff --git a/project.properties b/project.properties index b2b2557..a62728b 100644 --- a/project.properties +++ b/project.properties @@ -13,3 +13,4 @@ # Project target. target=android-18 android.library.reference.1=../android-support-v7-appcompat +android.library.reference.2=../android-support-v7-mediarouter diff --git a/src/com/github/nutomic/controldlna/gui/RendererFragment.java b/src/com/github/nutomic/controldlna/gui/RendererFragment.java index 24dd7f7..0276181 100644 --- a/src/com/github/nutomic/controldlna/gui/RendererFragment.java +++ b/src/com/github/nutomic/controldlna/gui/RendererFragment.java @@ -65,6 +65,7 @@ import android.widget.SeekBar.OnSeekBarChangeListener; import com.github.nutomic.controldlna.R; import com.github.nutomic.controldlna.gui.MainActivity.OnBackPressedListener; +import com.github.nutomic.controldlna.upnp.UpnpController; import com.github.nutomic.controldlna.upnp.UpnpPlayer; import com.github.nutomic.controldlna.utility.DeviceArrayAdapter; import com.github.nutomic.controldlna.utility.FileArrayAdapter; @@ -146,8 +147,8 @@ public class RendererFragment extends Fragment implements * playback is active. */ private void pollTimePosition() { - Service service = getPlayer() - .getService(mCurrentRenderer, "AVTransport"); + Service service = UpnpController + .getService(mCurrentRenderer, "AVTransport"); getPlayer().execute( new GetPositionInfo(service) { diff --git a/src/com/github/nutomic/controldlna/mediarouter/Provider.java b/src/com/github/nutomic/controldlna/mediarouter/Provider.java new file mode 100644 index 0000000..de21fcc --- /dev/null +++ b/src/com/github/nutomic/controldlna/mediarouter/Provider.java @@ -0,0 +1,358 @@ +/* +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.mediarouter; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map.Entry; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentFilter.MalformedMimeTypeException; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; +import android.support.v7.media.MediaControlIntent; +import android.support.v7.media.MediaRouteDescriptor; +import android.support.v7.media.MediaRouteDiscoveryRequest; +import android.support.v7.media.MediaRouteProvider; +import android.support.v7.media.MediaRouteProviderDescriptor.Builder; +import android.support.v7.media.MediaRouter; +import android.support.v7.media.MediaRouter.ControlRequestCallback; + +/** + * Allows playing to a DLNA renderer from a remote app. + * + * @author Felix Ableitner + */ +final class Provider extends MediaRouteProvider { + + // Device has been added. + // param: Device device + public static final int MSG_RENDERER_ADDED = 1; + // Device has been removed. + // param: int id + public static final int MSG_RENDERER_REMOVED = 2; + + /** + * Allows passing and storing basic information about a device. + */ + static public class Device implements Parcelable { + + public String id; + public String name; + public String description; + public int volume; + public int volumeMax; + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public Device createFromParcel(Parcel in) { + return new Device(in); + } + + public Device[] newArray(int size) { + return new Device[size]; + } + }; + + private Device(Parcel in) { + id = in.readString(); + name = in.readString(); + description = in.readString(); + volume = in.readInt(); + volumeMax = in.readInt(); + } + + public Device(String id, String name, String description, int volume, int volumeMax) { + this.id = id; + this.name = name; + this.description = description; + this.volume = volume; + this.volumeMax = volumeMax; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(name); + dest.writeString(description); + dest.writeInt(volume); + dest.writeInt(volumeMax); + } + + } + + private HashMap mDevices = new HashMap(); + + private Messenger mRemotePlayService; + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + mRemotePlayService = new Messenger(service); + Message msg = Message.obtain(null, RemotePlayService.MSG_OPEN, 0, 0); + msg.replyTo = mListener; + try { + mRemotePlayService.send(msg); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + public void onServiceDisconnected(ComponentName className) { + mRemotePlayService = null; + } + }; + + private static final ArrayList CONTROL_FILTERS; + // Static constructor for 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"); + try { + f.addDataType("video/*"); + f.addDataType("audio/*"); + } catch (MalformedMimeTypeException ex) { + throw new RuntimeException(ex); + } + + CONTROL_FILTERS = new ArrayList(); + CONTROL_FILTERS.add(f); + } + + /** + * Listens for messages about devices. + */ + static private class DeviceListener extends Handler { + + private final WeakReference mService; + + DeviceListener(Provider provider) { + mService = new WeakReference(provider); + } + + @Override + public void handleMessage(Message msg) { + if (mService.get() != null) { + mService.get().handleMessage(msg); + } + } + } + + final Messenger mListener = new Messenger(new DeviceListener(this)); + + public Provider(Context context) { + super(context); + context.bindService( + new Intent(context, RemotePlayService.class), + mConnection, + Context.BIND_AUTO_CREATE + ); + } + + public void close() { + getContext().unbindService(mConnection); + } + + @Override + public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) { + if (request == null) return; + Message msg; + if (request.isActiveScan()) { + msg = Message.obtain(null, RemotePlayService.MSG_OPEN, 0, 0); + msg.replyTo = mListener; + } + else { + msg = Message.obtain(null, RemotePlayService.MSG_CLOSE, 0, 0); + } + try { + if (mRemotePlayService != null) { + mRemotePlayService.send(msg); + } + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + @Override + public RouteController onCreateRouteController(String routeId) { + return new RouteController(routeId); + } + + private void updateRoutes() { + Builder builder = new Builder(); + for (Entry d : mDevices.entrySet()) { + MediaRouteDescriptor routeDescriptor = new MediaRouteDescriptor.Builder( + d.getValue().id, + d.getValue().name) + .setDescription("DLNA Playback") + .addControlFilters(CONTROL_FILTERS) + .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) + .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) + .setVolumeMax(d.getValue().volumeMax) + .setVolume(d.getValue().volume) + .build(); + builder.addRoute(routeDescriptor); + } + setDescriptor(builder.build()); + } + + /** + * Receives and forwards device selections, volume change + * requests and control requests. + */ + private final class RouteController extends MediaRouteProvider.RouteController { + private final String mRouteId; + + public RouteController(String routeId) { + mRouteId = routeId; + } + + @Override + public void onRelease() { + } + + @Override + public void onSelect() { + Message msg = Message.obtain(null, RemotePlayService.MSG_SELECT, 0, 0); + msg.getData().putString("id", mRouteId); + try { + mRemotePlayService.send(msg); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + @Override + public void onUnselect() { + Message msg = Message.obtain(null, RemotePlayService.MSG_UNSELECT, 0, 0); + msg.getData().putString("id", mRouteId); + try { + mRemotePlayService.send(msg); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + @Override + public void onSetVolume(int volume) { + Message msg = Message.obtain(null, RemotePlayService.MSG_SET_VOPLUME, 0, 0); + msg.getData().putInt("volume", volume); + mDevices.get(mRouteId).volume = volume; + try { + mRemotePlayService.send(msg); + } catch (RemoteException e) { + e.printStackTrace(); + } + updateRoutes(); + } + + @Override + public void onUpdateVolume(int delta) { + Message msg = Message.obtain(null, RemotePlayService.MSG_CHANGE_VOLUME, 0, 0); + msg.getData().putInt("delta", delta); + mDevices.get(mRouteId).volume += delta; + try { + mRemotePlayService.send(msg); + } catch (RemoteException e) { + e.printStackTrace(); + } + updateRoutes(); + } + + @Override + public boolean onControlRequest(Intent intent, ControlRequestCallback callback) { + try { + if (intent.getAction().equals(MediaControlIntent.ACTION_PLAY)) { + Message msg = Message.obtain(null, RemotePlayService.MSG_PLAY, 0, 0); + msg.getData().putString("uri", intent.getDataString()); + mRemotePlayService.send(msg); + return true; + } + else if (intent.getAction().equals(MediaControlIntent.ACTION_PAUSE)) { + Message msg = Message.obtain(null, RemotePlayService.MSG_PAUSE, 0, 0); + mRemotePlayService.send(msg); + return true; + } + else if (intent.getAction().equals(MediaControlIntent.ACTION_PLAY)) { + Message msg = Message.obtain(null, RemotePlayService.MSG_STOP, 0, 0); + mRemotePlayService.send(msg); + return true; + } + else if (intent.getAction().equals(MediaControlIntent.ACTION_PLAY)) { + Message msg = Message.obtain(null, RemotePlayService.MSG_PLAY, 0, 0); + msg.getData().putLong("milliseconds", + intent.getIntExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0)); + mRemotePlayService.send(msg); + return true; + } + } catch (RemoteException e) { + e.printStackTrace(); + } + return false; + } + } + + public void handleMessage(Message msg) { + Bundle data = msg.getData(); + switch (msg.what) { + case MSG_RENDERER_ADDED: + msg.getData().setClassLoader(Device.class.getClassLoader()); + Device device = (Device) data.getParcelable("device"); + mDevices.put(device.id, device); + updateRoutes(); + break; + case MSG_RENDERER_REMOVED: + mDevices.remove(data.getString("id")); + updateRoutes(); + break; + } + } + +} \ No newline at end of file diff --git a/src/com/github/nutomic/controldlna/mediarouter/ProviderService.java b/src/com/github/nutomic/controldlna/mediarouter/ProviderService.java new file mode 100644 index 0000000..ad5798f --- /dev/null +++ b/src/com/github/nutomic/controldlna/mediarouter/ProviderService.java @@ -0,0 +1,50 @@ +/* +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.mediarouter; + +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.close(); + mProvider = null; + } +} diff --git a/src/com/github/nutomic/controldlna/mediarouter/RemotePlayService.java b/src/com/github/nutomic/controldlna/mediarouter/RemotePlayService.java new file mode 100644 index 0000000..676bd99 --- /dev/null +++ b/src/com/github/nutomic/controldlna/mediarouter/RemotePlayService.java @@ -0,0 +1,247 @@ +/* +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.mediarouter; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import org.teleal.cling.model.action.ActionInvocation; +import org.teleal.cling.model.message.UpnpResponse; +import org.teleal.cling.model.meta.Device; +import org.teleal.cling.model.meta.RemoteDevice; +import org.teleal.cling.model.meta.StateVariableAllowedValueRange; +import org.teleal.cling.support.model.Res; +import org.teleal.cling.support.model.item.Item; +import org.teleal.cling.support.renderingcontrol.callback.GetVolume; + +import android.app.Service; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.util.Log; + +import com.github.nutomic.controldlna.upnp.DeviceListener.DeviceListenerCallback; +import com.github.nutomic.controldlna.upnp.UpnpController; +import com.github.nutomic.controldlna.upnp.UpnpPlayer; + +/** + * Allows UPNP playback from within different apps by providing a proxy interface. + * + * @author Felix Ableitner + * + */ +public class RemotePlayService extends Service implements DeviceListenerCallback { + + private static final String TAG = "RemotePlayService"; + + // Start device discovery. + public static final int MSG_OPEN = 1; + // Stop device discovery. + public static final int MSG_CLOSE = 2; + // Select renderer. + // param: string device_id + public static final int MSG_SELECT = 3; + // Unselect renderer. + // param: int device_id + public static final int MSG_UNSELECT = 4; + // Set absolute volume. + // param: int volume + public static final int MSG_SET_VOPLUME = 5; + // Change volume relative to current volume. + // param: int delta + public static final int MSG_CHANGE_VOLUME = 6; + // Play from uri. + // param: String uri + public static final int MSG_PLAY = 7; + // Pause playback. + public static final int MSG_PAUSE = 8; + // Stop playback. + public static final int MSG_STOP = 9; + // Seek to absolute time in ms. + // param: long milliseconds + public static final int MSG_SEEK = 10; + + /** + * Handles incoming messages from clients. + */ + private static class IncomingHandler extends Handler { + + private final WeakReference mService; + + IncomingHandler(RemotePlayService service) { + mService = new WeakReference(service); + } + + @Override + public void handleMessage(Message msg) { + if (mService.get() != null) { + mService.get().handleMessage(msg); + } + } + } + + private final Messenger mMessenger = new Messenger(new IncomingHandler(this)); + + private final UpnpPlayer mPlayer = new UpnpPlayer(); + + private Messenger mListener; + + private HashMap> mDevices = new HashMap>(); + + @Override + public IBinder onBind(Intent itnent) { + return mMessenger.getBinder(); + } + + @Override + public void onCreate() { + super.onCreate(); + mPlayer.open(this); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mPlayer.getPlayService().getRenderer() != null) { + mPlayer.getPlayService().stop(); + } + mPlayer.close(this); + } + + @Override + public void deviceAdded(final Device device) { + if (device.getType().getType().equals("MediaRenderer") && + device instanceof RemoteDevice) { + mDevices.put(device.getIdentity().getUdn().toString(), device); + + + + mPlayer.execute( + new GetVolume(UpnpController.getService(device, "RenderingControl")) { + + @SuppressWarnings("rawtypes") + @Override + public void failure(ActionInvocation invocation, + UpnpResponse operation, String defaultMessage) { + Log.w(TAG, "Failed to get current Volume: " + defaultMessage); + } + + @SuppressWarnings("rawtypes") + @Override + public void received(ActionInvocation invocation, int currentVolume) { + int maxVolume = 100; + if (UpnpPlayer.getService(device, "RenderingControl").getStateVariable("Volume") != null) { + StateVariableAllowedValueRange volumeRange = + UpnpPlayer.getService(device, "RenderingControl").getStateVariable("Volume") + .getTypeDetails().getAllowedValueRange(); + maxVolume = (int) volumeRange.getMaximum(); + } + + Message msg = Message.obtain(null, Provider.MSG_RENDERER_ADDED, 0, 0); + msg.getData().putParcelable("device", new Provider.Device( + device.getIdentity().getUdn().toString(), + device.getDisplayString(), + device.getDetails().getManufacturerDetails().getManufacturer(), + currentVolume, + maxVolume)); + try { + mListener.send(msg); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + }); + } + } + + @Override + public void deviceRemoved(Device device) { + if (device.getType().getType().equals("MediaRenderer") && + device instanceof RemoteDevice) { + Message msg = Message.obtain(null, Provider.MSG_RENDERER_REMOVED, 0, 0); + + String udn = device.getIdentity().getUdn().toString(); + msg.getData().putString("id", udn); + mDevices.remove(udn); + try { + mListener.send(msg); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + } + + public void handleMessage(Message msg) { + Bundle data = msg.getData(); + switch (msg.what) { + case MSG_OPEN: + mPlayer.getDeviceListener().addCallback(this); + mListener = msg.replyTo; + break; + case MSG_CLOSE: + break; + case MSG_SELECT: + mPlayer.selectRenderer(mDevices.get(data.getString("id"))); + break; + case MSG_UNSELECT: + mPlayer.getPlayService().stop(); + break; + case MSG_SET_VOPLUME: + mPlayer.setVolume(data.getInt("volume")); + break; + case MSG_CHANGE_VOLUME: + mPlayer.changeVolume(data.getInt("delta")); + break; + case MSG_PLAY: + mPlayer.getPlayService().setShowNotification(false); + Item item = new Item(); + item.addResource(new Res()); + item.getFirstResource().setValue(data.getString("uri")); + List playlist = new ArrayList(); + playlist.add(item); + mPlayer.getPlayService().setPlaylist(playlist, 0); + break; + case MSG_PAUSE: + mPlayer.getPlayService().pause(); + break; + case MSG_STOP: + mPlayer.getPlayService().stop(); + break; + case MSG_SEEK: + mPlayer.seek((int) data.getLong("milliseconds") / 1000); + break; + } + } + +} diff --git a/src/com/github/nutomic/controldlna/upnp/DeviceListener.java b/src/com/github/nutomic/controldlna/upnp/DeviceListener.java index 89a7f21..131cf8b 100644 --- a/src/com/github/nutomic/controldlna/upnp/DeviceListener.java +++ b/src/com/github/nutomic/controldlna/upnp/DeviceListener.java @@ -64,9 +64,9 @@ public class DeviceListener implements RegistryListener { public void addCallback(DeviceListenerCallback callback) { mListeners.add(callback); - for (Device d : mDevices) + for (Device d : mDevices) { callback.deviceAdded(d); - + } } public void removeCallback(DeviceListenerCallback callback) { @@ -75,14 +75,16 @@ public class DeviceListener implements RegistryListener { private void deviceAdded(Device device) { mDevices.add(device); - for (DeviceListenerCallback l : mListeners) + for (DeviceListenerCallback l : mListeners) { l.deviceAdded(device); + } } private void deviceRemoved(Device device) { mDevices.remove(device); - for (DeviceListenerCallback l : mListeners) + for (DeviceListenerCallback l : mListeners) { l.deviceRemoved(device); + } } @Override @@ -126,8 +128,7 @@ public class DeviceListener implements RegistryListener { @Override public void remoteDeviceUpdated(Registry registry, RemoteDevice device) { deviceRemoved(device); - deviceAdded(device); - + deviceAdded(device); } } diff --git a/src/com/github/nutomic/controldlna/upnp/PlayService.java b/src/com/github/nutomic/controldlna/upnp/PlayService.java index fde1685..11b29a5 100644 --- a/src/com/github/nutomic/controldlna/upnp/PlayService.java +++ b/src/com/github/nutomic/controldlna/upnp/PlayService.java @@ -85,6 +85,8 @@ public class PlayService extends Service { private static final int NOTIFICATION_ID = 1; + private boolean mShowNotification = true; + private final PlayServiceBinder mBinder = new PlayServiceBinder(this); /** @@ -205,9 +207,14 @@ public class PlayService extends Service { Log.w(TAG, "Metadata serialization failed", e); metadata = "NO METADATA"; } - mUpnpService.getControlPoint().execute(new SetAVTransportURI( + setTransportUri(metadata, + mPlaylist.get(track).getFirstResource().getValue()); + } + + private void setTransportUri(String metadata, final String uri) { + mUpnpService.getControlPoint().execute(new SetAVTransportURI( mAvTransportService, - mPlaylist.get(track).getFirstResource().getValue(), metadata) { + uri, metadata) { @SuppressWarnings("rawtypes") @Override public void failure(ActionInvocation invocation, @@ -224,17 +231,16 @@ public class PlayService extends Service { } private void updateNotification() { - new CreateNotificationTask().execute(mPlaylist.get(mCurrentTrack) - .getFirstPropertyValue(DIDLObject.Property.UPNP.ALBUM_ART_URI.class)); + if (mShowNotification) { + new CreateNotificationTask().execute(mPlaylist.get(mCurrentTrack) + .getFirstPropertyValue(DIDLObject.Property.UPNP.ALBUM_ART_URI.class)); + } } /** * Sends 'play' signal to current renderer. */ public void play() { - if (mPlaylist.size() == 0) - return; - updateNotification(); mUpnpService.getControlPoint().execute( new Play(mAvTransportService) { @@ -262,19 +268,30 @@ public class PlayService extends Service { UpnpResponse operation, String defaultMessage) { Log.w(TAG, "Pause failed, trying stop: " + defaultMessage); // Sometimes stop works even though pause does not. - mUpnpService.getControlPoint().execute( - new Stop(mAvTransportService) { - - @Override - public void failure(ActionInvocation invocation, - UpnpResponse operation, String defaultMessage) { - Log.w(TAG, "Stop failed: " + defaultMessage); - } - }); + stop(); } }); } + /** + * Sends 'stop' signal to current renderer. + */ + public void stop() { + mManuallyStopped.set(true); + + mUpnpService.getControlPoint().execute( + new Stop(mAvTransportService) { + + @SuppressWarnings("rawtypes") + @Override + public void failure( ActionInvocation invocation, + UpnpResponse operation, String defaultMessage) { + Log.w(TAG, "Stop failed: " + defaultMessage); + } + }); + + } + public void setRenderer(Device renderer) { if (mSubscriptionCallback != null) mSubscriptionCallback.end(); @@ -389,5 +406,9 @@ public class PlayService extends Service { public int getCurrentTrack() { return mCurrentTrack; } + + public void setShowNotification(boolean value) { + mShowNotification = value; + } } diff --git a/src/com/github/nutomic/controldlna/upnp/UpnpController.java b/src/com/github/nutomic/controldlna/upnp/UpnpController.java index ea5a7b8..9864415 100644 --- a/src/com/github/nutomic/controldlna/upnp/UpnpController.java +++ b/src/com/github/nutomic/controldlna/upnp/UpnpController.java @@ -110,7 +110,7 @@ public class UpnpController { /** * Returns a device service by name for direct queries. */ - public Service getService(Device device, String name) { + public static Service getService(Device device, String name) { return device.findService( new ServiceType("schemas-upnp-org", name)); } diff --git a/src/com/github/nutomic/controldlna/upnp/UpnpPlayer.java b/src/com/github/nutomic/controldlna/upnp/UpnpPlayer.java index 1f56d6d..6885373 100644 --- a/src/com/github/nutomic/controldlna/upnp/UpnpPlayer.java +++ b/src/com/github/nutomic/controldlna/upnp/UpnpPlayer.java @@ -83,7 +83,7 @@ public class UpnpPlayer extends UpnpController { new Intent(context, PlayService.class), mPlayServiceConnection, Context.BIND_AUTO_CREATE - ); + ); } @Override @@ -110,7 +110,6 @@ public class UpnpPlayer extends UpnpController { if (newVolume < mMinVolume) newVolume = mMinVolume; mCurrentVolume = newVolume; - Log.d(TAG, "volume: " + Integer.toString(mCurrentVolume)); mUpnpService.getControlPoint().execute( new SetVolume(getService("RenderingControl"), newVolume) { @@ -145,7 +144,7 @@ public class UpnpPlayer extends UpnpController { public void selectRenderer(Device renderer) { mPlayService.getService().setRenderer(renderer); - if (getService("RenderingControl").getStateVariable("Volume") != null) { + if (getService("RenderingControl").getStateVariable("Volume") != null) { StateVariableAllowedValueRange volumeRange = getService("RenderingControl").getStateVariable("Volume") .getTypeDetails().getAllowedValueRange(); @@ -156,7 +155,6 @@ public class UpnpPlayer extends UpnpController { else { mMinVolume = 0; mMaxVolume = 100; - mVolumeStep = 1; } mUpnpService.getControlPoint().execute( @@ -203,7 +201,9 @@ public class UpnpPlayer extends UpnpController { * Returns the service that handles actual playback. */ public PlayService getPlayService() { - return mPlayService.getService(); + return (mPlayService != null) + ? mPlayService.getService() + : null; } }