Changed playback to work entirely through MediaRouter.

This allows playing to non-UPNP-devices.
This commit is contained in:
Felix Ableitner 2013-10-03 20:57:36 +02:00
parent d19b9aaf39
commit 838ad69360
20 changed files with 1613 additions and 1622 deletions

View file

@ -31,14 +31,14 @@
</intent-filter> </intent-filter>
</activity> </activity>
<service android:name="org.teleal.cling.android.AndroidUpnpServiceImpl" /> <service android:name="org.teleal.cling.android.AndroidUpnpServiceImpl" />
<service android:name="com.github.nutomic.controldlna.upnp.PlayService" /> <service android:name="com.github.nutomic.controldlna.mediarouter.MediaRouterPlayService" />
<service android:name="com.github.nutomic.controldlna.mediarouter.RemotePlayService" /> <service android:name="com.github.nutomic.controldlna.upnp.RemotePlayService" />
<service android:name=".mediarouter.ProviderService" <service android:name="com.github.nutomic.controldlna.upnp.ProviderService"
android:label="sample_media_route_provider_service" android:label="sample_media_route_provider_service"
android:process=":mrp"> android:process=":mrp">
<intent-filter> <intent-filter>

View file

@ -0,0 +1,3 @@
package android.os;
parcelable Messenger;

View file

@ -27,7 +27,13 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package com.github.nutomic.controldlna.gui; package com.github.nutomic.controldlna.gui;
import java.util.List;
import org.teleal.cling.support.model.item.Item;
import android.os.Bundle; 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.app.FragmentTransaction;
import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
@ -37,10 +43,9 @@ import android.support.v7.app.ActionBarActivity;
import android.view.KeyEvent; import android.view.KeyEvent;
import com.github.nutomic.controldlna.R; 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 * @author Felix Ableitner
* *
@ -54,21 +59,34 @@ public class MainActivity extends ActionBarActivity {
boolean onBackPressed(); boolean onBackPressed();
} }
/** FragmentStatePagerAdapter mSectionsPagerAdapter =
* Manages all UPNP connections including playback. new FragmentStatePagerAdapter(getSupportFragmentManager()) {
*/
private UpnpPlayer mPlayer = new UpnpPlayer(); @Override
public Fragment getItem(int position) {
/** switch (position) {
* Holds fragments. case 0: return mServerFragment;
*/ case 1: return mRendererFragment;
SectionsPagerAdapter mSectionsPagerAdapter; default: return null;
}
}
@Override
public int getCount() {
return 2;
}
};
private ServerFragment mServerFragment = new ServerFragment();
private RouteFragment mRendererFragment = new RouteFragment();
/**
* Allows tab swiping.
*/
ViewPager mViewPager; ViewPager mViewPager;
/**
* Initializes tab navigation.
*/
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
final ActionBar actionBar = getSupportActionBar(); final ActionBar actionBar = getSupportActionBar();
@ -77,11 +95,6 @@ public class MainActivity extends ActionBarActivity {
actionBar.setDisplayShowTitleEnabled(false); actionBar.setDisplayShowTitleEnabled(false);
actionBar.setDisplayShowHomeEnabled(false); actionBar.setDisplayShowHomeEnabled(false);
setContentView(R.layout.activity_main); 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 = (ViewPager) findViewById(R.id.pager);
mViewPager.setAdapter(mSectionsPagerAdapter); mViewPager.setAdapter(mSectionsPagerAdapter);
@ -112,61 +125,46 @@ public class MainActivity extends ActionBarActivity {
.setTabListener(tabListener)); .setTabListener(tabListener));
actionBar.addTab(actionBar.newTab() actionBar.addTab(actionBar.newTab()
.setText(R.string.title_renderer) .setText(R.string.title_renderer)
.setTabListener(tabListener)); .setTabListener(tabListener));
mPlayer.open(getApplicationContext());
} }
@Override
protected void onDestroy() {
super.onDestroy();
mPlayer.close(getApplicationContext());
}
/** /**
* Forwards back press to active Fragment (unless the fragment is * Forwards back press to active Fragment (unless the fragment is
* showing its root view). * showing its root view).
*/ */
@Override @Override
public void onBackPressed() { public void onBackPressed() {
OnBackPressedListener currentFragment = (OnBackPressedListener) OnBackPressedListener currentFragment = (OnBackPressedListener)
mSectionsPagerAdapter.getItem(mViewPager.getCurrentItem()); mSectionsPagerAdapter.getItem(mViewPager.getCurrentItem());
if (!currentFragment.onBackPressed()) if (!currentFragment.onBackPressed())
super.onBackPressed(); super.onBackPressed();
} }
/** /**
* Changes volume on key press. * Changes volume on key press (via RouteFragment).
*/ */
@Override @Override
public boolean dispatchKeyEvent(KeyEvent event) { public boolean dispatchKeyEvent(KeyEvent event) {
switch (event.getKeyCode()) { switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_UP:
if (event.getAction() == KeyEvent.ACTION_DOWN) if (event.getAction() == KeyEvent.ACTION_DOWN)
mPlayer.changeVolume(1); mRendererFragment.increaseVolume();
return true; return true;
case KeyEvent.KEYCODE_VOLUME_DOWN: case KeyEvent.KEYCODE_VOLUME_DOWN:
if (event.getAction() == KeyEvent.ACTION_DOWN) if (event.getAction() == KeyEvent.ACTION_DOWN)
mPlayer.changeVolume(-1); mRendererFragment.decreaseVolume();
return true; return true;
default: default:
return super.dispatchKeyEvent(event); 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<Item> playlist, int start) {
mViewPager.setCurrentItem(1); mViewPager.setCurrentItem(1);
mRendererFragment.play(playlist, start);
} }
} }

View file

@ -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 <organization> 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 <COPYRIGHT HOLDER> 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<String, StateVariableValue> 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();
}
}

View file

@ -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 <organization> 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 <COPYRIGHT HOLDER> 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<Item> 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();
}
}

View file

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

View file

@ -50,6 +50,7 @@ import android.view.View;
import android.widget.ListView; import android.widget.ListView;
import com.github.nutomic.controldlna.gui.MainActivity.OnBackPressedListener; 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.DeviceArrayAdapter;
import com.github.nutomic.controldlna.utility.FileArrayAdapter; 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. * Holds the scroll position in the list view at each directory level.
*/ */
private Stack<Parcelable> mListState = new Stack<Parcelable>(); private Stack<Parcelable> mListState = new Stack<Parcelable>();
/**
* Manages all UPNP connections including playback.
*/
private UpnpController mController = new UpnpController();
/** /**
* Initializes ListView adapters, launches Cling UPNP service. * Initializes ListView adapters, launches Cling UPNP service.
@ -104,8 +110,26 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene
mServerAdapter = new DeviceArrayAdapter( mServerAdapter = new DeviceArrayAdapter(
getActivity(), DeviceArrayAdapter.SERVER); getActivity(), DeviceArrayAdapter.SERVER);
setListAdapter(mServerAdapter); setListAdapter(mServerAdapter);
MainActivity activity = (MainActivity) getActivity(); mController.open(getActivity());
activity.getUpnpPlayer().getDeviceListener().addCallback(mServerAdapter); 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); getFiles(ROOT_DIRECTORY);
} }
else if (getListAdapter() == mFileAdapter) { else if (getListAdapter() == mFileAdapter) {
if (mFileAdapter.getItem(position) instanceof Container) { if (mFileAdapter.getItem(position) instanceof Container)
getFiles(((Container) mFileAdapter.getItem(position)).getId()); getFiles(((Container) mFileAdapter.getItem(position)).getId());
}
else { else {
List<Item> playlist = new ArrayList<Item>(); List<Item> playlist = new ArrayList<Item>();
for (int i = 0; i < mFileAdapter.getCount(); i++) { 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)); playlist.add((Item) mFileAdapter.getItem(i));
} }
MainActivity activity = (MainActivity) getActivity(); MainActivity activity = (MainActivity) getActivity();
activity.switchToRendererTab(); activity.play(playlist, position);
activity.getUpnpPlayer().getPlayService()
.setPlaylist(playlist, position);
} }
} }
} }
@ -154,8 +175,7 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene
private void getFiles(final boolean restoreListState) { private void getFiles(final boolean restoreListState) {
Service<?, ?> service = mCurrentServer.findService( Service<?, ?> service = mCurrentServer.findService(
new ServiceType("schemas-upnp-org", "ContentDirectory")); new ServiceType("schemas-upnp-org", "ContentDirectory"));
MainActivity activity = (MainActivity) getActivity(); mController.execute(new Browse(service,
activity.getUpnpPlayer().execute(new Browse(service,
mCurrentPath.peek(), BrowseFlag.DIRECT_CHILDREN) { mCurrentPath.peek(), BrowseFlag.DIRECT_CHILDREN) {
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")

View file

@ -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 <organization> 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 <COPYRIGHT HOLDER> 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<Item> mPlaylist = new ArrayList<Item>();
/**
* The track that is currently being played.
*/
private int mCurrentTrack;
private String mItemId;
private String mSessionId;
private WeakReference<RouteFragment> mRendererFragment = new WeakReference<RouteFragment>(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<RouteFragment>(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<Item> 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);
}
}

View file

@ -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. 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; import android.os.Binder;
@ -35,15 +35,15 @@ import android.os.Binder;
* @author Felix Ableitner * @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; mService = service;
} }
public PlayService getService() { public MediaRouterPlayService getService() {
return mService; return mService;
} }
} }

View file

@ -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 <organization> 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 <COPYRIGHT HOLDER> 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<RemotePlayService> mService;
IncomingHandler(RemotePlayService service) {
mService = new WeakReference<RemotePlayService>(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<String, Device<?, ?, ?>> mDevices = new HashMap<String, Device<?, ?, ?>>();
@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<Item> playlist = new ArrayList<Item>();
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.
}
}

View file

@ -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 <organization> 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 <COPYRIGHT HOLDER> 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<Device<?, ?, ?>> mDevices = new ArrayList<Device<?, ?, ?>>();
private ArrayList<DeviceListenerCallback> mListeners = new ArrayList<DeviceListenerCallback>();
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);
}
}
}

View file

@ -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 <organization> 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 <COPYRIGHT HOLDER> 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);
}

View file

@ -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 <organization> 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 <COPYRIGHT HOLDER> 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<Item> mPlaylist = new ArrayList<Item>();
/**
* 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<String, StateVariableValue> 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<Item> 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<Item> getPlaylist() {
return mPlaylist;
}
public int getCurrentTrack() {
return mCurrentTrack;
}
public void setShowNotification(boolean value) {
mShowNotification = value;
}
}

View file

@ -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. 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.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Random;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; 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.MediaRouteProviderDescriptor.Builder;
import android.support.v7.media.MediaRouter; import android.support.v7.media.MediaRouter;
import android.support.v7.media.MediaRouter.ControlRequestCallback; 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. * Allows playing to a DLNA renderer from a remote app.
@ -67,6 +70,10 @@ final class Provider extends MediaRouteProvider {
// Device has been removed. // Device has been removed.
// param: int id // param: int id
public static final int MSG_RENDERER_REMOVED = 2; 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. * Allows passing and storing basic information about a device.
@ -124,29 +131,23 @@ final class Provider extends MediaRouteProvider {
private HashMap<String, Device> mDevices = new HashMap<String, Device>(); private HashMap<String, Device> mDevices = new HashMap<String, Device>();
private Messenger mRemotePlayService; private SparseArray<Pair<Intent, ControlRequestCallback>> mRequests =
new SparseArray<Pair<Intent, ControlRequestCallback>>();
IRemotePlayService mIRemotePlayService;
private ServiceConnection mConnection = new ServiceConnection() { private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) { public void onServiceConnected(ComponentName className, IBinder service) {
mRemotePlayService = new Messenger(service); mIRemotePlayService = IRemotePlayService.Stub.asInterface(service);
Message msg = Message.obtain(null, RemotePlayService.MSG_OPEN, 0, 0);
msg.replyTo = mListener;
try {
mRemotePlayService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
} }
public void onServiceDisconnected(ComponentName className) { public void onServiceDisconnected(ComponentName className) {
mRemotePlayService = null; mIRemotePlayService = null;
} }
}; };
private static final ArrayList<IntentFilter> CONTROL_FILTERS; private static final ArrayList<IntentFilter> CONTROL_FILTERS;
// Static constructor for CONTROL_FILTERS.
static { static {
IntentFilter f = new IntentFilter(); IntentFilter f = new IntentFilter();
f.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); f.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
f.addAction(MediaControlIntent.ACTION_PLAY); f.addAction(MediaControlIntent.ACTION_PLAY);
@ -179,9 +180,8 @@ final class Provider extends MediaRouteProvider {
@Override @Override
public void handleMessage(Message msg) { public void handleMessage(Message msg) {
if (mService.get() != null) { if (mService.get() != null)
mService.get().handleMessage(msg); mService.get().handleMessage(msg);
}
} }
} }
@ -202,22 +202,14 @@ final class Provider extends MediaRouteProvider {
@Override @Override
public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) { public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
if (request == null) return; try {
Message msg; if (request != null && request.isActiveScan())
if (request.isActiveScan()) { mIRemotePlayService.startSearch(mListener);
msg = Message.obtain(null, RemotePlayService.MSG_OPEN, 0, 0); else
msg.replyTo = mListener; mIRemotePlayService.stopSearch();
} } catch (RemoteException e) {
else { e.printStackTrace();
msg = Message.obtain(null, RemotePlayService.MSG_CLOSE, 0, 0); }
}
try {
if (mRemotePlayService != null) {
mRemotePlayService.send(msg);
}
} catch (RemoteException e) {
e.printStackTrace();
}
} }
@Override @Override
@ -260,88 +252,115 @@ final class Provider extends MediaRouteProvider {
@Override @Override
public void onSelect() { public void onSelect() {
Message msg = Message.obtain(null, RemotePlayService.MSG_SELECT, 0, 0);
msg.getData().putString("id", mRouteId);
try { try {
mRemotePlayService.send(msg); mIRemotePlayService.selectRenderer(mRouteId);
} catch (RemoteException e) { } catch (RemoteException e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
@Override @Override
public void onUnselect() { public void onUnselect() {
Message msg = Message.obtain(null, RemotePlayService.MSG_UNSELECT, 0, 0);
msg.getData().putString("id", mRouteId);
try { try {
mRemotePlayService.send(msg); mIRemotePlayService.unselectRenderer(mRouteId);
} catch (RemoteException e) { } catch (RemoteException e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
@Override @Override
public void onSetVolume(int volume) { public void onSetVolume(int volume) {
Message msg = Message.obtain(null, RemotePlayService.MSG_SET_VOPLUME, 0, 0); if (volume < 0 || volume > mDevices.get(mRouteId).volumeMax)
msg.getData().putInt("volume", volume); return;
mDevices.get(mRouteId).volume = volume;
try { try {
mRemotePlayService.send(msg); mIRemotePlayService.setVolume(volume);
} catch (RemoteException e) { } catch (RemoteException e) {
e.printStackTrace(); e.printStackTrace();
} }
mDevices.get(mRouteId).volume = volume;
updateRoutes(); updateRoutes();
} }
@Override @Override
public void onUpdateVolume(int delta) { public void onUpdateVolume(int delta) {
Message msg = Message.obtain(null, RemotePlayService.MSG_CHANGE_VOLUME, 0, 0); onSetVolume(mDevices.get(mRouteId).volume + delta);
msg.getData().putInt("delta", delta);
mDevices.get(mRouteId).volume += delta;
try {
mRemotePlayService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
updateRoutes();
} }
/**
* Handles play, pause, resume, stop, seek and get_status requests for this route.
*/
@Override @Override
public boolean onControlRequest(Intent intent, ControlRequestCallback callback) { public boolean onControlRequest(Intent intent, ControlRequestCallback callback) {
try { try {
if (intent.getAction().equals(MediaControlIntent.ACTION_PLAY)) { if (intent.getAction().equals(MediaControlIntent.ACTION_PLAY)) {
Message msg = Message.obtain(null, RemotePlayService.MSG_PLAY, 0, 0); String metadata = (intent.hasExtra(MediaControlIntent.EXTRA_ITEM_METADATA))
msg.getData().putString("uri", intent.getDataString()); ? intent.getExtras().getString(MediaControlIntent.EXTRA_ITEM_METADATA)
mRemotePlayService.send(msg); : 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; return true;
} }
else if (intent.getAction().equals(MediaControlIntent.ACTION_PAUSE)) { else if (intent.getAction().equals(MediaControlIntent.ACTION_PAUSE)) {
Message msg = Message.obtain(null, RemotePlayService.MSG_PAUSE, 0, 0); mIRemotePlayService.pause(mRouteId);
mRemotePlayService.send(msg);
return true; return true;
} }
else if (intent.getAction().equals(MediaControlIntent.ACTION_PLAY)) { else if (intent.getAction().equals(MediaControlIntent.ACTION_RESUME)) {
Message msg = Message.obtain(null, RemotePlayService.MSG_STOP, 0, 0); mIRemotePlayService.resume(mRouteId);
mRemotePlayService.send(msg);
return true; return true;
} }
else if (intent.getAction().equals(MediaControlIntent.ACTION_PLAY)) { else if (intent.getAction().equals(MediaControlIntent.ACTION_STOP)) {
Message msg = Message.obtain(null, RemotePlayService.MSG_PLAY, 0, 0); mIRemotePlayService.stop(mRouteId);
msg.getData().putLong("milliseconds",
intent.getIntExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0));
mRemotePlayService.send(msg);
return true; return true;
} }
} catch (RemoteException e) { else if (intent.getAction().equals(MediaControlIntent.ACTION_SEEK)) {
e.printStackTrace(); mIRemotePlayService.seek(mRouteId,
} intent.getStringExtra(
return false; 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<Intent, ControlRequestCallback> pair =
new Pair<Intent, ControlRequestCallback>(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) { public void handleMessage(Message msg) {
Bundle data = msg.getData(); Bundle data = msg.getData();
switch (msg.what) { switch (msg.what) {
case MSG_RENDERER_ADDED: case MSG_RENDERER_ADDED:
msg.getData().setClassLoader(Device.class.getClassLoader()); msg.getData().setClassLoader(Device.class.getClassLoader());
Device device = (Device) data.getParcelable("device"); Device device = (Device) data.getParcelable("device");
@ -352,6 +371,20 @@ final class Provider extends MediaRouteProvider {
mDevices.remove(data.getString("id")); mDevices.remove(data.getString("id"));
updateRoutes(); updateRoutes();
break; break;
case MSG_STATUS_INFO:
Pair<Intent, ControlRequestCallback> 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);
} }
} }

View file

@ -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. 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.MediaRouteProvider;
import android.support.v7.media.MediaRouteProviderService; import android.support.v7.media.MediaRouteProviderService;

View file

@ -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 <organization> 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 <COPYRIGHT HOLDER> 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<String, Device<?, ?, ?>> mDevices = new HashMap<String, Device<?, ?, ?>>();
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<String, StateVariableValue> 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);
}
}

View file

@ -27,6 +27,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package com.github.nutomic.controldlna.upnp; package com.github.nutomic.controldlna.upnp;
import java.util.ArrayList;
import org.teleal.cling.android.AndroidUpnpService; import org.teleal.cling.android.AndroidUpnpService;
import org.teleal.cling.android.AndroidUpnpServiceImpl; import org.teleal.cling.android.AndroidUpnpServiceImpl;
import org.teleal.cling.controlpoint.ActionCallback; 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.RemoteDevice;
import org.teleal.cling.model.meta.Service; import org.teleal.cling.model.meta.Service;
import org.teleal.cling.model.types.ServiceType; 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.ComponentName;
import android.content.Context; import android.content.Context;
@ -50,32 +54,54 @@ import android.util.Log;
* @author Felix Ableitner * @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<Device<?, ?, ?>> mDevices = new ArrayList<Device<?, ?, ?>>();
private ArrayList<DeviceListenerCallback> mListeners =
new ArrayList<DeviceListenerCallback>();
protected AndroidUpnpService mUpnpService; protected AndroidUpnpService mUpnpService;
private ServiceConnection mUpnpServiceConnection = new ServiceConnection() { 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) { public void onServiceConnected(ComponentName className, IBinder service) {
mUpnpService = (AndroidUpnpService) service; mUpnpService = (AndroidUpnpService) service;
Log.i(TAG, "Starting device search"); mUpnpService.getRegistry().addListener(UpnpController.this);
mUpnpService.getRegistry().addListener(mDeviceListener);
for (Device<?, ?, ?> d : mUpnpService.getControlPoint().getRegistry().getDevices()) { for (Device<?, ?, ?> d : mUpnpService.getControlPoint().getRegistry().getDevices()) {
if (d instanceof LocalDevice) { if (d instanceof LocalDevice)
mDeviceListener.localDeviceAdded(mUpnpService.getRegistry(), (LocalDevice) d); localDeviceAdded(mUpnpService.getRegistry(), (LocalDevice) d);
} else
else { remoteDeviceAdded(mUpnpService.getRegistry(), (RemoteDevice) d);
mDeviceListener.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) { public void onServiceDisconnected(ComponentName className) {
@ -93,7 +119,7 @@ public class UpnpController {
new Intent(context, AndroidUpnpServiceImpl.class), new Intent(context, AndroidUpnpServiceImpl.class),
mUpnpServiceConnection, mUpnpServiceConnection,
Context.BIND_AUTO_CREATE Context.BIND_AUTO_CREATE
); );
} }
/** /**
@ -102,10 +128,29 @@ public class UpnpController {
* @param context Application context. * @param context Application context.
*/ */
public void close(Context context) { public void close(Context context) {
mUpnpService.getRegistry().removeListener(mDeviceListener); mUpnpService.getRegistry().removeListener(this);
context.unbindService(mUpnpServiceConnection); 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. * Returns a device service by name for direct queries.
@ -115,6 +160,29 @@ public class UpnpController {
new ServiceType("schemas-upnp-org", name)); 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) { public void execute(ActionCallback callback) {
mUpnpService.getControlPoint().execute(callback); mUpnpService.getControlPoint().execute(callback);
} }
@ -123,8 +191,48 @@ public class UpnpController {
mUpnpService.getControlPoint().execute(callback); mUpnpService.getControlPoint().execute(callback);
} }
public DeviceListener getDeviceListener() { @Override
return mDeviceListener; 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);
} }
} }

View file

@ -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 <organization> 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 <COPYRIGHT HOLDER> 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;
}
}

View file

@ -43,7 +43,7 @@ import android.widget.ArrayAdapter;
import android.widget.TextView; import android.widget.TextView;
import com.github.nutomic.controldlna.R; 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<Device<?, ?, ?>>
(RemoteImageView) convertView.findViewById(R.id.image); (RemoteImageView) convertView.findViewById(R.id.image);
tv.setText(getItem(position).getDetails().getFriendlyName()); tv.setText(getItem(position).getDetails().getFriendlyName());
// Loading icons for local devices is not currently implemented. if (getItem(position).hasIcons()) {
if (getItem(position) instanceof RemoteDevice && URI uri = getItem(position).getIcons()[0].getUri();
getItem(position).hasIcons()) { if (getItem(position) instanceof RemoteDevice) {
RemoteDevice device = (RemoteDevice) getItem(position); try {
URI uri = null; RemoteDevice device = (RemoteDevice) getItem(position);
try { uri = device.normalizeURI(uri).toURI();
uri = device.normalizeURI( } catch (URISyntaxException e) {
getItem(position).getIcons()[0].getUri()).toURI(); Log.w(TAG, "Failed to get device icon URI", e);
} catch (URISyntaxException e) { }
Log.w(TAG, "Failed to get device icon URI", e);
} }
image.setImageUri(uri); image.setImageUri(uri);
} }
return convertView; return convertView;
} }
@ -113,7 +113,7 @@ public class DeviceArrayAdapter extends ArrayAdapter<Device<?, ?, ?>>
@Override @Override
public void run() { public void run() {
if (device.getType().getType().equals(mDeviceType)) if (device.getType().getType().equals(mDeviceType))
add(device); add(device);
} }
}); });
} }

View file

@ -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<RouteInfo> {
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;
}
}