diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 73e1ece..a1b7633 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -31,14 +31,14 @@
-
-
+
+
-
+
-
+
-
diff --git a/src/android/os/Messenger.aidl b/src/android/os/Messenger.aidl
new file mode 100644
index 0000000..6ae396e
--- /dev/null
+++ b/src/android/os/Messenger.aidl
@@ -0,0 +1,3 @@
+package android.os;
+
+parcelable Messenger;
\ No newline at end of file
diff --git a/src/com/github/nutomic/controldlna/gui/MainActivity.java b/src/com/github/nutomic/controldlna/gui/MainActivity.java
index fc934e0..115d208 100644
--- a/src/com/github/nutomic/controldlna/gui/MainActivity.java
+++ b/src/com/github/nutomic/controldlna/gui/MainActivity.java
@@ -27,7 +27,13 @@ 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.FragmentStatePagerAdapter;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar;
@@ -37,10 +43,9 @@ import android.support.v7.app.ActionBarActivity;
import android.view.KeyEvent;
import com.github.nutomic.controldlna.R;
-import com.github.nutomic.controldlna.upnp.UpnpPlayer;
/**
- * Main activity, with tabs for media servers and media renderers.
+ * Main activity, with tabs for media servers and media routes.
*
* @author Felix Ableitner
*
@@ -54,21 +59,34 @@ public class MainActivity extends ActionBarActivity {
boolean onBackPressed();
}
- /**
- * Manages all UPNP connections including playback.
- */
- private UpnpPlayer mPlayer = new UpnpPlayer();
-
- /**
- * Holds fragments.
- */
- SectionsPagerAdapter mSectionsPagerAdapter;
+ FragmentStatePagerAdapter mSectionsPagerAdapter =
+ new FragmentStatePagerAdapter(getSupportFragmentManager()) {
+
+ @Override
+ public Fragment getItem(int position) {
+ switch (position) {
+ case 0: return mServerFragment;
+ case 1: return mRendererFragment;
+ default: return null;
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return 2;
+ }
+
+ };
+
+ private ServerFragment mServerFragment = new ServerFragment();
+
+ private RouteFragment mRendererFragment = new RouteFragment();
- /**
- * Allows tab swiping.
- */
ViewPager mViewPager;
+ /**
+ * Initializes tab navigation.
+ */
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ActionBar actionBar = getSupportActionBar();
@@ -77,11 +95,6 @@ public class MainActivity extends ActionBarActivity {
actionBar.setDisplayShowTitleEnabled(false);
actionBar.setDisplayShowHomeEnabled(false);
setContentView(R.layout.activity_main);
-
- // ViewPager and its adapters use support library
- // fragments, so use getSupportFragmentManager.
- mSectionsPagerAdapter = new SectionsPagerAdapter(
- getSupportFragmentManager());
mViewPager = (ViewPager) findViewById(R.id.pager);
mViewPager.setAdapter(mSectionsPagerAdapter);
@@ -112,61 +125,46 @@ public class MainActivity extends ActionBarActivity {
.setTabListener(tabListener));
actionBar.addTab(actionBar.newTab()
.setText(R.string.title_renderer)
- .setTabListener(tabListener));
-
- mPlayer.open(getApplicationContext());
+ .setTabListener(tabListener));
}
- @Override
- protected void onDestroy() {
- super.onDestroy();
- mPlayer.close(getApplicationContext());
- }
-
/**
* Forwards back press to active Fragment (unless the fragment is
* showing its root view).
*/
@Override
public void onBackPressed() {
- OnBackPressedListener currentFragment = (OnBackPressedListener)
- mSectionsPagerAdapter.getItem(mViewPager.getCurrentItem());
- if (!currentFragment.onBackPressed())
- super.onBackPressed();
+ OnBackPressedListener currentFragment = (OnBackPressedListener)
+ mSectionsPagerAdapter.getItem(mViewPager.getCurrentItem());
+ if (!currentFragment.onBackPressed())
+ super.onBackPressed();
}
/**
- * Changes volume on key press.
+ * Changes volume on key press (via RouteFragment).
*/
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_VOLUME_UP:
if (event.getAction() == KeyEvent.ACTION_DOWN)
- mPlayer.changeVolume(1);
+ mRendererFragment.increaseVolume();
return true;
case KeyEvent.KEYCODE_VOLUME_DOWN:
if (event.getAction() == KeyEvent.ACTION_DOWN)
- mPlayer.changeVolume(-1);
+ mRendererFragment.decreaseVolume();
return true;
default:
return super.dispatchKeyEvent(event);
}
}
-
- /**
- * Returns shared instance of UPNP player.
- * @return
- */
- public UpnpPlayer getUpnpPlayer() {
- return mPlayer;
-
- }
/**
- * Switches to the "renderer" tab.
+ * Starts playing the playlist from item start (via RouteFragment).
*/
- public void switchToRendererTab() {
+ public void play(List- playlist, int start) {
mViewPager.setCurrentItem(1);
+ mRendererFragment.play(playlist, start);
}
+
}
diff --git a/src/com/github/nutomic/controldlna/gui/RendererFragment.java b/src/com/github/nutomic/controldlna/gui/RendererFragment.java
deleted file mode 100644
index 0276181..0000000
--- a/src/com/github/nutomic/controldlna/gui/RendererFragment.java
+++ /dev/null
@@ -1,394 +0,0 @@
-/*
-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.gui;
-
-import java.util.Map;
-
-import org.teleal.cling.controlpoint.SubscriptionCallback;
-import org.teleal.cling.model.action.ActionInvocation;
-import org.teleal.cling.model.gena.CancelReason;
-import org.teleal.cling.model.gena.GENASubscription;
-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.support.avtransport.callback.GetPositionInfo;
-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 android.app.AlertDialog;
-import android.content.DialogInterface;
-import android.graphics.Color;
-import android.os.Bundle;
-import android.os.Handler;
-import android.support.v4.app.Fragment;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.ViewGroup;
-import android.widget.AbsListView;
-import android.widget.AbsListView.OnScrollListener;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.ImageButton;
-import android.widget.ListView;
-import android.widget.SeekBar;
-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;
-
-/**
- * Shows a list of media servers, allowing to select one for playback.
- *
- * @author Felix Ableitner
- *
- */
-public class RendererFragment extends Fragment implements
- OnBackPressedListener, OnItemClickListener, OnClickListener,
- OnSeekBarChangeListener, OnScrollListener {
-
- private final String TAG = "RendererFragment";
-
- private ListView mListView;
-
- private View mControls;
- private SeekBar mProgressBar;
- private ImageButton mPlayPause;
-
- private boolean mPlaying = false;
-
- private Device, ?, ?> mCurrentRenderer;
-
- private View mCurrentTrackView;
-
- /**
- * ListView adapter of media renderers.
- */
- private DeviceArrayAdapter mRendererAdapter;
-
- private FileArrayAdapter mPlaylistAdapter;
-
- private SubscriptionCallback mSubscriptionCallback;
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- return inflater.inflate(R.layout.renderer_fragment, null);
- };
-
- /**
- * Initializes ListView adapters, launches Cling UPNP service.
- */
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- mListView = (ListView) getView().findViewById(R.id.listview);
- mPlaylistAdapter = new FileArrayAdapter(getActivity());
- mRendererAdapter = new DeviceArrayAdapter(
- getActivity(), DeviceArrayAdapter.RENDERER);
- mListView.setAdapter(mRendererAdapter);
- mListView.setOnItemClickListener(this);
- mListView.setOnScrollListener(this);
- mControls = getView().findViewById(R.id.controls);
- mProgressBar = (SeekBar) getView().findViewById(R.id.progressBar);
- mProgressBar.setOnSeekBarChangeListener(this);
- ImageButton previous = (ImageButton) getView().findViewById(R.id.previous);
- previous.setImageResource(R.drawable.ic_media_previous);
- ImageButton next = (ImageButton) getView().findViewById(R.id.next);
- next.setImageResource(R.drawable.ic_media_next);
- mPlayPause = (ImageButton) getView().findViewById(R.id.playpause);
- mPlayPause.setOnClickListener(this);
- mPlayPause.setImageResource(R.drawable.ic_media_play);
- getView().findViewById(R.id.previous).setOnClickListener(this);
- getView().findViewById(R.id.next).setOnClickListener(this);
- 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
- * playback is active.
- */
- private void pollTimePosition() {
- Service, ?> service = UpnpController
- .getService(mCurrentRenderer, "AVTransport");
- getPlayer().execute(
- new GetPositionInfo(service) {
-
- @SuppressWarnings("rawtypes")
- @Override
- public void failure(ActionInvocation invocation,
- UpnpResponse operation, String defaultMessage) {
- Log.w(TAG, "Get position failed: " + defaultMessage);
- }
-
- @SuppressWarnings("rawtypes")
- @Override
- public void received(ActionInvocation invocation, PositionInfo positionInfo) {
- mProgressBar.setMax((int) positionInfo.getTrackDurationSeconds());
- mProgressBar.setProgress((int) positionInfo.getTrackElapsedSeconds());
- }
- });
-
- if (mPlaying) {
- new Handler().postDelayed(new Runnable() {
-
- @Override
- public void run() {
- pollTimePosition();
- }
- }, 1000);
- }
- }
-
- /**
- * Selects a media renderer.
- */
- @Override
- public void onItemClick(AdapterView> a, View v, final int position, long id) {
- if (mListView.getAdapter() == mRendererAdapter) {
- if (mCurrentRenderer != null &&
- mCurrentRenderer != mRendererAdapter.getItem(position)) {
- new AlertDialog.Builder(getActivity())
- .setMessage(R.string.exit_renderer)
- .setPositiveButton(android.R.string.yes,
- new DialogInterface.OnClickListener() {
-
- @Override
- public void onClick(DialogInterface dialog,
- int which) {
- mControls.setVisibility(View.VISIBLE);
- selectRenderer(mRendererAdapter
- .getItem(position));
- }
- })
- .setNegativeButton(android.R.string.no, null)
- .show();
-
- }
- else {
- mControls.setVisibility(View.VISIBLE);
- selectRenderer(mRendererAdapter.getItem(position));
- }
- }
- else if (mListView.getAdapter() == mPlaylistAdapter)
- getPlayer().getPlayService().playTrack(position);
- }
-
- /**
- * Shows controls and playlist for the selected media renderer.
- *
- * @param renderer The new renderer to select.
- */
- private void selectRenderer(Device, ?, ?> renderer) {
- if (mCurrentRenderer != renderer) {
- if (mSubscriptionCallback != null)
- mSubscriptionCallback.end();
-
- mCurrentRenderer = renderer;
- getPlayer().selectRenderer(renderer);
- mSubscriptionCallback = new SubscriptionCallback(
- getPlayer().getService("AVTransport"), 600) {
-
- @SuppressWarnings("rawtypes")
- @Override
- protected void established(GENASubscription sub) {
- }
-
- @SuppressWarnings("rawtypes")
- @Override
- protected void ended(GENASubscription sub, CancelReason reason,
- UpnpResponse response) {
- }
-
- @SuppressWarnings("rawtypes")
- @Override
- protected void eventReceived(final GENASubscription sub) {
- if (getActivity() == null) return;
- getActivity().runOnUiThread(new Runnable() {
-
- @Override
- public void run() {
- @SuppressWarnings("unchecked")
- Map m = sub.getCurrentValues();
- try {
- LastChange lastChange = new LastChange(
- new AVTransportLastChangeParser(),
- m.get("LastChange").toString());
- switch (lastChange.getEventedValue(0,
- AVTransportVariable.TransportState.class)
- .getValue()) {
- case PLAYING:
- mPlaying = true;
- mPlayPause.setImageResource(R.drawable.ic_media_pause);
- mPlayPause.setContentDescription(getResources().
- getString(R.string.pause));
- mPlaylistAdapter.clear();
- mPlaylistAdapter.add(getPlayer().getPlayService().getPlaylist());
- pollTimePosition();
- enableTrackHighlight();
- break;
- case STOPPED:
- // fallthrough
- case PAUSED_PLAYBACK:
- mPlayPause.setImageResource(R.drawable.ic_media_play);
- mPlayPause.setContentDescription(getResources().
- getString(R.string.play));
- mPlaying = false;
- break;
- default:
- break;
- }
-
- } catch (Exception e) {
- Log.w(TAG, "Failed to parse UPNP event", e);
- }
- }
- });
- }
-
- @SuppressWarnings("rawtypes")
- @Override
- protected void eventsMissed(GENASubscription sub,
- int numberOfMissedEvents) {
- }
-
- @SuppressWarnings("rawtypes")
- @Override
- protected void failed(GENASubscription sub, UpnpResponse responseStatus,
- Exception exception, String defaultMsg) {
- Log.d(TAG, defaultMsg);
- }
- };
- getPlayer().execute(mSubscriptionCallback);
- }
- mPlaylistAdapter.clear();
- mPlaylistAdapter.add(getPlayer().getPlayService().getPlaylist());
- mListView.setAdapter(mPlaylistAdapter);
- }
-
- /**
- * Sets colored background on the item that is currently playing.
- */
- private void enableTrackHighlight() {
- if (mListView.getAdapter() == mRendererAdapter)
- return;
- disableTrackHighlight();
- mCurrentTrackView = mListView.getChildAt(getPlayer().getPlayService()
- .getCurrentTrack()
- - mListView.getFirstVisiblePosition() + mListView.getHeaderViewsCount());
- if (mCurrentTrackView != null)
- mCurrentTrackView.setBackgroundColor(
- getResources().getColor(R.color.currently_playing_background));
- }
-
- /**
- * Removes highlight from the item that was last highlighted.
- */
- private void disableTrackHighlight() {
- if (mCurrentTrackView != null)
- mCurrentTrackView.setBackgroundColor(Color.TRANSPARENT);
- }
-
- /**
- * Unselects current media renderer if one is selected.
- */
- @Override
- public boolean onBackPressed() {
- if (mListView.getAdapter() == mPlaylistAdapter) {
- mControls.setVisibility(View.GONE);
- mListView.setAdapter(mRendererAdapter);
- disableTrackHighlight();
- return true;
- }
- return false;
- }
-
- /**
- * Plays/pauses playback on button click.
- */
- @Override
- public void onClick(View v) {
- switch (v.getId()) {
- case R.id.playpause:
- if (mPlaying)
- getPlayer().getPlayService().pause();
- else
- getPlayer().getPlayService().play();
- break;
- case R.id.previous:
- getPlayer().getPlayService().playPrevious();
- break;
- case R.id.next:
- getPlayer().getPlayService().playNext();
- break;
- }
- }
-
- /**
- * Sends manual seek on progress bar to renderer.
- */
- @Override
- public void onProgressChanged(SeekBar seekBar, int progress,
- boolean fromUser) {
- if (fromUser) {
- getPlayer().seek(progress);
- }
- }
-
- @Override
- public void onStartTrackingTouch(SeekBar seekBar) {
- }
-
- @Override
- public void onStopTrackingTouch(SeekBar seekBar) {
- }
-
- @Override
- public void onScroll(AbsListView arg0, int arg1, int arg2, int arg3) {
- enableTrackHighlight();
- }
-
- @Override
- public void onScrollStateChanged(AbsListView arg0, int arg1) {
- enableTrackHighlight();
- }
-
-}
diff --git a/src/com/github/nutomic/controldlna/gui/RouteFragment.java b/src/com/github/nutomic/controldlna/gui/RouteFragment.java
new file mode 100644
index 0000000..5069a52
--- /dev/null
+++ b/src/com/github/nutomic/controldlna/gui/RouteFragment.java
@@ -0,0 +1,414 @@
+/*
+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.gui;
+
+import java.util.List;
+
+import org.teleal.cling.support.model.item.Item;
+
+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.IBinder;
+import android.support.v7.app.MediaRouteDiscoveryFragment;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaItemStatus;
+import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.media.MediaRouter;
+import android.support.v7.media.MediaRouter.Callback;
+import android.support.v7.media.MediaRouter.ProviderInfo;
+import android.support.v7.media.MediaRouter.RouteInfo;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+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.R;
+import com.github.nutomic.controldlna.gui.MainActivity.OnBackPressedListener;
+import com.github.nutomic.controldlna.mediarouter.MediaRouterPlayService;
+import com.github.nutomic.controldlna.mediarouter.MediaRouterPlayServiceBinder;
+import com.github.nutomic.controldlna.utility.FileArrayAdapter;
+import com.github.nutomic.controldlna.utility.RouteAdapter;
+
+/**
+ * Controls media playback by showing a list of routes, and after selecting one,
+ * the current playlist and playback controls.
+ *
+ * @author Felix Ableitner
+ *
+ */
+public class RouteFragment extends MediaRouteDiscoveryFragment implements
+ OnBackPressedListener, OnItemClickListener, OnClickListener,
+ OnSeekBarChangeListener, OnScrollListener {
+
+ private ListView mListView;
+
+ private View mControls;
+ private SeekBar mProgressBar;
+ private ImageButton mPlayPause;
+
+ private View mCurrentTrackView;
+
+ private boolean mPlaying;
+
+ private RouteAdapter mRouteAdapter;
+
+ private FileArrayAdapter mPlaylistAdapter;
+
+ private boolean mRouteSelected = false;
+
+ /**
+ * If true, the item at this position will be played as soon as a route is selected.
+ */
+ private int mStartPlayingOnSelect = -1;
+
+ private MediaRouterPlayServiceBinder mMediaRouterPlayService;
+
+ private ServiceConnection mPlayServiceConnection = new ServiceConnection() {
+
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ mMediaRouterPlayService = (MediaRouterPlayServiceBinder) service;
+ mMediaRouterPlayService.getService().setRendererFragment(RouteFragment.this);
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ mMediaRouterPlayService = null;
+ }
+ };
+
+ /**
+ * Selects remote playback route category.
+ */
+ public RouteFragment() {
+ MediaRouteSelector mSelector = new MediaRouteSelector.Builder()
+ .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
+ .build();
+ setRouteSelector(mSelector);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+
+ return inflater.inflate(R.layout.renderer_fragment, null);
+ };
+
+ /**
+ * Initializes views, connects to service, adds default route.
+ */
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mRouteAdapter = new RouteAdapter(getActivity());
+ mPlaylistAdapter = new FileArrayAdapter(getActivity());
+
+ mListView = (ListView) getView().findViewById(R.id.listview);
+ mListView.setAdapter(mRouteAdapter);
+ mListView.setOnItemClickListener(this);
+ mListView.setOnScrollListener(this);
+
+ mControls = getView().findViewById(R.id.controls);
+ mProgressBar = (SeekBar) getView().findViewById(R.id.progressBar);
+ mProgressBar.setOnSeekBarChangeListener(this);
+
+ ImageButton previous = (ImageButton) getView().findViewById(R.id.previous);
+ previous.setImageResource(R.drawable.ic_media_previous);
+ getView().findViewById(R.id.previous).setOnClickListener(this);
+
+ ImageButton next = (ImageButton) getView().findViewById(R.id.next);
+ next.setImageResource(R.drawable.ic_media_next);
+ getView().findViewById(R.id.next).setOnClickListener(this);
+
+ mPlayPause = (ImageButton) getView().findViewById(R.id.playpause);
+ mPlayPause.setOnClickListener(this);
+ mPlayPause.setImageResource(R.drawable.ic_media_play);
+
+ getActivity().bindService(
+ new Intent(getActivity(), MediaRouterPlayService.class),
+ mPlayServiceConnection,
+ Context.BIND_AUTO_CREATE
+ );
+ }
+
+ /**
+ * Starts active route discovery (which is automatically stopped on
+ * fragment stop by parent class).
+ */
+ @Override
+ public int onPrepareCallbackFlags() {
+ return MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY
+ | MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN;
+ }
+
+ @Override
+ public Callback onCreateCallback() {
+
+ return new MediaRouter.Callback() {
+ @Override
+ public void onRouteAdded(MediaRouter router, RouteInfo route) {
+ mRouteAdapter.add(route);
+ }
+
+ @Override
+ public void onRouteChanged(MediaRouter router, RouteInfo route) {
+ mRouteAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onRouteRemoved(MediaRouter router, RouteInfo route) {
+ mRouteAdapter.remove(route);
+ }
+
+ @Override
+ public void onRouteSelected(MediaRouter router, RouteInfo route) {
+ }
+
+ @Override
+ public void onRouteUnselected(MediaRouter router, RouteInfo route) {
+ }
+
+ @Override
+ public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) {
+ }
+
+ @Override
+ public void onRoutePresentationDisplayChanged(
+ MediaRouter router, RouteInfo route) {
+ }
+
+ @Override
+ public void onProviderAdded(MediaRouter router, ProviderInfo provider) {
+ }
+
+ @Override
+ public void onProviderRemoved(MediaRouter router, ProviderInfo provider) {
+ }
+
+ @Override
+ public void onProviderChanged(MediaRouter router, ProviderInfo provider) {
+ }
+ };
+
+ }
+
+ /**
+ * Selects a route or starts playback (depending on current ListAdapter).
+ */
+ @Override
+ public void onItemClick(AdapterView> a, View v, final int position, long id) {
+ if (mListView.getAdapter() == mRouteAdapter) {
+ mMediaRouterPlayService.getService().selectRoute(mRouteAdapter.getItem(position));
+ mRouteSelected = true;
+ mListView.setAdapter(mPlaylistAdapter);
+ mControls.setVisibility(View.VISIBLE);
+ if (mStartPlayingOnSelect == -1)
+ mPlaylistAdapter.clear();
+ else {
+ mMediaRouterPlayService.getService().play(mStartPlayingOnSelect);
+ mStartPlayingOnSelect = -1;
+ }
+ }
+ else
+ mMediaRouterPlayService.getService().play(position);
+ }
+
+ /**
+ * Sets colored background on the item that is currently playing.
+ */
+ private void enableTrackHighlight() {
+ if (mListView.getAdapter() == mRouteAdapter)
+ return;
+
+ disableTrackHighlight();
+ mCurrentTrackView = mListView.getChildAt(mMediaRouterPlayService.getService()
+ .getCurrentTrack()
+ - mListView.getFirstVisiblePosition() + mListView.getHeaderViewsCount());
+ if (mCurrentTrackView != null)
+ mCurrentTrackView.setBackgroundColor(
+ getResources().getColor(R.color.currently_playing_background));
+ }
+
+ /**
+ * Removes highlight from the item that was last highlighted.
+ */
+ private void disableTrackHighlight() {
+ if (mCurrentTrackView != null)
+ mCurrentTrackView.setBackgroundColor(Color.TRANSPARENT);
+ }
+
+ /**
+ * Unselects current media renderer if one is selected (with dialog).
+ */
+ @Override
+ public boolean onBackPressed() {
+ if (mListView.getAdapter() == mPlaylistAdapter) {
+ if (mPlaying) {
+ new AlertDialog.Builder(getActivity())
+ .setMessage(R.string.exit_renderer)
+ .setPositiveButton(android.R.string.yes,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ mMediaRouterPlayService.getService().stop();
+ mControls.setVisibility(View.GONE);
+ mListView.setAdapter(mRouteAdapter);
+ disableTrackHighlight();
+ mRouteSelected = false;
+ }
+ })
+ .setNegativeButton(android.R.string.no, null)
+ .show();
+ }
+ else {
+ mControls.setVisibility(View.GONE);
+ mListView.setAdapter(mRouteAdapter);
+ disableTrackHighlight();
+ mRouteSelected = false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Plays/pauses playback on button click.
+ */
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.playpause:
+ if (mPlaying)
+ mMediaRouterPlayService.getService().pause();
+ else
+ mMediaRouterPlayService.getService().resume();
+ break;
+ case R.id.previous:
+ mMediaRouterPlayService.getService().playPrevious();
+ break;
+ case R.id.next:
+ mMediaRouterPlayService.getService().playNext();
+ break;
+ }
+ }
+
+ /**
+ * Sends manual seek on progress bar to renderer.
+ */
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress,
+ boolean fromUser) {
+ if (fromUser)
+ mMediaRouterPlayService.getService().seek(progress);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+
+ /**
+ * Keeps track highlighting on the correct item while views are rebuilt.
+ */
+ @Override
+ public void onScroll(AbsListView arg0, int arg1, int arg2, int arg3) {
+ enableTrackHighlight();
+ }
+
+ @Override
+ public void onScrollStateChanged(AbsListView arg0, int arg1) {
+ enableTrackHighlight();
+ }
+
+ public void increaseVolume() {
+ mMediaRouterPlayService.getService().increaseVolume();
+ }
+
+ public void decreaseVolume() {
+ mMediaRouterPlayService.getService().decreaseVolume();
+ }
+
+ /**
+ * Applies the playlist and starts playing at position.
+ */
+ public void play(List
- playlist, int start) {
+ mPlaylistAdapter.clear();
+ mPlaylistAdapter.addAll(playlist);
+ mMediaRouterPlayService.getService().setPlaylist(playlist);
+
+ if (mRouteSelected)
+ mMediaRouterPlayService.getService().play(start);
+ else {
+ Toast.makeText(getActivity(), R.string.select_renderer, Toast.LENGTH_SHORT)
+ .show();
+ mStartPlayingOnSelect = start;
+ }
+ }
+
+ /**
+ * Receives information from MediaRouterPlayService about playback status.
+ */
+ public void receivePlaybackStatus(MediaItemStatus status) {
+ mProgressBar.setProgress((int) status.getContentPosition() / 1000);
+ mProgressBar.setMax((int) status.getContentDuration() / 1000);
+ if (status.getPlaybackState() == MediaItemStatus.PLAYBACK_STATE_PLAYING ||
+ status.getPlaybackState() == MediaItemStatus.PLAYBACK_STATE_BUFFERING ||
+ status.getPlaybackState() == MediaItemStatus.PLAYBACK_STATE_PENDING) {
+ mPlaying = true;
+ mPlayPause.setImageResource(R.drawable.ic_media_pause);
+ }
+ else {
+ mPlaying = false;
+ mPlayPause.setImageResource(R.drawable.ic_media_play);
+ }
+
+ if (mListView.getAdapter() == mPlaylistAdapter)
+ enableTrackHighlight();
+
+ }
+
+}
diff --git a/src/com/github/nutomic/controldlna/gui/SectionsPagerAdapter.java b/src/com/github/nutomic/controldlna/gui/SectionsPagerAdapter.java
deleted file mode 100644
index 3ba20c8..0000000
--- a/src/com/github/nutomic/controldlna/gui/SectionsPagerAdapter.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.github.nutomic.controldlna.gui;
-
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentStatePagerAdapter;
-
-
-
-//Since this is an object collection, use a FragmentStatePagerAdapter,
-//and NOT a FragmentPagerAdapter.
-public class SectionsPagerAdapter extends FragmentStatePagerAdapter {
-
- /**
- * Fragment for second tab, holding media servers.
- */
- private ServerFragment mServerFragment = new ServerFragment();
-
- /**
- * Fragment for first tab, holding media renderers.
- */
- private RendererFragment mRendererFragment = new RendererFragment();
-
-
- public SectionsPagerAdapter(FragmentManager fm) {
- super(fm);
- }
-
- @Override
- public Fragment getItem(int position) {
- switch (position) {
- case 0: return mServerFragment;
- case 1: return mRendererFragment;
- default: return null;
- }
- }
-
- @Override
- public int getCount() {
- return 2;
- }
-
-}
\ No newline at end of file
diff --git a/src/com/github/nutomic/controldlna/gui/ServerFragment.java b/src/com/github/nutomic/controldlna/gui/ServerFragment.java
index 2d4b136..7706051 100644
--- a/src/com/github/nutomic/controldlna/gui/ServerFragment.java
+++ b/src/com/github/nutomic/controldlna/gui/ServerFragment.java
@@ -50,6 +50,7 @@ import android.view.View;
import android.widget.ListView;
import com.github.nutomic.controldlna.gui.MainActivity.OnBackPressedListener;
+import com.github.nutomic.controldlna.upnp.UpnpController;
import com.github.nutomic.controldlna.utility.DeviceArrayAdapter;
import com.github.nutomic.controldlna.utility.FileArrayAdapter;
@@ -92,6 +93,11 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene
* Holds the scroll position in the list view at each directory level.
*/
private Stack mListState = new Stack();
+
+ /**
+ * Manages all UPNP connections including playback.
+ */
+ private UpnpController mController = new UpnpController();
/**
* Initializes ListView adapters, launches Cling UPNP service.
@@ -104,8 +110,26 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene
mServerAdapter = new DeviceArrayAdapter(
getActivity(), DeviceArrayAdapter.SERVER);
setListAdapter(mServerAdapter);
- MainActivity activity = (MainActivity) getActivity();
- activity.getUpnpPlayer().getDeviceListener().addCallback(mServerAdapter);
+ mController.open(getActivity());
+ mController.addCallback(mServerAdapter);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mController.startSearch();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mController.stopSearch();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mController.close(getActivity());
}
/**
@@ -119,9 +143,8 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene
getFiles(ROOT_DIRECTORY);
}
else if (getListAdapter() == mFileAdapter) {
- if (mFileAdapter.getItem(position) instanceof Container) {
- getFiles(((Container) mFileAdapter.getItem(position)).getId());
- }
+ if (mFileAdapter.getItem(position) instanceof Container)
+ getFiles(((Container) mFileAdapter.getItem(position)).getId());
else {
List
- playlist = new ArrayList
- ();
for (int i = 0; i < mFileAdapter.getCount(); i++) {
@@ -129,9 +152,7 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene
playlist.add((Item) mFileAdapter.getItem(i));
}
MainActivity activity = (MainActivity) getActivity();
- activity.switchToRendererTab();
- activity.getUpnpPlayer().getPlayService()
- .setPlaylist(playlist, position);
+ activity.play(playlist, position);
}
}
}
@@ -154,8 +175,7 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene
private void getFiles(final boolean restoreListState) {
Service, ?> service = mCurrentServer.findService(
new ServiceType("schemas-upnp-org", "ContentDirectory"));
- MainActivity activity = (MainActivity) getActivity();
- activity.getUpnpPlayer().execute(new Browse(service,
+ mController.execute(new Browse(service,
mCurrentPath.peek(), BrowseFlag.DIRECT_CHILDREN) {
@SuppressWarnings("rawtypes")
diff --git a/src/com/github/nutomic/controldlna/mediarouter/MediaRouterPlayService.java b/src/com/github/nutomic/controldlna/mediarouter/MediaRouterPlayService.java
new file mode 100644
index 0000000..21fcc62
--- /dev/null
+++ b/src/com/github/nutomic/controldlna/mediarouter/MediaRouterPlayService.java
@@ -0,0 +1,313 @@
+/*
+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.List;
+
+import org.teleal.cling.support.contentdirectory.DIDLParser;
+import org.teleal.cling.support.model.DIDLContent;
+import org.teleal.cling.support.model.DIDLObject;
+import org.teleal.cling.support.model.item.Item;
+import org.teleal.cling.support.model.item.MusicTrack;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.support.v4.app.NotificationCompat;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaItemStatus;
+import android.support.v7.media.MediaRouter;
+import android.support.v7.media.MediaRouter.ControlRequestCallback;
+import android.support.v7.media.MediaRouter.RouteInfo;
+import android.util.Log;
+
+import com.github.nutomic.controldlna.R;
+import com.github.nutomic.controldlna.gui.MainActivity;
+import com.github.nutomic.controldlna.gui.RouteFragment;
+import com.github.nutomic.controldlna.utility.LoadImageTask;
+
+/**
+ * Background service that handles media playback to a single UPNP media renderer.
+ *
+ * @author Felix Ableitner
+ *
+ */
+public class MediaRouterPlayService extends Service {
+
+ private static final String TAG = "PlayService";
+
+ private static final int NOTIFICATION_ID = 1;
+
+ private final MediaRouterPlayServiceBinder mBinder = new MediaRouterPlayServiceBinder(this);
+
+ private MediaRouter mMediaRouter;
+
+ /**
+ * Media items that should be played.
+ */
+ private List
- mPlaylist = new ArrayList
- ();
+
+ /**
+ * The track that is currently being played.
+ */
+ private int mCurrentTrack;
+
+ private String mItemId;
+
+ private String mSessionId;
+
+ private WeakReference mRendererFragment = new WeakReference(null);
+
+ private boolean mPollingStatus = false;
+
+ /**
+ * Creates a notification after the icon bitmap is loaded.
+ */
+ private class CreateNotificationTask extends LoadImageTask {
+
+ @Override
+ protected void onPostExecute(Bitmap result) {
+ String title = "";
+ String artist = "";
+ if (mCurrentTrack < mPlaylist.size()) {
+ title = mPlaylist.get(mCurrentTrack).getTitle();
+ if (mPlaylist.get(mCurrentTrack) instanceof MusicTrack) {
+ MusicTrack track = (MusicTrack) mPlaylist.get(mCurrentTrack);
+ artist = track.getArtists()[0].getName();
+ }
+ }
+ Notification notification = new NotificationCompat.Builder(MediaRouterPlayService.this)
+ .setContentIntent(PendingIntent.getActivity(MediaRouterPlayService.this, 0,
+ new Intent(MediaRouterPlayService.this, MainActivity.class), 0))
+ .setContentTitle(title)
+ .setContentText(artist)
+ .setLargeIcon(result)
+ .setSmallIcon(R.drawable.ic_launcher)
+ .build();
+ NotificationManager notificationManager =
+ (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ notificationManager.notify(NOTIFICATION_ID, notification);
+ notification.flags |= Notification.FLAG_ONGOING_EVENT;
+ }
+
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ mMediaRouter = MediaRouter.getInstance(this);
+ pollStatus();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ public void setRendererFragment(RouteFragment rf) {
+ mRendererFragment = new WeakReference(rf);
+ }
+
+ public void selectRoute(RouteInfo route) {
+ mMediaRouter.selectRoute(route);
+ }
+
+ public void sendControlRequest(Intent intent) {
+ mMediaRouter.getSelectedRoute().sendControlRequest(intent, null);
+ }
+
+ /**
+ * Sets current track in renderer to specified item in playlist, then
+ * starts playback.
+ */
+ public void play(int trackNumber) {
+ if (trackNumber < 0 || trackNumber >= mPlaylist.size())
+ return;
+
+ updateNotification();
+
+ mCurrentTrack = trackNumber;
+ Item track = mPlaylist.get(trackNumber);
+ DIDLParser parser = new DIDLParser();
+ DIDLContent didl = new DIDLContent();
+ didl.addItem(track);
+ String metadata = "";
+ try {
+ metadata = parser.generate(didl, true);
+ }
+ catch (Exception e) {
+ Log.w(TAG, "Metadata generation failed", e);
+ }
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_PLAY);
+ intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ intent.setData(Uri.parse(track.getFirstResource().getValue()));
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_METADATA, metadata);
+
+ mMediaRouter.getSelectedRoute().sendControlRequest(intent,
+ new ControlRequestCallback() {
+ @Override
+ public void onResult(Bundle data) {
+ mSessionId = data.getString(MediaControlIntent.EXTRA_SESSION_ID);
+ mItemId = data.getString(MediaControlIntent.EXTRA_ITEM_ID);
+ mPollingStatus = true;
+ }
+ });
+ }
+
+ private void updateNotification() {
+ new CreateNotificationTask().execute(mPlaylist.get(mCurrentTrack)
+ .getFirstPropertyValue(DIDLObject.Property.UPNP.ALBUM_ART_URI.class));
+ }
+
+ /**
+ * Sends 'pause' signal to current renderer.
+ */
+ public void pause() {
+ Intent intent = new Intent(MediaControlIntent.ACTION_PAUSE);
+ intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mSessionId);
+ mMediaRouter.getSelectedRoute().sendControlRequest(intent, null);
+ }
+
+ /**
+ * Sends 'resume' signal to current renderer.
+ */
+ public void resume() {
+ Intent intent = new Intent(MediaControlIntent.ACTION_RESUME);
+ intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mSessionId);
+ mMediaRouter.getSelectedRoute().sendControlRequest(intent, null);
+ mPollingStatus = true;
+ }
+
+ /**
+ * Sends 'stop' signal to current renderer.
+ */
+ public void stop() {
+ Intent intent = new Intent(MediaControlIntent.ACTION_STOP);
+ intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mSessionId);
+ mMediaRouter.getSelectedRoute().sendControlRequest(intent, null);
+ }
+
+ public void seek(int seconds) {
+ Intent intent = new Intent(MediaControlIntent.ACTION_SEEK);
+ intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mSessionId);
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, mItemId);
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION,
+ (long) seconds * 1000);
+ mMediaRouter.getSelectedRoute().sendControlRequest(intent, null);
+ }
+
+ /**
+ * Sets a new playlist and starts playing.
+ *
+ * @param playlist The media files in the playlist.
+ */
+ public void setPlaylist(List
- playlist) {
+ mPlaylist = playlist;
+ }
+
+ public void playNext() {
+ play(mCurrentTrack + 1);
+ }
+
+ public void playPrevious() {
+ play(mCurrentTrack - 1);
+ }
+
+ public int getCurrentTrack() {
+ return mCurrentTrack;
+ }
+
+ public RouteInfo getDefaultRoute() {
+ return mMediaRouter.getDefaultRoute();
+ }
+
+ /**
+ * Requests playback information every second, as long as RendererFragment
+ * is attached or media is playing.
+ */
+ private void pollStatus() {
+ if (mPollingStatus && mSessionId != null && mItemId != null) {
+ Intent i = new Intent();
+ i.setAction(MediaControlIntent.ACTION_GET_STATUS);
+ i.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mSessionId);
+ i.putExtra(MediaControlIntent.EXTRA_ITEM_ID, mItemId);
+ mMediaRouter.getSelectedRoute().sendControlRequest(i,
+ new ControlRequestCallback() {
+ @Override
+ public void onResult(Bundle data) {
+ MediaItemStatus status = MediaItemStatus.fromBundle(data);
+ if (mRendererFragment.get() != null)
+ mRendererFragment.get().receivePlaybackStatus(status);
+
+ if (status.getPlaybackState() == MediaItemStatus.PLAYBACK_STATE_FINISHED) {
+ if (mCurrentTrack + 1 < mPlaylist.size())
+ playNext();
+ else {
+ mPollingStatus = false;
+ NotificationManager notificationManager =
+ (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ notificationManager.cancel(NOTIFICATION_ID);
+ }
+ }
+ }
+ });
+ }
+
+ new Handler().postDelayed(new Runnable() {
+
+ @Override
+ public void run() {
+ pollStatus();
+ }
+ }, 1000);
+ }
+
+ public void increaseVolume() {
+ mMediaRouter.getSelectedRoute().requestUpdateVolume(1);
+ }
+
+ public void decreaseVolume() {
+ mMediaRouter.getSelectedRoute().requestUpdateVolume(-1);
+ }
+
+}
diff --git a/src/com/github/nutomic/controldlna/upnp/PlayServiceBinder.java b/src/com/github/nutomic/controldlna/mediarouter/MediaRouterPlayServiceBinder.java
similarity index 86%
rename from src/com/github/nutomic/controldlna/upnp/PlayServiceBinder.java
rename to src/com/github/nutomic/controldlna/mediarouter/MediaRouterPlayServiceBinder.java
index 76f7090..eec2222 100644
--- a/src/com/github/nutomic/controldlna/upnp/PlayServiceBinder.java
+++ b/src/com/github/nutomic/controldlna/mediarouter/MediaRouterPlayServiceBinder.java
@@ -25,7 +25,7 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package com.github.nutomic.controldlna.upnp;
+package com.github.nutomic.controldlna.mediarouter;
import android.os.Binder;
@@ -35,15 +35,15 @@ import android.os.Binder;
* @author Felix Ableitner
*
*/
-public class PlayServiceBinder extends Binder {
+public class MediaRouterPlayServiceBinder extends Binder {
- PlayService mService;
+ MediaRouterPlayService mService;
- public PlayServiceBinder(PlayService service) {
+ public MediaRouterPlayServiceBinder(MediaRouterPlayService service) {
mService = service;
}
- public PlayService getService() {
+ public MediaRouterPlayService getService() {
return mService;
}
}
diff --git a/src/com/github/nutomic/controldlna/mediarouter/RemotePlayService.java b/src/com/github/nutomic/controldlna/mediarouter/RemotePlayService.java
deleted file mode 100644
index 95770e5..0000000
--- a/src/com/github/nutomic/controldlna/mediarouter/RemotePlayService.java
+++ /dev/null
@@ -1,252 +0,0 @@
-/*
-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;
- }
- }
-
- @Override
- public void deviceUpdated(Device, ?, ?> device) {
- // No need to update as the parameters we need are already known.
- }
-
-}
diff --git a/src/com/github/nutomic/controldlna/upnp/DeviceListener.java b/src/com/github/nutomic/controldlna/upnp/DeviceListener.java
deleted file mode 100644
index 179bbeb..0000000
--- a/src/com/github/nutomic/controldlna/upnp/DeviceListener.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
-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.upnp;
-
-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);
- public void deviceUpdated(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) {
- for (DeviceListenerCallback l : mListeners) {
- l.deviceUpdated(device);
- }
- }
-
-}
diff --git a/src/com/github/nutomic/controldlna/upnp/IRemotePlayService.aidl b/src/com/github/nutomic/controldlna/upnp/IRemotePlayService.aidl
new file mode 100644
index 0000000..a50fa19
--- /dev/null
+++ b/src/com/github/nutomic/controldlna/upnp/IRemotePlayService.aidl
@@ -0,0 +1,44 @@
+/*
+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.upnp;
+
+import android.os.Messenger;
+
+interface IRemotePlayService {
+ void startSearch(in Messenger listener);
+ void stopSearch();
+ void selectRenderer(String id);
+ void unselectRenderer(String id);
+ void setVolume(int volume);
+ void play(String uri, String metadata);
+ void pause(String sessionId);
+ void resume(String sessionId);
+ void stop(String sessionId);
+ void seek(String sessionId, String itemId, long milliseconds);
+ void getItemStatus(String sessionId, String itemId, int requestHash);
+}
\ No newline at end of file
diff --git a/src/com/github/nutomic/controldlna/upnp/PlayService.java b/src/com/github/nutomic/controldlna/upnp/PlayService.java
deleted file mode 100644
index 11b29a5..0000000
--- a/src/com/github/nutomic/controldlna/upnp/PlayService.java
+++ /dev/null
@@ -1,414 +0,0 @@
-/*
-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.upnp;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-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;
-import org.teleal.cling.model.gena.GENASubscription;
-import org.teleal.cling.model.message.UpnpResponse;
-import org.teleal.cling.model.meta.Device;
-import org.teleal.cling.model.state.StateVariableValue;
-import org.teleal.cling.model.types.ServiceType;
-import org.teleal.cling.support.avtransport.callback.Pause;
-import org.teleal.cling.support.avtransport.callback.Play;
-import org.teleal.cling.support.avtransport.callback.SetAVTransportURI;
-import org.teleal.cling.support.avtransport.callback.Stop;
-import org.teleal.cling.support.avtransport.lastchange.AVTransportLastChangeParser;
-import org.teleal.cling.support.avtransport.lastchange.AVTransportVariable;
-import org.teleal.cling.support.contentdirectory.DIDLParser;
-import org.teleal.cling.support.lastchange.LastChange;
-import org.teleal.cling.support.model.DIDLContent;
-import org.teleal.cling.support.model.DIDLObject;
-import org.teleal.cling.support.model.item.Item;
-import org.teleal.cling.support.model.item.MusicTrack;
-
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.graphics.Bitmap;
-import android.os.IBinder;
-import android.support.v4.app.NotificationCompat;
-import android.util.Log;
-import android.widget.Toast;
-
-import com.github.nutomic.controldlna.R;
-import com.github.nutomic.controldlna.gui.MainActivity;
-import com.github.nutomic.controldlna.utility.LoadImageTask;
-
-/**
- * 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";
-
- private static final int NOTIFICATION_ID = 1;
-
- private boolean mShowNotification = true;
-
- private final PlayServiceBinder mBinder = new PlayServiceBinder(this);
-
- /**
- * The DLNA media renderer device that is currently active for playback.
- */
- private Device, ?, ?> mRenderer;
-
- /**
- * Media items that should be played.
- */
- private List
- mPlaylist = new ArrayList
- ();
-
- /**
- * The track that is currently being played.
- */
- private int mCurrentTrack;
-
- /**
- * True if a playlist was set with no renderer active.
- */
- private boolean mWaitingForRenderer;
-
- /**
- * Used to determine when the player stops due to the media file being
- * over (so the next one can be played).
- */
- private AtomicBoolean mManuallyStopped = new AtomicBoolean(false);
-
- private org.teleal.cling.model.meta.Service, ?> mAvTransportService;
-
- /**
- * Cling UPNP service.
- */
- private AndroidUpnpService mUpnpService;
-
- /**
- * Cling connection to UPNP service.
- */
- private ServiceConnection mUpnpServiceConnection = new ServiceConnection() {
-
- public void onServiceConnected(ComponentName className, IBinder service) {
- mUpnpService = (AndroidUpnpService) service;
- }
-
- public void onServiceDisconnected(ComponentName className) {
- mUpnpService = null;
- }
- };
-
- private SubscriptionCallback mSubscriptionCallback;
-
- /**
- * Creates a notification after the icon bitmap is loaded.
- *
- * @author Felix
- *
- */
- private class CreateNotificationTask extends LoadImageTask {
-
- @Override
- protected void onPostExecute(Bitmap result) {
- String title = "";
- String artist = "";
- if (mCurrentTrack < mPlaylist.size()) {
- title = mPlaylist.get(mCurrentTrack).getTitle();
- if (mPlaylist.get(mCurrentTrack) instanceof MusicTrack) {
- MusicTrack track = (MusicTrack) mPlaylist.get(mCurrentTrack);
- artist = track.getArtists()[0].getName();
- }
- }
- Notification notification = new NotificationCompat.Builder(PlayService.this)
- .setContentIntent(PendingIntent.getActivity(PlayService.this, 0,
- new Intent(PlayService.this, MainActivity.class), 0))
- .setContentTitle(title)
- .setContentText(artist)
- .setLargeIcon(result)
- .setSmallIcon(R.drawable.ic_launcher)
- .build();
- NotificationManager notificationManager =
- (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
- notificationManager.notify(NOTIFICATION_ID, notification);
- notification.flags |= Notification.FLAG_ONGOING_EVENT;
- }
-
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- getApplicationContext().bindService(
- new Intent(this, AndroidUpnpServiceImpl.class),
- mUpnpServiceConnection,
- Context.BIND_AUTO_CREATE
- );
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return mBinder;
- }
-
- /**
- * Sets current track in renderer to specified item in playlist, then
- * starts playback.
- */
- public void playTrack(int track) {
- if (track < 0 || track >= mPlaylist.size())
- return;
- mCurrentTrack = track;
- DIDLParser parser = new DIDLParser();
- DIDLContent didl = new DIDLContent();
- didl.addItem(mPlaylist.get(track));
- String metadata;
- try {
- metadata = parser.generate(didl, true);
- }
- catch (Exception e) {
- Log.w(TAG, "Metadata serialization failed", e);
- metadata = "NO METADATA";
- }
- setTransportUri(metadata,
- mPlaylist.get(track).getFirstResource().getValue());
- }
-
- private void setTransportUri(String metadata, final String uri) {
- mUpnpService.getControlPoint().execute(new SetAVTransportURI(
- mAvTransportService,
- uri, metadata) {
- @SuppressWarnings("rawtypes")
- @Override
- public void failure(ActionInvocation invocation,
- UpnpResponse operation, String defaultMsg) {
- Log.w(TAG, "Playback failed: " + defaultMsg);
- }
-
- @SuppressWarnings("rawtypes")
- @Override
- public void success(ActionInvocation invocation) {
- play();
- }
- });
- }
-
- private void updateNotification() {
- 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() {
- updateNotification();
- mUpnpService.getControlPoint().execute(
- new Play(mAvTransportService) {
-
- @SuppressWarnings("rawtypes")
- @Override
- public void failure(ActionInvocation invocation,
- UpnpResponse operation, String defaultMessage) {
- Log.w(TAG, "Play failed: " + defaultMessage);
- }
- });
- }
-
- /**
- * Sends 'pause' signal to current renderer.
- */
- public void pause() {
- mManuallyStopped.set(true);
- mUpnpService.getControlPoint().execute(
- new Pause(mAvTransportService) {
-
- @SuppressWarnings("rawtypes")
- @Override
- public void failure(ActionInvocation invocation,
- UpnpResponse operation, String defaultMessage) {
- Log.w(TAG, "Pause failed, trying stop: " + defaultMessage);
- // Sometimes stop works even though pause does not.
- 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();
- if (mRenderer != null && !renderer.equals(mRenderer))
- pause();
-
- mRenderer = renderer;
- mAvTransportService = mRenderer.findService(
- new ServiceType("schemas-upnp-org", "AVTransport"));
- mSubscriptionCallback = new SubscriptionCallback(
- mAvTransportService, 600) {
-
- @SuppressWarnings("rawtypes")
- @Override
- protected void established(GENASubscription sub) {
- }
-
- @SuppressWarnings("rawtypes")
- @Override
- protected void ended(GENASubscription sub, CancelReason reason,
- UpnpResponse response) {
- }
-
- @SuppressWarnings("rawtypes")
- @Override
- protected void eventReceived(final GENASubscription sub) {
- @SuppressWarnings("unchecked")
- Map m = sub.getCurrentValues();
- try {
- LastChange lastChange = new LastChange(
- new AVTransportLastChangeParser(),
- m.get("LastChange").toString());
- switch (lastChange.getEventedValue(0,
- AVTransportVariable.TransportState.class)
- .getValue()) {
- case PLAYING:
- break;
- case STOPPED:
- if (!mManuallyStopped.get() &&
- (mPlaylist.size() > mCurrentTrack + 1)) {
- mManuallyStopped.set(false);
- playTrack(mCurrentTrack +1);
- break;
- }
- // fallthrough
- case PAUSED_PLAYBACK:
- NotificationManager notificationManager =
- (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
- notificationManager.cancel(NOTIFICATION_ID);
- mManuallyStopped.set(false);
- break;
- default:
- }
-
- } catch (Exception e) {
- Log.w(TAG, "Failed to parse UPNP event", e);
- }
- }
-
- @SuppressWarnings("rawtypes")
- @Override
- protected void eventsMissed(GENASubscription sub,
- int numberOfMissedEvents) {
- }
-
- @SuppressWarnings("rawtypes")
- @Override
- protected void failed(GENASubscription sub, UpnpResponse responseStatus,
- Exception exception, String defaultMsg) {
- Log.d(TAG, defaultMsg);
- }
- };
- mUpnpService.getControlPoint().execute(mSubscriptionCallback);
- if (mWaitingForRenderer)
- playTrack(mCurrentTrack);
- }
-
- public Device, ?, ?> getRenderer() {
- return mRenderer;
- }
-
- /**
- * Sets a new playlist and starts playing.
- *
- * @param playlist The media files in the playlist.
- * @param first Index of the first file to play.
- */
- public void setPlaylist(List
- playlist, int first) {
- mPlaylist = playlist;
- if (mRenderer == null) {
- mWaitingForRenderer = true;
- Toast.makeText(this, R.string.select_renderer, Toast.LENGTH_SHORT)
- .show();
- mCurrentTrack = first;
- }
- else
- playTrack(first);
- }
-
- public void playNext() {
- playTrack(mCurrentTrack + 1);
- }
-
- public void playPrevious() {
- playTrack(mCurrentTrack - 1);
- }
-
- public List
- getPlaylist() {
- return mPlaylist;
- }
-
- public int getCurrentTrack() {
- return mCurrentTrack;
- }
-
- public void setShowNotification(boolean value) {
- mShowNotification = value;
- }
-
-}
diff --git a/src/com/github/nutomic/controldlna/mediarouter/Provider.java b/src/com/github/nutomic/controldlna/upnp/Provider.java
similarity index 66%
rename from src/com/github/nutomic/controldlna/mediarouter/Provider.java
rename to src/com/github/nutomic/controldlna/upnp/Provider.java
index de21fcc..0d8fcc0 100644
--- a/src/com/github/nutomic/controldlna/mediarouter/Provider.java
+++ b/src/com/github/nutomic/controldlna/upnp/Provider.java
@@ -25,12 +25,13 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package com.github.nutomic.controldlna.mediarouter;
+package com.github.nutomic.controldlna.upnp;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map.Entry;
+import java.util.Random;
import android.content.ComponentName;
import android.content.Context;
@@ -53,6 +54,8 @@ 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;
+import android.util.Pair;
+import android.util.SparseArray;
/**
* Allows playing to a DLNA renderer from a remote app.
@@ -67,6 +70,10 @@ final class Provider extends MediaRouteProvider {
// Device has been removed.
// param: int id
public static final int MSG_RENDERER_REMOVED = 2;
+ // Playback status information, retrieved after RemotePlayService.MSG_GET_STATUS.
+ // param: bundle media_item_status
+ // param: int hash
+ public static final int MSG_STATUS_INFO = 3;
/**
* Allows passing and storing basic information about a device.
@@ -124,29 +131,23 @@ final class Provider extends MediaRouteProvider {
private HashMap mDevices = new HashMap();
- private Messenger mRemotePlayService;
+ private SparseArray> mRequests =
+ new SparseArray>();
+
+ IRemotePlayService mIRemotePlayService;
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();
- }
+ mIRemotePlayService = IRemotePlayService.Stub.asInterface(service);
}
public void onServiceDisconnected(ComponentName className) {
- mRemotePlayService = null;
+ mIRemotePlayService = 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);
@@ -179,9 +180,8 @@ final class Provider extends MediaRouteProvider {
@Override
public void handleMessage(Message msg) {
- if (mService.get() != null) {
+ if (mService.get() != null)
mService.get().handleMessage(msg);
- }
}
}
@@ -202,22 +202,14 @@ final class Provider extends MediaRouteProvider {
@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();
- }
+ try {
+ if (request != null && request.isActiveScan())
+ mIRemotePlayService.startSearch(mListener);
+ else
+ mIRemotePlayService.stopSearch();
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
}
@Override
@@ -260,88 +252,115 @@ final class Provider extends MediaRouteProvider {
@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();
- }
+ mIRemotePlayService.selectRenderer(mRouteId);
+ } 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();
- }
+ mIRemotePlayService.unselectRenderer(mRouteId);
+ } 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;
+ if (volume < 0 || volume > mDevices.get(mRouteId).volumeMax)
+ return;
+
try {
- mRemotePlayService.send(msg);
- } catch (RemoteException e) {
- e.printStackTrace();
- }
+ mIRemotePlayService.setVolume(volume);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ mDevices.get(mRouteId).volume = volume;
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();
+ onSetVolume(mDevices.get(mRouteId).volume + delta);
}
+ /**
+ * Handles play, pause, resume, stop, seek and get_status requests for this route.
+ */
@Override
public boolean onControlRequest(Intent intent, ControlRequestCallback callback) {
- try {
+ 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);
+ String metadata = (intent.hasExtra(MediaControlIntent.EXTRA_ITEM_METADATA))
+ ? intent.getExtras().getString(MediaControlIntent.EXTRA_ITEM_METADATA)
+ : null;
+ mIRemotePlayService.play(intent.getDataString(), metadata);
+ // Store in intent extras for later.
+ intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mRouteId);
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, intent.getDataString());
+ getItemStatus(intent, callback);
return true;
}
else if (intent.getAction().equals(MediaControlIntent.ACTION_PAUSE)) {
- Message msg = Message.obtain(null, RemotePlayService.MSG_PAUSE, 0, 0);
- mRemotePlayService.send(msg);
+ mIRemotePlayService.pause(mRouteId);
return true;
}
- else if (intent.getAction().equals(MediaControlIntent.ACTION_PLAY)) {
- Message msg = Message.obtain(null, RemotePlayService.MSG_STOP, 0, 0);
- mRemotePlayService.send(msg);
+ else if (intent.getAction().equals(MediaControlIntent.ACTION_RESUME)) {
+ mIRemotePlayService.resume(mRouteId);
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);
+ else if (intent.getAction().equals(MediaControlIntent.ACTION_STOP)) {
+ mIRemotePlayService.stop(mRouteId);
return true;
}
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- return false;
+ else if (intent.getAction().equals(MediaControlIntent.ACTION_SEEK)) {
+ mIRemotePlayService.seek(mRouteId,
+ intent.getStringExtra(
+ MediaControlIntent.EXTRA_ITEM_ID),
+ intent.getLongExtra(
+ MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0));
+ getItemStatus(intent, callback);
+ return true;
+ }
+ else if(intent.getAction().equals(MediaControlIntent.ACTION_GET_STATUS)) {
+ getItemStatus(intent, callback);
+ return true;
+ }
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ return false;
}
}
+ /**
+ * Requests status info via RemotePlayService, stores intent and callback to
+ * access later in handleMessage.
+ */
+ private void getItemStatus(Intent intent, ControlRequestCallback callback)
+ throws RemoteException {
+ if (callback == null)
+ return;
+
+ Pair pair =
+ new Pair(intent, callback);
+ int r = new Random().nextInt();
+ mRequests.put(r, pair);
+ mIRemotePlayService.getItemStatus(
+ intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID),
+ intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID),
+ r);
+ }
+
+ /**
+ * Handles device add and remove as well as sending status info requested earlier.
+ */
public void handleMessage(Message msg) {
Bundle data = msg.getData();
- switch (msg.what) {
+ switch (msg.what) {
case MSG_RENDERER_ADDED:
msg.getData().setClassLoader(Device.class.getClassLoader());
Device device = (Device) data.getParcelable("device");
@@ -352,6 +371,20 @@ final class Provider extends MediaRouteProvider {
mDevices.remove(data.getString("id"));
updateRoutes();
break;
+ case MSG_STATUS_INFO:
+ Pair pair =
+ mRequests.get(data.getInt("hash"));
+ Bundle status = data.getBundle("media_item_status");
+
+ if (pair.first.hasExtra(MediaControlIntent.EXTRA_SESSION_ID)) {
+ status.putString(MediaControlIntent.EXTRA_SESSION_ID,
+ pair.first.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID));
+ }
+ if (pair.first.hasExtra(MediaControlIntent.EXTRA_ITEM_ID)) {
+ status.putString(MediaControlIntent.EXTRA_ITEM_ID,
+ pair.first.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID));
+ }
+ pair.second.onResult(status);
}
}
diff --git a/src/com/github/nutomic/controldlna/mediarouter/ProviderService.java b/src/com/github/nutomic/controldlna/upnp/ProviderService.java
similarity index 97%
rename from src/com/github/nutomic/controldlna/mediarouter/ProviderService.java
rename to src/com/github/nutomic/controldlna/upnp/ProviderService.java
index ad5798f..1d77864 100644
--- a/src/com/github/nutomic/controldlna/mediarouter/ProviderService.java
+++ b/src/com/github/nutomic/controldlna/upnp/ProviderService.java
@@ -25,7 +25,7 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package com.github.nutomic.controldlna.mediarouter;
+package com.github.nutomic.controldlna.upnp;
import android.support.v7.media.MediaRouteProvider;
import android.support.v7.media.MediaRouteProviderService;
diff --git a/src/com/github/nutomic/controldlna/upnp/RemotePlayService.java b/src/com/github/nutomic/controldlna/upnp/RemotePlayService.java
new file mode 100644
index 0000000..11e9459
--- /dev/null
+++ b/src/com/github/nutomic/controldlna/upnp/RemotePlayService.java
@@ -0,0 +1,469 @@
+/*
+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.upnp;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.teleal.cling.controlpoint.SubscriptionCallback;
+import org.teleal.cling.model.action.ActionInvocation;
+import org.teleal.cling.model.gena.CancelReason;
+import org.teleal.cling.model.gena.GENASubscription;
+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.model.state.StateVariableValue;
+import org.teleal.cling.support.avtransport.callback.GetPositionInfo;
+import org.teleal.cling.support.avtransport.callback.Pause;
+import org.teleal.cling.support.avtransport.callback.Play;
+import org.teleal.cling.support.avtransport.callback.Seek;
+import org.teleal.cling.support.avtransport.callback.SetAVTransportURI;
+import org.teleal.cling.support.avtransport.callback.Stop;
+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.renderingcontrol.callback.GetVolume;
+import org.teleal.cling.support.renderingcontrol.callback.SetVolume;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.support.v7.media.MediaItemStatus;
+import android.support.v7.media.MediaItemStatus.Builder;
+import android.util.Log;
+
+import com.github.nutomic.controldlna.upnp.UpnpController.DeviceListenerCallback;
+
+
+/**
+ * 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";
+
+ private Messenger mListener;
+
+ private HashMap> mDevices = new HashMap>();
+
+ private Device, ?, ?> mCurrentRenderer;
+
+ private int mPlaybackState;
+
+ private boolean mManuallyStopped;
+
+ private UpnpController mUpnpController = new UpnpController();
+
+ /**
+ * Receives events from current renderer.
+ */
+ private SubscriptionCallback mSubscriptionCallback;
+
+ private final IRemotePlayService.Stub mBinder = new IRemotePlayService.Stub() {
+
+ @Override
+ public void startSearch(Messenger listener)
+ throws RemoteException {
+ mUpnpController.startSearch();
+ mListener = listener;
+ }
+
+ @Override
+ public void stopSearch() throws RemoteException {
+ mUpnpController.stopSearch();
+ }
+
+ @Override
+ public void selectRenderer(String id) throws RemoteException {
+ mCurrentRenderer = mDevices.get(id);
+ mSubscriptionCallback = new SubscriptionCallback(
+ UpnpController.getService(mCurrentRenderer, "AVTransport"), 600) {
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ protected void established(GENASubscription sub) {
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ protected void ended(GENASubscription sub, CancelReason reason,
+ UpnpResponse response) {
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ protected void eventReceived(final GENASubscription sub) {
+ @SuppressWarnings("unchecked")
+ Map m = sub.getCurrentValues();
+ try {
+ LastChange lastChange = new LastChange(
+ new AVTransportLastChangeParser(),
+ m.get("LastChange").toString());
+ switch (lastChange.getEventedValue(0,
+ AVTransportVariable.TransportState.class)
+ .getValue()) {
+ case PLAYING:
+ mPlaybackState = MediaItemStatus.PLAYBACK_STATE_PLAYING;
+ break;
+ case PAUSED_PLAYBACK:
+ mPlaybackState = MediaItemStatus.PLAYBACK_STATE_PAUSED;
+ break;
+ case STOPPED:
+ if (mManuallyStopped) {
+ mManuallyStopped = false;
+ mPlaybackState = MediaItemStatus.PLAYBACK_STATE_CANCELED;
+ }
+ else
+ mPlaybackState = MediaItemStatus.PLAYBACK_STATE_FINISHED;
+ break;
+ case TRANSITIONING:
+ mPlaybackState = MediaItemStatus.PLAYBACK_STATE_PENDING;
+ break;
+ case NO_MEDIA_PRESENT:
+ mPlaybackState = MediaItemStatus.PLAYBACK_STATE_ERROR;
+ break;
+ default:
+ }
+
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to parse UPNP event", e);
+ }
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ protected void eventsMissed(GENASubscription sub,
+ int numberOfMissedEvents) {
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ protected void failed(GENASubscription sub, UpnpResponse responseStatus,
+ Exception exception, String defaultMsg) {
+ }
+ };
+ mUpnpController.execute(mSubscriptionCallback);
+ }
+
+ @Override
+ public void unselectRenderer(String sessionId) throws RemoteException {
+ stop(sessionId);
+ mSubscriptionCallback.end();
+ mCurrentRenderer = null;
+ }
+
+ /**
+ * Sets an absolute volume. The value is assumed to be within the valid
+ * volume range.
+ */
+ @Override
+ public void setVolume(int volume) throws RemoteException {
+ mUpnpController.execute(
+ new SetVolume(UpnpController.getService(mCurrentRenderer,
+ "RenderingControl"), volume) {
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void failure(ActionInvocation invocation,
+ UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Failed to set new Volume: " + defaultMessage);
+ }
+ });
+ }
+
+ /**
+ * Sets playback source and metadata, then starts playing on
+ * current renderer.
+ */
+ @Override
+ public void play(String uri, String metadata) throws RemoteException {
+ mUpnpController.execute(new SetAVTransportURI(
+ UpnpController.getService(mCurrentRenderer, "AVTransport"),
+ uri, metadata) {
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void failure(ActionInvocation invocation,
+ UpnpResponse operation, String defaultMsg) {
+ Log.w(TAG, "Playback failed: " + defaultMsg);
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void success(ActionInvocation invocation) {
+ mUpnpController.execute(
+ new Play(UpnpController.getService(mCurrentRenderer,
+ "AVTransport")) {
+
+ @Override
+ public void failure(ActionInvocation invocation,
+ UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Play failed: " + defaultMessage);
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Pauses playback on current renderer.
+ */
+ @Override
+ public void pause(final String sessionId) throws RemoteException {
+ mUpnpController.execute(
+ new Pause(UpnpController.getService(mDevices.get(sessionId),
+ "AVTransport")) {
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void failure(ActionInvocation invocation,
+ UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Pause failed, trying stop: " + defaultMessage);
+ // Sometimes stop works even though pause does not.
+ try {
+ stop(sessionId);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void resume(String sessionId) throws RemoteException {
+ mUpnpController.execute(
+ new Play(UpnpController.getService(mDevices.get(sessionId),
+ "AVTransport")) {
+
+ @Override
+ @SuppressWarnings("rawtypes")
+ public void failure(ActionInvocation invocation,
+ UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Play failed: " + defaultMessage);
+ }
+ });
+ }
+
+ /**
+ * Stops playback on current renderer.
+ */
+ @Override
+ public void stop(String sessionId) throws RemoteException {
+ mManuallyStopped = true;
+ mUpnpController.execute(
+ new Stop(UpnpController.getService(mDevices.get(sessionId),
+ "AVTransport")) {
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void failure(ActionInvocation invocation,
+ org.teleal.cling.model.message.UpnpResponse operation,
+ String defaultMessage) {
+ Log.w(TAG, "Stop failed: " + defaultMessage);
+ }
+ });
+ }
+
+ /**
+ * Seeks to the given absolute time in seconds.
+ */
+ @Override
+ public void seek(String sessionId, String itemId, long milliseconds)
+ throws RemoteException {
+ mUpnpController.execute(new Seek(
+ UpnpController.getService(mDevices.get(sessionId), "AVTransport"),
+ SeekMode.REL_TIME,
+ Integer.toString((int) milliseconds / 1000)) {
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void failure(ActionInvocation invocation,
+ UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Seek failed: " + defaultMessage);
+ }
+ });
+ }
+
+ /**
+ * Sends a message with current status for the route and item.
+ *
+ * If itemId does not match with the item currently played,
+ * MediaItemStatus.PLAYBACK_STATE_INVALIDATED is returned.
+ *
+ * @param sessionId Identifier of the session (equivalent to route) to get info for.
+ * @param itemId Identifier of the item to get info for.
+ * @param requestHash Passed back in message to find original request object.
+ */
+ @Override
+ public void getItemStatus(String sessionId, final String itemId, final int requestHash)
+ throws RemoteException {
+ mUpnpController.execute(new GetPositionInfo(
+ UpnpController.getService(mDevices.get(sessionId), "AVTransport")) {
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void failure(ActionInvocation invocation,
+ UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Get position failed: " + defaultMessage);
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void received(ActionInvocation invocation, PositionInfo positionInfo) {
+ Message msg = Message.obtain(null, Provider.MSG_STATUS_INFO, 0, 0);
+ Builder status = null;
+
+ if (positionInfo.getTrackURI().equals(itemId)) {
+ status = new MediaItemStatus.Builder(mPlaybackState)
+ .setContentPosition(positionInfo.getTrackElapsedSeconds() * 1000)
+ .setContentDuration(positionInfo.getTrackDurationSeconds() * 1000)
+ .setTimestamp(positionInfo.getAbsCount());
+ }
+ else {
+ status = new MediaItemStatus.Builder(
+ MediaItemStatus.PLAYBACK_STATE_INVALIDATED);
+ }
+
+ msg.getData().putBundle("media_item_status", status.build().asBundle());
+ msg.getData().putInt("hash", requestHash);
+
+ try {
+ mListener.send(msg);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+ };
+
+ @Override
+ public IBinder onBind(Intent itnent) {
+ return mBinder;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mUpnpController.open(this);
+ mUpnpController.addCallback(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mUpnpController.close(this);
+ }
+
+ /**
+ * Gather device data and send it to Provider.
+ */
+ @Override
+ public void deviceAdded(final Device, ?, ?> device) {
+ final org.teleal.cling.model.meta.Service, ?> rc = UpnpController.getService(device, "RenderingControl");
+ if (rc == null || mListener == null)
+ return;
+
+ if (device.getType().getType().equals("MediaRenderer") &&
+ device instanceof RemoteDevice) {
+ mDevices.put(device.getIdentity().getUdn().toString(), device);
+
+ mUpnpController.execute(
+ new GetVolume(rc) {
+
+ @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 (rc.getStateVariable("Volume") != null) {
+ StateVariableAllowedValueRange volumeRange =
+ rc.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();
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Remove the device from Provider.
+ */
+ @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();
+ }
+ }
+ }
+
+ /**
+ * If a device was updated, we just add it again (devices are stored in
+ * maps, so adding the same one again just overwrites the old one).
+ */
+ @Override
+ public void deviceUpdated(Device, ?, ?> device) {
+ deviceAdded(device);
+ }
+}
diff --git a/src/com/github/nutomic/controldlna/upnp/UpnpController.java b/src/com/github/nutomic/controldlna/upnp/UpnpController.java
index 9864415..d2996ad 100644
--- a/src/com/github/nutomic/controldlna/upnp/UpnpController.java
+++ b/src/com/github/nutomic/controldlna/upnp/UpnpController.java
@@ -27,6 +27,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package com.github.nutomic.controldlna.upnp;
+import java.util.ArrayList;
+
import org.teleal.cling.android.AndroidUpnpService;
import org.teleal.cling.android.AndroidUpnpServiceImpl;
import org.teleal.cling.controlpoint.ActionCallback;
@@ -36,6 +38,8 @@ 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 org.teleal.cling.registry.Registry;
+import org.teleal.cling.registry.RegistryListener;
import android.content.ComponentName;
import android.content.Context;
@@ -50,32 +54,54 @@ import android.util.Log;
* @author Felix Ableitner
*
*/
-public class UpnpController {
+public class UpnpController implements RegistryListener {
- private static final String TAG = "DlnaController";
+ private static final String TAG = "UpnpController";
- private DeviceListener mDeviceListener = new 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);
+ public void deviceUpdated(Device, ?, ?> device);
+ }
+
+ /**
+ * Used if the UPNP service is not yet ready. If true, a device search
+ * will be started as soon as the service becomes available.
+ */
+ private boolean mStartSearchImmediately;
+
+ private ArrayList> mDevices = new ArrayList>();
+
+ private ArrayList mListeners =
+ new ArrayList();
protected AndroidUpnpService mUpnpService;
private ServiceConnection mUpnpServiceConnection = new ServiceConnection() {
/**
- * Registers DeviceListener, adds known devices and starts search.
+ * Registers DeviceListener, adds known devices and starts search if requested.
*/
public void onServiceConnected(ComponentName className, IBinder service) {
mUpnpService = (AndroidUpnpService) service;
- Log.i(TAG, "Starting device search");
- mUpnpService.getRegistry().addListener(mDeviceListener);
+ mUpnpService.getRegistry().addListener(UpnpController.this);
for (Device, ?, ?> d : mUpnpService.getControlPoint().getRegistry().getDevices()) {
- if (d instanceof LocalDevice) {
- mDeviceListener.localDeviceAdded(mUpnpService.getRegistry(), (LocalDevice) d);
- }
- else {
- mDeviceListener.remoteDeviceAdded(mUpnpService.getRegistry(), (RemoteDevice) d);
- }
+ if (d instanceof LocalDevice)
+ localDeviceAdded(mUpnpService.getRegistry(), (LocalDevice) d);
+ else
+ remoteDeviceAdded(mUpnpService.getRegistry(), (RemoteDevice) d);
}
- mUpnpService.getControlPoint().search();
+ if (mStartSearchImmediately) {
+ Log.i(TAG, "Starting device search");
+ mUpnpService.getControlPoint().search();
+ mStartSearchImmediately = false;
+ }
}
public void onServiceDisconnected(ComponentName className) {
@@ -93,7 +119,7 @@ public class UpnpController {
new Intent(context, AndroidUpnpServiceImpl.class),
mUpnpServiceConnection,
Context.BIND_AUTO_CREATE
- );
+ );
}
/**
@@ -102,10 +128,29 @@ public class UpnpController {
* @param context Application context.
*/
public void close(Context context) {
- mUpnpService.getRegistry().removeListener(mDeviceListener);
+ mUpnpService.getRegistry().removeListener(this);
context.unbindService(mUpnpServiceConnection);
}
-
+
+ /**
+ * Starts active search for UPNP devices.
+ */
+ public void startSearch() {
+ if (mUpnpService != null) {
+ Log.i(TAG, "Starting device search");
+ mUpnpService.getControlPoint().search();
+ }
+ else
+ mStartSearchImmediately = true;
+ }
+
+ /**
+ * Stops active search for UPNP devices.
+ *
+ * Not yet implemented.
+ */
+ public void stopSearch() {
+ }
/**
* Returns a device service by name for direct queries.
@@ -115,6 +160,29 @@ public class UpnpController {
new ServiceType("schemas-upnp-org", name));
}
+ 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);
+ }
+
public void execute(ActionCallback callback) {
mUpnpService.getControlPoint().execute(callback);
}
@@ -123,8 +191,48 @@ public class UpnpController {
mUpnpService.getControlPoint().execute(callback);
}
- public DeviceListener getDeviceListener() {
- return mDeviceListener;
+ @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) {
+ for (DeviceListenerCallback l : mListeners)
+ l.deviceUpdated(device);
}
}
diff --git a/src/com/github/nutomic/controldlna/upnp/UpnpPlayer.java b/src/com/github/nutomic/controldlna/upnp/UpnpPlayer.java
deleted file mode 100644
index 6885373..0000000
--- a/src/com/github/nutomic/controldlna/upnp/UpnpPlayer.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
-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.upnp;
-
-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;
-
-
-/**
- * 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 int mMinVolume;
-
- private int mMaxVolume;
-
- private int mVolumeStep;
-
- private int mCurrentVolume;
-
- 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(int newVolume) {
- if (mPlayService.getService().getRenderer() == null)
- return;
-
- if (newVolume > mMaxVolume) newVolume = mMaxVolume;
- if (newVolume < mMinVolume) newVolume = mMinVolume;
-
- mCurrentVolume = newVolume;
- 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).
- */
- public void changeVolume(int delta) {
- if (delta > 0 && delta < mVolumeStep) {
- delta = mVolumeStep;
- }
- else if (delta < 0 && delta > -mVolumeStep) {
- delta = -mVolumeStep;
- }
- setVolume(mCurrentVolume + delta);
- }
-
- /**
- * 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 = (int) volumeRange.getMinimum();
- mMaxVolume = (int) volumeRange.getMaximum();
- mVolumeStep = (int) volumeRange.getStep();
- }
- else {
- mMinVolume = 0;
- mMaxVolume = 100;
- }
-
- 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) {
- mCurrentVolume = currentVolume;
- }
- });
- }
-
- /**
- * 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 != null)
- ? mPlayService.getService()
- : null;
- }
-
-}
diff --git a/src/com/github/nutomic/controldlna/utility/DeviceArrayAdapter.java b/src/com/github/nutomic/controldlna/utility/DeviceArrayAdapter.java
index a717b35..7e5bb02 100644
--- a/src/com/github/nutomic/controldlna/utility/DeviceArrayAdapter.java
+++ b/src/com/github/nutomic/controldlna/utility/DeviceArrayAdapter.java
@@ -43,7 +43,7 @@ import android.widget.ArrayAdapter;
import android.widget.TextView;
import com.github.nutomic.controldlna.R;
-import com.github.nutomic.controldlna.upnp.DeviceListener.DeviceListenerCallback;
+import com.github.nutomic.controldlna.upnp.UpnpController.DeviceListenerCallback;
/**
@@ -87,19 +87,19 @@ public class DeviceArrayAdapter extends ArrayAdapter>
(RemoteImageView) convertView.findViewById(R.id.image);
tv.setText(getItem(position).getDetails().getFriendlyName());
- // Loading icons for local devices is not currently implemented.
- if (getItem(position) instanceof RemoteDevice &&
- getItem(position).hasIcons()) {
- RemoteDevice device = (RemoteDevice) getItem(position);
- URI uri = null;
- try {
- uri = device.normalizeURI(
- getItem(position).getIcons()[0].getUri()).toURI();
- } catch (URISyntaxException e) {
- Log.w(TAG, "Failed to get device icon URI", e);
+ if (getItem(position).hasIcons()) {
+ URI uri = getItem(position).getIcons()[0].getUri();
+ if (getItem(position) instanceof RemoteDevice) {
+ try {
+ RemoteDevice device = (RemoteDevice) getItem(position);
+ uri = device.normalizeURI(uri).toURI();
+ } catch (URISyntaxException e) {
+ Log.w(TAG, "Failed to get device icon URI", e);
+ }
}
image.setImageUri(uri);
}
+
return convertView;
}
@@ -113,7 +113,7 @@ public class DeviceArrayAdapter extends ArrayAdapter>
@Override
public void run() {
if (device.getType().getType().equals(mDeviceType))
- add(device);
+ add(device);
}
});
}
diff --git a/src/com/github/nutomic/controldlna/utility/RouteAdapter.java b/src/com/github/nutomic/controldlna/utility/RouteAdapter.java
new file mode 100644
index 0000000..6e6b4ee
--- /dev/null
+++ b/src/com/github/nutomic/controldlna/utility/RouteAdapter.java
@@ -0,0 +1,36 @@
+package com.github.nutomic.controldlna.utility;
+
+import android.content.Context;
+import android.support.v7.media.MediaRouter.RouteInfo;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import com.github.nutomic.controldlna.R;
+
+public class RouteAdapter extends ArrayAdapter {
+
+ public RouteAdapter(Context context) {
+ super(context, R.layout.list_item);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ LayoutInflater inflater = (LayoutInflater) getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ convertView = inflater.inflate(R.layout.list_item, parent, false);
+ }
+
+ TextView title = (TextView) convertView.findViewById(R.id.title);
+ title.setText(getItem(position).getName());
+
+ TextView subtitle = (TextView) convertView.findViewById(R.id.subtitle);
+ subtitle.setText(getItem(position).getDescription());
+
+ return convertView;
+ }
+
+}