From c236173a45a97686183eb12f0e54837a385a49dc Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 23 Sep 2013 02:10:35 +0200 Subject: [PATCH] Extracted UPNP functionality from GUI into seperate classes. --- .../controldlna/DeviceArrayAdapter.java | 73 +------ .../nutomic/controldlna/DeviceListener.java | 106 ++++++++++ .../nutomic/controldlna/FileArrayAdapter.java | 12 ++ .../nutomic/controldlna/LoadImageTask.java | 2 +- .../nutomic/controldlna/RemoteImageView.java | 2 +- .../nutomic/controldlna/UpnpController.java | 103 ++++++++++ .../nutomic/controldlna/UpnpPlayer.java | 192 ++++++++++++++++++ .../nutomic/controldlna/gui/MainActivity.java | 76 ++++--- .../controldlna/gui/RendererFragment.java | 171 +++------------- .../controldlna/gui/ServerFragment.java | 66 ++---- .../controldlna/service/PlayService.java | 14 +- .../service/PlayServiceBinder.java | 6 + 12 files changed, 523 insertions(+), 300 deletions(-) create mode 100644 src/com/github/nutomic/controldlna/DeviceListener.java create mode 100644 src/com/github/nutomic/controldlna/UpnpController.java create mode 100644 src/com/github/nutomic/controldlna/UpnpPlayer.java diff --git a/src/com/github/nutomic/controldlna/DeviceArrayAdapter.java b/src/com/github/nutomic/controldlna/DeviceArrayAdapter.java index e6b2a7e..7f901dd 100644 --- a/src/com/github/nutomic/controldlna/DeviceArrayAdapter.java +++ b/src/com/github/nutomic/controldlna/DeviceArrayAdapter.java @@ -29,16 +29,9 @@ package com.github.nutomic.controldlna; import java.net.URI; import java.net.URISyntaxException; -import java.util.Collection; import org.teleal.cling.model.meta.Device; -import org.teleal.cling.model.meta.LocalDevice; import org.teleal.cling.model.meta.RemoteDevice; -import org.teleal.cling.registry.Registry; -import org.teleal.cling.registry.RegistryListener; - -import com.github.nutomic.controldlna.RemoteImageView; - import android.app.Activity; import android.content.Context; @@ -49,15 +42,17 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; +import com.github.nutomic.controldlna.DeviceListener.DeviceListenerCallback; + /** * Displays the devices that are inserted through the RegistryListener (either * of type RENDERER or SERVER). * - * @author Felix + * @author Felix Ableitner * */ public class DeviceArrayAdapter extends ArrayAdapter> - implements RegistryListener { + implements DeviceListenerCallback { private static final String TAG = "DeviceArrayAdapter"; @@ -105,56 +100,12 @@ public class DeviceArrayAdapter extends ArrayAdapter> } return convertView; } - - @Override - public void afterShutdown() { - } - - @Override - public void beforeShutdown(Registry registry) { - } - - @Override - public void localDeviceAdded(Registry registry, LocalDevice device) { - deviceAdded(device); - } - - @Override - public void localDeviceRemoved(Registry registry, LocalDevice device) { - deviceRemoved(device); - } - - @Override - public void remoteDeviceAdded(Registry registry, RemoteDevice device) { - deviceAdded(device); - } - - @Override - public void remoteDeviceDiscoveryFailed(Registry registry, - RemoteDevice device, Exception exception) { - Log.w(TAG, "Device discovery failed", exception); - } - - @Override - public void remoteDeviceDiscoveryStarted(Registry registry, - RemoteDevice device) { - } - - @Override - public void remoteDeviceRemoved(Registry registry, RemoteDevice device) { - deviceRemoved(device); - } - - @Override - public void remoteDeviceUpdated(Registry registry, RemoteDevice device) { - if (!(device.getType().getType().equals(mDeviceType))) - deviceRemoved(device); - } /** * Adds a new device to the list if its type equals mDeviceType. */ - private void deviceAdded(final Device device) { + @Override + public void deviceAdded(final Device device) { mActivity.runOnUiThread(new Runnable() { @Override @@ -168,7 +119,8 @@ public class DeviceArrayAdapter extends ArrayAdapter> /** * Removes the device from the list (if it is an element). */ - private void deviceRemoved(final Device device) { + @Override + public void deviceRemoved(final Device device) { mActivity.runOnUiThread(new Runnable() { @Override @@ -178,13 +130,4 @@ public class DeviceArrayAdapter extends ArrayAdapter> } }); } - - /** - * Replacement for addAll, which is not implemented on lower API levels. - */ - @SuppressWarnings("rawtypes") - public void add(Collection collection) { - for (Device d : collection) - add(d); - } } diff --git a/src/com/github/nutomic/controldlna/DeviceListener.java b/src/com/github/nutomic/controldlna/DeviceListener.java new file mode 100644 index 0000000..f2911c1 --- /dev/null +++ b/src/com/github/nutomic/controldlna/DeviceListener.java @@ -0,0 +1,106 @@ +package com.github.nutomic.controldlna; + +import java.util.ArrayList; + +import org.teleal.cling.model.meta.Device; +import org.teleal.cling.model.meta.LocalDevice; +import org.teleal.cling.model.meta.RemoteDevice; +import org.teleal.cling.registry.Registry; +import org.teleal.cling.registry.RegistryListener; + +import android.util.Log; + +/** + * Provides an interface that informs about UPNP devices being added or removed. + * + * @author Felix Ableitner + * + */ +public class DeviceListener implements RegistryListener { + + private static final String TAG = "DeviceListener"; + + /** + * Callbacks may be called from a background thread. + * + * @author Felix Ableitner + * + */ + public interface DeviceListenerCallback { + public void deviceAdded(Device device); + public void deviceRemoved(Device device); + } + + private ArrayList> mDevices = new ArrayList>(); + + private ArrayList mListeners = new ArrayList(); + + public void addCallback(DeviceListenerCallback callback) { + mListeners.add(callback); + for (Device d : mDevices) + callback.deviceAdded(d); + + } + + public void removeCallback(DeviceListenerCallback callback) { + mListeners.remove(callback); + } + + private void deviceAdded(Device device) { + mDevices.add(device); + for (DeviceListenerCallback l : mListeners) + l.deviceAdded(device); + } + + private void deviceRemoved(Device device) { + mDevices.remove(device); + for (DeviceListenerCallback l : mListeners) + l.deviceRemoved(device); + } + + @Override + public void afterShutdown() { + } + + @Override + public void beforeShutdown(Registry registry) { + } + + @Override + public void localDeviceAdded(Registry registry, LocalDevice device) { + deviceAdded(device); + } + + @Override + public void localDeviceRemoved(Registry registry, LocalDevice device) { + deviceRemoved(device); + } + + @Override + public void remoteDeviceAdded(Registry registry, RemoteDevice device) { + deviceAdded(device); + } + + @Override + public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice device, + Exception exception) { + Log.w(TAG, "Remote device discovery failed", exception); + } + + @Override + public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) { + } + + @Override + public void remoteDeviceRemoved(Registry registry, RemoteDevice device) { + deviceRemoved(device); + } + + @Override + public void remoteDeviceUpdated(Registry registry, RemoteDevice device) { + deviceRemoved(device); + deviceAdded(device); + + } + +} diff --git a/src/com/github/nutomic/controldlna/FileArrayAdapter.java b/src/com/github/nutomic/controldlna/FileArrayAdapter.java index 47efbac..13f5422 100644 --- a/src/com/github/nutomic/controldlna/FileArrayAdapter.java +++ b/src/com/github/nutomic/controldlna/FileArrayAdapter.java @@ -42,8 +42,17 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; +/** + * Allows displaying UPNP media server directory contents in a ListView. + * + * @author Felix Ableitner + * + */ public class FileArrayAdapter extends ArrayAdapter { + /** + * Provides sorting of elements by track number. + */ public FileArrayAdapter(Context context) { super(context, R.layout.list_item); sort(new Comparator() { @@ -65,6 +74,9 @@ public class FileArrayAdapter extends ArrayAdapter { }); } + /** + * Returns a view with folder/media title, and artist name (for audio only). + */ @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { diff --git a/src/com/github/nutomic/controldlna/LoadImageTask.java b/src/com/github/nutomic/controldlna/LoadImageTask.java index 3904c53..359d1eb 100644 --- a/src/com/github/nutomic/controldlna/LoadImageTask.java +++ b/src/com/github/nutomic/controldlna/LoadImageTask.java @@ -15,7 +15,7 @@ import android.util.Log; /** * Handles background task of loading a bitmap by URI. * - * @author Felix + * @author Felix Ableitner * */ public class LoadImageTask extends AsyncTask { diff --git a/src/com/github/nutomic/controldlna/RemoteImageView.java b/src/com/github/nutomic/controldlna/RemoteImageView.java index eacc065..a85bc8d 100644 --- a/src/com/github/nutomic/controldlna/RemoteImageView.java +++ b/src/com/github/nutomic/controldlna/RemoteImageView.java @@ -13,7 +13,7 @@ import android.widget.ImageView; /** * ImageView that can directly load from a UPNP URI. * - * @author Felix + * @author Felix Ableitner * */ public class RemoteImageView extends ImageView { diff --git a/src/com/github/nutomic/controldlna/UpnpController.java b/src/com/github/nutomic/controldlna/UpnpController.java new file mode 100644 index 0000000..b80a1d9 --- /dev/null +++ b/src/com/github/nutomic/controldlna/UpnpController.java @@ -0,0 +1,103 @@ +package com.github.nutomic.controldlna; + +import org.teleal.cling.android.AndroidUpnpService; +import org.teleal.cling.android.AndroidUpnpServiceImpl; +import org.teleal.cling.controlpoint.ActionCallback; +import org.teleal.cling.controlpoint.SubscriptionCallback; +import org.teleal.cling.model.meta.Device; +import org.teleal.cling.model.meta.LocalDevice; +import org.teleal.cling.model.meta.RemoteDevice; +import org.teleal.cling.model.meta.Service; +import org.teleal.cling.model.types.ServiceType; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.util.Log; + +/** + * Handles UPNP connection, including device discovery and general queries. + * + * @author Felix Ableitner + * + */ +public class UpnpController { + + private static final String TAG = "DlnaController"; + + private DeviceListener mDeviceListener = new DeviceListener(); + + protected AndroidUpnpService mUpnpService; + + private ServiceConnection mUpnpServiceConnection = new ServiceConnection() { + + /** + * Registers DeviceListener, adds known devices and starts search. + */ + public void onServiceConnected(ComponentName className, IBinder service) { + mUpnpService = (AndroidUpnpService) service; + Log.i(TAG, "Starting device search"); + mUpnpService.getRegistry().addListener(mDeviceListener); + for (Device d : mUpnpService.getControlPoint().getRegistry().getDevices()) { + if (d instanceof LocalDevice) { + mDeviceListener.localDeviceAdded(mUpnpService.getRegistry(), (LocalDevice) d); + } + else { + mDeviceListener.remoteDeviceAdded(mUpnpService.getRegistry(), (RemoteDevice) d); + } + } + mUpnpService.getControlPoint().search(); + } + + public void onServiceDisconnected(ComponentName className) { + mUpnpService = null; + } + }; + + /** + * Opens connection to the Cling UPNP service. + * + * @param context Application context. + */ + public void open(Context context) { + context.bindService( + new Intent(context, AndroidUpnpServiceImpl.class), + mUpnpServiceConnection, + Context.BIND_AUTO_CREATE + ); + } + + /** + * Closes the connection to the Cling UPNP service. + * + * @param context Application context. + */ + public void close(Context context) { + mUpnpService.getRegistry().removeListener(mDeviceListener); + context.unbindService(mUpnpServiceConnection); + } + + + /** + * Returns a device service by name for direct queries. + */ + public Service getService(Device device, String name) { + return device.findService( + new ServiceType("schemas-upnp-org", name)); + } + + public void execute(ActionCallback callback) { + mUpnpService.getControlPoint().execute(callback); + } + + public void execute(SubscriptionCallback callback) { + mUpnpService.getControlPoint().execute(callback); + } + + public DeviceListener getDeviceListener() { + return mDeviceListener; + } + +} diff --git a/src/com/github/nutomic/controldlna/UpnpPlayer.java b/src/com/github/nutomic/controldlna/UpnpPlayer.java new file mode 100644 index 0000000..5599206 --- /dev/null +++ b/src/com/github/nutomic/controldlna/UpnpPlayer.java @@ -0,0 +1,192 @@ +package com.github.nutomic.controldlna; + +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.Service; +import org.teleal.cling.model.meta.StateVariableAllowedValueRange; +import org.teleal.cling.support.avtransport.callback.Seek; +import org.teleal.cling.support.model.SeekMode; +import org.teleal.cling.support.renderingcontrol.callback.GetVolume; +import org.teleal.cling.support.renderingcontrol.callback.SetVolume; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.util.Log; + +import com.github.nutomic.controldlna.service.PlayService; +import com.github.nutomic.controldlna.service.PlayServiceBinder; + +/** + * Handles connection to PlayService and provides methods related to playback. + * + * @author Felix Ableitner + * + */ +public class UpnpPlayer extends UpnpController { + + private static final String TAG = "UpnpPlayer"; + + private PlayServiceBinder mPlayService; + + private long mMinVolume; + + private long mMaxVolume; + + private long mVolumeStep; + + private ServiceConnection mPlayServiceConnection = new ServiceConnection() { + + public void onServiceConnected(ComponentName className, IBinder service) { + mPlayService = (PlayServiceBinder) service; + } + + public void onServiceDisconnected(ComponentName className) { + mPlayService = null; + } + }; + + @Override + public void open(Context context) { + super.open(context); + context.bindService( + new Intent(context, PlayService.class), + mPlayServiceConnection, + Context.BIND_AUTO_CREATE + ); + } + + @Override + public void close(Context context) { + super.close(context); + context.unbindService(mPlayServiceConnection); + } + + /** + * Returns a device service by name for direct queries. + */ + public Service getService(String name) { + return getService(mPlayService.getService().getRenderer(), name); + } + + /** + * Sets an absolute volume. + */ + public void setVolume(long newVolume) { + if (mPlayService.getService().getRenderer() == null) + return; + + if (newVolume > mMaxVolume) newVolume = mMaxVolume; + if (newVolume < mMinVolume) newVolume = mMinVolume; + + mUpnpService.getControlPoint().execute( + new SetVolume(getService("RenderingControl"), newVolume) { + + @SuppressWarnings("rawtypes") + @Override + public void failure(ActionInvocation invocation, + UpnpResponse operation, String defaultMessage) { + Log.d(TAG, "Failed to set new Volume: " + defaultMessage); + } + }); + } + + + /** + * Increases or decreases volume relative to current one. + * + * @param amount Amount to change volume by (negative to lower volume). + */ + private void changeVolume(final long amount) { + if (mPlayService.getService().getRenderer() == null) + return; + + mUpnpService.getControlPoint().execute( + new GetVolume(getService("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) { + setVolume(currentVolume + amount); + } + }); + } + + /** + * Increases the device volume by a minimum volume step. + */ + public void increaseVolume() { + changeVolume(mVolumeStep); + } + + /** + * Decreases the device volume by a minimum volume step. + */ + public void decreaseVolume() { + changeVolume(- mVolumeStep); + } + + /** + * Selects the renderer for playback, applying its minimum and maximum volume. + */ + public void selectRenderer(Device renderer) { + mPlayService.getService().setRenderer(renderer); + + if (getService("RenderingControl").getStateVariable("Volume") != null) { + StateVariableAllowedValueRange volumeRange = + getService("RenderingControl").getStateVariable("Volume") + .getTypeDetails().getAllowedValueRange(); + mMinVolume = volumeRange.getMinimum(); + mMaxVolume = volumeRange.getMaximum(); + mVolumeStep = volumeRange.getStep(); + } + else { + mMinVolume = 0; + mMaxVolume = 100; + mVolumeStep = 1; + } + // Hack, needed as using a smaller step seems to not + // increase volume on some devices. + mVolumeStep = 4; + } + + /** + * Seeks to the given absolute time in seconds. + */ + public void seek(int absoluteTime) { + if (mPlayService.getService().getRenderer() == null) + return; + + mUpnpService.getControlPoint().execute(new Seek( + getService(mPlayService.getService().getRenderer(), "AVTransport"), + SeekMode.REL_TIME, + Integer.toString(absoluteTime)) { + + @SuppressWarnings("rawtypes") + @Override + public void failure(ActionInvocation invocation, + UpnpResponse operation, String defaultMessage) { + Log.w(TAG, "Seek failed: " + defaultMessage); + } + }); + + } + + /** + * Returns the service that handles actual playback. + */ + public PlayService getPlayService() { + return mPlayService.getService(); + } + +} diff --git a/src/com/github/nutomic/controldlna/gui/MainActivity.java b/src/com/github/nutomic/controldlna/gui/MainActivity.java index 2119616..fa9451e 100644 --- a/src/com/github/nutomic/controldlna/gui/MainActivity.java +++ b/src/com/github/nutomic/controldlna/gui/MainActivity.java @@ -27,30 +27,33 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package com.github.nutomic.controldlna.gui; -import java.util.List; - -import org.teleal.cling.support.model.item.Item; - import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.app.FragmentTransaction; import android.support.v4.view.ViewPager; +import android.util.Log; import android.view.KeyEvent; import com.actionbarsherlock.app.ActionBar; import com.actionbarsherlock.app.SherlockFragmentActivity; import com.github.nutomic.controldlna.R; +import com.github.nutomic.controldlna.UpnpPlayer; /** * Main activity, with tabs for media servers and media renderers. * - * @author Felix + * @author Felix Ableitner * */ public class MainActivity extends SherlockFragmentActivity implements ActionBar.TabListener { + + /** + * Manages all UPNP connections including playback. + */ + private UpnpPlayer mPlayer = new UpnpPlayer(); /** * Provides Fragments, holding all of them in memory. @@ -102,6 +105,14 @@ public class MainActivity extends SherlockFragmentActivity implements actionBar.addTab(actionBar.newTab() .setText(R.string.title_renderer) .setTabListener(this)); + + mPlayer.open(getApplicationContext()); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mPlayer.close(getApplicationContext()); } @Override @@ -148,15 +159,12 @@ public class MainActivity extends SherlockFragmentActivity implements * Listener for the 'back' key. */ public interface OnBackPressedListener { - - /** - * Returns true if the press was consumed, false otherwise. - */ public boolean onBackPressed(); } /** - * Forwards back press to active Fragment. + * Forwards back press to active Fragment (unless the fragment is + * showing its root view). */ @Override public void onBackPressed() { @@ -167,31 +175,37 @@ public class MainActivity extends SherlockFragmentActivity implements } /** - * Utility function to call RendererFragment.play from ServerFragment. + * Changes volume on key press. */ - public void play(List playlist, int start) { - getSupportActionBar().selectTab(getSupportActionBar().getTabAt(1)); - mRendererFragment.setPlaylist(playlist, start); + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_VOLUME_UP: + if (event.getAction() == KeyEvent.ACTION_DOWN) + mPlayer.increaseVolume(); + return true; + case KeyEvent.KEYCODE_VOLUME_DOWN: + if (event.getAction() == KeyEvent.ACTION_DOWN) + mPlayer.decreaseVolume(); + return true; + default: + return super.dispatchKeyEvent(event); + } + } + + /** + * Returns shared instance of UpnpPlayer. + */ + public UpnpPlayer getUpnpPlayer() { + return mPlayer; } /** - * Sends volume key events to RendererFragment (which sends them to - * media renderer). + * Opens the UPNP renderer tab in the activity. */ - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_VOLUME_UP: - if (event.getAction() == KeyEvent.ACTION_DOWN) - mRendererFragment.changeVolume(true); - return true; - case KeyEvent.KEYCODE_VOLUME_DOWN: - if (event.getAction() == KeyEvent.ACTION_DOWN) - mRendererFragment.changeVolume(false); - return true; - default: - return super.dispatchKeyEvent(event); - } - } + public void switchToRendererTab() { + Log.d("tag", "called"); + getSupportActionBar().selectTab(getSupportActionBar().getTabAt(1)); + } } diff --git a/src/com/github/nutomic/controldlna/gui/RendererFragment.java b/src/com/github/nutomic/controldlna/gui/RendererFragment.java index fc2d9db..48ec2b9 100644 --- a/src/com/github/nutomic/controldlna/gui/RendererFragment.java +++ b/src/com/github/nutomic/controldlna/gui/RendererFragment.java @@ -30,8 +30,6 @@ package com.github.nutomic.controldlna.gui; import java.util.List; import java.util.Map; -import org.teleal.cling.android.AndroidUpnpService; -import org.teleal.cling.android.AndroidUpnpServiceImpl; import org.teleal.cling.controlpoint.SubscriptionCallback; import org.teleal.cling.model.action.ActionInvocation; import org.teleal.cling.model.gena.CancelReason; @@ -40,28 +38,18 @@ import org.teleal.cling.model.message.UpnpResponse; import org.teleal.cling.model.meta.Device; import org.teleal.cling.model.meta.Service; import org.teleal.cling.model.state.StateVariableValue; -import org.teleal.cling.model.types.ServiceType; import org.teleal.cling.support.avtransport.callback.GetPositionInfo; -import org.teleal.cling.support.avtransport.callback.Seek; import org.teleal.cling.support.avtransport.lastchange.AVTransportLastChangeParser; import org.teleal.cling.support.avtransport.lastchange.AVTransportVariable; import org.teleal.cling.support.lastchange.LastChange; import org.teleal.cling.support.model.PositionInfo; -import org.teleal.cling.support.model.SeekMode; import org.teleal.cling.support.model.item.Item; -import org.teleal.cling.support.renderingcontrol.callback.GetVolume; -import org.teleal.cling.support.renderingcontrol.callback.SetVolume; import android.app.AlertDialog; -import android.content.ComponentName; -import android.content.Context; import android.content.DialogInterface; -import android.content.Intent; -import android.content.ServiceConnection; import android.graphics.Color; import android.os.Bundle; import android.os.Handler; -import android.os.IBinder; import android.support.v4.app.Fragment; import android.util.Log; import android.view.LayoutInflater; @@ -76,19 +64,17 @@ import android.widget.ImageButton; import android.widget.ListView; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; -import android.widget.Toast; import com.github.nutomic.controldlna.DeviceArrayAdapter; import com.github.nutomic.controldlna.FileArrayAdapter; import com.github.nutomic.controldlna.R; +import com.github.nutomic.controldlna.UpnpPlayer; import com.github.nutomic.controldlna.gui.MainActivity.OnBackPressedListener; -import com.github.nutomic.controldlna.service.PlayService; -import com.github.nutomic.controldlna.service.PlayServiceBinder; /** * Shows a list of media servers, allowing to select one for playback. * - * @author Felix + * @author Felix Ableitner * */ public class RendererFragment extends Fragment implements @@ -117,43 +103,6 @@ public class RendererFragment extends Fragment implements private FileArrayAdapter mPlaylistAdapter; private SubscriptionCallback mSubscriptionCallback; - - private PlayServiceBinder mPlayService; - - private ServiceConnection mPlayServiceConnection = new ServiceConnection() { - - public void onServiceConnected(ComponentName className, IBinder service) { - mPlayService = (PlayServiceBinder) service; - } - - public void onServiceDisconnected(ComponentName className) { - mPlayService = null; - } - }; - - /** - * Cling UPNP service. - */ - private AndroidUpnpService mUpnpService; - - /** - * Connection Cling to UPNP service. - */ - private ServiceConnection mUpnpServiceConnection = new ServiceConnection() { - - public void onServiceConnected(ComponentName className, IBinder service) { - mUpnpService = (AndroidUpnpService) service; - Log.i(TAG, "Starting device search"); - mUpnpService.getRegistry().addListener(mRendererAdapter); - mUpnpService.getControlPoint().search(); - mRendererAdapter.add( - mUpnpService.getControlPoint().getRegistry().getDevices()); - } - - public void onServiceDisconnected(ComponentName className) { - mUpnpService = null; - } - }; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -186,29 +135,22 @@ public class RendererFragment extends Fragment implements mPlayPause.setImageResource(R.drawable.ic_media_play); getView().findViewById(R.id.previous).setOnClickListener(this); getView().findViewById(R.id.next).setOnClickListener(this); - - getActivity().startService(new Intent(getActivity(), PlayService.class)); - getActivity().getApplicationContext().bindService( - new Intent(getActivity(), PlayService.class), - mPlayServiceConnection, - Context.BIND_AUTO_CREATE - ); - - getActivity().getApplicationContext().bindService( - new Intent(getActivity(), AndroidUpnpServiceImpl.class), - mUpnpServiceConnection, - Context.BIND_AUTO_CREATE - ); + getPlayer().getDeviceListener().addCallback(mRendererAdapter); + } + + private UpnpPlayer getPlayer() { + MainActivity activity = (MainActivity) getActivity(); + return activity.getUpnpPlayer(); } /** * Polls the renderer for the current play progress as long as - * mPlaying is true. + * playback is active. */ private void pollTimePosition() { - final Service service = mCurrentRenderer.findService( - new ServiceType("schemas-upnp-org", "AVTransport")); - mUpnpService.getControlPoint().execute( + Service service = getPlayer() + .getService(mCurrentRenderer, "AVTransport"); + getPlayer().execute( new GetPositionInfo(service) { @SuppressWarnings("rawtypes") @@ -236,17 +178,6 @@ public class RendererFragment extends Fragment implements }, 1000); } } - - /** - * Closes Cling UPNP service. - */ - @Override - public void onDestroy() { - super.onDestroy(); - mUpnpService.getRegistry().removeListener(mRendererAdapter); - getActivity().getApplicationContext().unbindService(mUpnpServiceConnection); - getActivity().getApplicationContext().unbindService(mPlayServiceConnection); - } /** * Sets the new playlist. @@ -254,7 +185,7 @@ public class RendererFragment extends Fragment implements public void setPlaylist(List playlist, int start) { mPlaylistAdapter.clear(); mPlaylistAdapter.add(playlist); - mPlayService.getService().setPlaylist(playlist, start); + getPlayer().getPlayService().setPlaylist(playlist, start); } /** @@ -288,7 +219,7 @@ public class RendererFragment extends Fragment implements } } else if (mListView.getAdapter() == mPlaylistAdapter) - mPlayService.getService().playTrack(position); + getPlayer().getPlayService().playTrack(position); } /** @@ -302,9 +233,9 @@ public class RendererFragment extends Fragment implements mSubscriptionCallback.end(); mCurrentRenderer = renderer; - mPlayService.getService().setRenderer(renderer); + getPlayer().selectRenderer(renderer); mSubscriptionCallback = new SubscriptionCallback( - getService("AVTransport"), 600) { + getPlayer().getService("AVTransport"), 600) { @SuppressWarnings("rawtypes") @Override @@ -374,10 +305,10 @@ public class RendererFragment extends Fragment implements Log.d(TAG, defaultMsg); } }; - mUpnpService.getControlPoint().execute(mSubscriptionCallback); + getPlayer().execute(mSubscriptionCallback); } mPlaylistAdapter.clear(); - mPlaylistAdapter.add(mPlayService.getService().getPlaylist()); + mPlaylistAdapter.add(getPlayer().getPlayService().getPlaylist()); mListView.setAdapter(mPlaylistAdapter); } @@ -388,7 +319,7 @@ public class RendererFragment extends Fragment implements if (mListView.getAdapter() == mRendererAdapter) return; disableTrackHighlight(); - mCurrentTrackView = mListView.getChildAt(mPlayService.getService() + mCurrentTrackView = mListView.getChildAt(getPlayer().getPlayService() .getCurrentTrack() - mListView.getFirstVisiblePosition() + mListView.getHeaderViewsCount()); if (mCurrentTrackView != null) @@ -426,15 +357,15 @@ public class RendererFragment extends Fragment implements switch (v.getId()) { case R.id.playpause: if (mPlaying) - mPlayService.getService().pause(); + getPlayer().getPlayService().pause(); else - mPlayService.getService().play(); + getPlayer().getPlayService().play(); break; case R.id.previous: - mPlayService.getService().playPrevious(); + getPlayer().getPlayService().playPrevious(); break; case R.id.next: - mPlayService.getService().playNext(); + getPlayer().getPlayService().playNext(); break; } } @@ -446,17 +377,7 @@ public class RendererFragment extends Fragment implements public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser) { - mUpnpService.getControlPoint().execute(new Seek( - getService("AVTransport"), SeekMode.REL_TIME, - Integer.toString(progress)) { - - @SuppressWarnings("rawtypes") - @Override - public void failure(ActionInvocation invocation, - UpnpResponse operation, String defaultMessage) { - Log.w(TAG, "Seek failed: " + defaultMessage); - } - }); + getPlayer().seek(progress); } } @@ -468,50 +389,6 @@ public class RendererFragment extends Fragment implements public void onStopTrackingTouch(SeekBar seekBar) { } - /** - * Increases or decreases volume. - * - * @param increase True to increase volume, false to decrease. - */ - @SuppressWarnings("rawtypes") - public void changeVolume(final boolean increase) { - if (mCurrentRenderer == null) { - Toast.makeText(getActivity(), R.string.select_renderer, - Toast.LENGTH_SHORT).show(); - return; - } - final Service service = getService("RenderingControl"); - mUpnpService.getControlPoint().execute( - new GetVolume(service) { - - @Override - public void failure(ActionInvocation invocation, - UpnpResponse operation, String defaultMessage) { - Log.d(TAG, "Failed to get current Volume: " + defaultMessage); - } - - @Override - public void received(ActionInvocation invocation, int volume) { - int newVolume = volume + ((increase) ? 4 : -4); - if (newVolume < 0) newVolume = 0; - mUpnpService.getControlPoint().execute( - new SetVolume(service, newVolume) { - - @Override - public void failure(ActionInvocation invocation, - UpnpResponse operation, String defaultMessage) { - Log.d(TAG, "Failed to set new Volume: " + defaultMessage); - } - }); - } - }); - } - - private Service getService(String name) { - return mCurrentRenderer.findService( - new ServiceType("schemas-upnp-org", name)); - } - @Override public void onScroll(AbsListView arg0, int arg1, int arg2, int arg3) { enableTrackHighlight(); diff --git a/src/com/github/nutomic/controldlna/gui/ServerFragment.java b/src/com/github/nutomic/controldlna/gui/ServerFragment.java index 0c530ff..92477ff 100644 --- a/src/com/github/nutomic/controldlna/gui/ServerFragment.java +++ b/src/com/github/nutomic/controldlna/gui/ServerFragment.java @@ -31,8 +31,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Stack; -import org.teleal.cling.android.AndroidUpnpService; -import org.teleal.cling.android.AndroidUpnpServiceImpl; import org.teleal.cling.model.action.ActionInvocation; import org.teleal.cling.model.message.UpnpResponse; import org.teleal.cling.model.meta.Device; @@ -44,12 +42,7 @@ import org.teleal.cling.support.model.DIDLContent; import org.teleal.cling.support.model.container.Container; import org.teleal.cling.support.model.item.Item; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; import android.os.Bundle; -import android.os.IBinder; import android.os.Parcelable; import android.support.v4.app.ListFragment; import android.util.Log; @@ -61,10 +54,10 @@ import com.github.nutomic.controldlna.FileArrayAdapter; import com.github.nutomic.controldlna.gui.MainActivity.OnBackPressedListener; /** - * Shows a list of media servers, upon selecting one, allows browsing theur + * Shows a list of media servers, upon selecting one, allows browsing their * directories. * - * @author Felix + * @author Felix Ableitner * */ public class ServerFragment extends ListFragment implements OnBackPressedListener { @@ -99,30 +92,6 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene * Holds the scroll position in the list view at each directory level. */ private Stack mListState = new Stack(); - - /** - * Cling UPNP service. - */ - private AndroidUpnpService mUpnpService; - - /** - * Connection Cling to UPNP service. - */ - private ServiceConnection mUpnpServiceConnection = new ServiceConnection() { - - public void onServiceConnected(ComponentName className, IBinder service) { - mUpnpService = (AndroidUpnpService) service; - Log.i(TAG, "Starting device search"); - mUpnpService.getRegistry().addListener(mServerAdapter); - mUpnpService.getControlPoint().search(); - mServerAdapter.add( - mUpnpService.getControlPoint().getRegistry().getDevices()); - } - - public void onServiceDisconnected(ComponentName className) { - mUpnpService = null; - } - }; /** * Initializes ListView adapters, launches Cling UPNP service. @@ -134,24 +103,9 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene mServerAdapter = new DeviceArrayAdapter( getActivity(), DeviceArrayAdapter.SERVER); - setListAdapter(mServerAdapter); - - getActivity().getApplicationContext().bindService( - new Intent(getActivity(), AndroidUpnpServiceImpl.class), - mUpnpServiceConnection, - Context.BIND_AUTO_CREATE - ); - } - - /** - * Closes Cling UPNP service. - */ - @Override - public void onDestroy() { - super.onDestroy(); - if (mUpnpService != null) - mUpnpService.getRegistry().removeListener(mServerAdapter); - getActivity().getApplicationContext().unbindService(mUpnpServiceConnection); + setListAdapter(mServerAdapter); + MainActivity activity = (MainActivity) getActivity(); + activity.getUpnpPlayer().getDeviceListener().addCallback(mServerAdapter); } /** @@ -175,7 +129,9 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene playlist.add((Item) mFileAdapter.getItem(i)); } MainActivity activity = (MainActivity) getActivity(); - activity.play(playlist, position); + activity.switchToRendererTab(); + activity.getUpnpPlayer().getPlayService() + .setPlaylist(playlist, position); } } } @@ -191,11 +147,15 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene /** * Displays the current directory on the ListView. + * + * @param restoreListState True if we are going back up the directory tree, + * which means we restore scroll position etc. */ private void getFiles(final boolean restoreListState) { Service service = mCurrentServer.findService( new ServiceType("schemas-upnp-org", "ContentDirectory")); - mUpnpService.getControlPoint().execute(new Browse(service, + MainActivity activity = (MainActivity) getActivity(); + activity.getUpnpPlayer().execute(new Browse(service, mCurrentPath.peek(), BrowseFlag.DIRECT_CHILDREN) { @SuppressWarnings("rawtypes") diff --git a/src/com/github/nutomic/controldlna/service/PlayService.java b/src/com/github/nutomic/controldlna/service/PlayService.java index 6307162..e1a0bc2 100644 --- a/src/com/github/nutomic/controldlna/service/PlayService.java +++ b/src/com/github/nutomic/controldlna/service/PlayService.java @@ -73,6 +73,12 @@ import com.github.nutomic.controldlna.LoadImageTask; import com.github.nutomic.controldlna.R; import com.github.nutomic.controldlna.gui.MainActivity; +/** + * Background service that handles media playback to a single UPNP media renderer. + * + * @author Felix Ableitner + * + */ public class PlayService extends Service { private static final String TAG = "PlayService"; @@ -82,7 +88,7 @@ public class PlayService extends Service { private final PlayServiceBinder mBinder = new PlayServiceBinder(this); /** - * The DLNA media renderer device that is currently active. + * The DLNA media renderer device that is currently active for playback. */ private Device mRenderer; @@ -115,7 +121,7 @@ public class PlayService extends Service { private AndroidUpnpService mUpnpService; /** - * Connection Cling to UPNP service. + * Cling connection to UPNP service. */ private ServiceConnection mUpnpServiceConnection = new ServiceConnection() { @@ -346,6 +352,10 @@ public class PlayService extends Service { playTrack(mCurrentTrack); } + public Device getRenderer() { + return mRenderer; + } + /** * Sets a new playlist and starts playing. * diff --git a/src/com/github/nutomic/controldlna/service/PlayServiceBinder.java b/src/com/github/nutomic/controldlna/service/PlayServiceBinder.java index 84b5241..77392a9 100644 --- a/src/com/github/nutomic/controldlna/service/PlayServiceBinder.java +++ b/src/com/github/nutomic/controldlna/service/PlayServiceBinder.java @@ -29,6 +29,12 @@ package com.github.nutomic.controldlna.service; import android.os.Binder; +/** + * Provides connection to PlayService. + * + * @author Felix Ableitner + * + */ public class PlayServiceBinder extends Binder { PlayService mService;