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; + } + +}