Added service to handle playback.

This commit is contained in:
Felix Ableitner 2013-06-10 02:24:03 +02:00
parent c0608b0d0d
commit 5dcd13d126
6 changed files with 475 additions and 218 deletions

View file

@ -32,7 +32,9 @@
</activity>
<service android:name="org.teleal.cling.android.AndroidUpnpServiceImpl"/>
<service android:name="org.teleal.cling.android.AndroidUpnpServiceImpl" />
<service android:name="com.github.nutomic.controldlna.service.PlayService" />
</application>

View file

@ -9,5 +9,6 @@
<string name="exit_renderer">Do you really want to exit the renderer? Playback will be stopped.</string>
<string name="previous">Previous</string>
<string name="next">Next</string>
<string name="select_renderer">Please select a renderer</string>
</resources>

View file

@ -16,21 +16,17 @@ import org.teleal.cling.model.meta.Service;
import org.teleal.cling.model.state.StateVariableValue;
import org.teleal.cling.model.types.ServiceType;
import org.teleal.cling.support.avtransport.callback.GetPositionInfo;
import org.teleal.cling.support.avtransport.callback.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.contentdirectory.DIDLParser;
import org.teleal.cling.support.lastchange.LastChange;
import org.teleal.cling.support.model.DIDLContent;
import org.teleal.cling.support.model.PositionInfo;
import org.teleal.cling.support.model.SeekMode;
import org.teleal.cling.support.model.item.Item;
import org.teleal.cling.support.renderingcontrol.callback.GetVolume;
import org.teleal.cling.support.renderingcontrol.callback.SetVolume;
import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.Context;
@ -55,6 +51,8 @@ import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.Toast;
import com.github.nutomic.controldlna.MainActivity.OnBackPressedListener;
import com.github.nutomic.controldlna.service.PlayService;
import com.github.nutomic.controldlna.service.PlayServiceBinder;
/**
* Shows a list of media servers, allowing to select one for playback.
@ -76,15 +74,7 @@ public class RendererFragment extends Fragment implements
private boolean mPlaying = false;
private int mCurrentTrack;
/**
* Used to determine when the player stops due to the media file being
* over (so the next one can be played).
*/
private boolean mManuallyStopped;
private List<Item> mPlaylist;
private Device<?, ?, ?> mCurrentRenderer;
/**
* ListView adapter of media renderers.
@ -93,18 +83,22 @@ public class RendererFragment extends Fragment implements
private FileArrayAdapter mPlaylistAdapter;
/**
* The media renderer that is currently active.
*/
private Device<?, ?, ?> mCurrentRenderer;
/**
* First track to be played when a renderer is selected (-1 for none).
*/
private int mCachedStart = -1;
private SubscriptionCallback mSubscriptionCallback;
private PlayServiceBinder mPlayService;
private ServiceConnection mPlayServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
Log.d(TAG, "test");
mPlayService = (PlayServiceBinder) service;
}
public void onServiceDisconnected(ComponentName className) {
mPlayService = null;
}
};
/**
* Cling UPNP service.
*/
@ -113,7 +107,7 @@ public class RendererFragment extends Fragment implements
/**
* Connection Cling to UPNP service.
*/
private ServiceConnection mServiceConnection= new ServiceConnection() {
private ServiceConnection mUpnpServiceConnection = new ServiceConnection() {
@SuppressWarnings("unchecked")
public void onServiceConnected(ComponentName className, IBinder service) {
@ -157,17 +151,29 @@ public class RendererFragment extends Fragment implements
getView().findViewById(R.id.previous).setOnClickListener(this);
getView().findViewById(R.id.next).setOnClickListener(this);
getActivity().startService(new Intent(getActivity(), PlayService.class));
getActivity().getApplicationContext().bindService(
new Intent(getActivity(), PlayService.class),
mPlayServiceConnection,
Context.BIND_AUTO_CREATE
);
getActivity().getApplicationContext().bindService(
new Intent(getActivity(), AndroidUpnpServiceImpl.class),
mServiceConnection,
mUpnpServiceConnection,
Context.BIND_AUTO_CREATE
);
}
/**
* Polls the renderer for the current play progress as long as
* mPlaying is true.
*/
private void pollTimePosition() {
final Service<?, ?> service = mCurrentRenderer.findService(
new ServiceType("schemas-upnp-org", "AVTransport"));
mUpnpService.getControlPoint().execute(new GetPositionInfo(service) {
mUpnpService.getControlPoint().execute(
new GetPositionInfo(service) {
@SuppressWarnings("rawtypes")
@Override
@ -195,86 +201,69 @@ public class RendererFragment extends Fragment implements
}
}
/**
* Clears cached playback URI.
*/
@Override
public void onPause() {
super.onPause();
mCachedStart = -1;
}
/**
* Closes Cling UPNP service.
*/
@Override
public void onDestroy() {
super.onDestroy();
if (mUpnpService != null)
mUpnpService.getRegistry().removeListener(mRendererAdapter);
getActivity().getApplicationContext().unbindService(mServiceConnection);
getActivity().getApplicationContext().unbindService(mUpnpServiceConnection);
getActivity().getApplicationContext().unbindService(mPlayServiceConnection);
}
/**
* Sets the new playlist and starts playing it (if a renderer is selected).
* Sets the new playlist.
*/
public void setPlaylist(List<Item> playlist, int start) {
mPlaylist = playlist;
mPlaylistAdapter.clear();
mPlaylistAdapter.addAll(playlist);
playTrack(start);
}
/**
* Plays the specified track in the current playlist, caches value if no
* renderer is selected.
*/
private void playTrack(int track) {
if (mCurrentRenderer != null) {
mListView.setAdapter(mPlaylistAdapter);
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";
}
mUpnpService.getControlPoint().execute(new SetAVTransportURI(
getService("AVTransport"),
mPlaylist.get(track).getFirstResource().getValue(), 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();
}
});
}
else {
Toast.makeText(getActivity(), "Please select a renderer.",
Toast.LENGTH_SHORT).show();
mCachedStart = track;
}
mPlayService.getService().setPlaylist(playlist, start);
}
/**
* Selects a media renderer.
*/
@Override
public void onItemClick(AdapterView<?> a, View v, int position, long id) {
public void onItemClick(AdapterView<?> a, View v, final int position, long id) {
if (mListView.getAdapter() == mRendererAdapter) {
mCurrentRenderer = mRendererAdapter.getItem(position);
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)
mPlayService.getService().playTrack(position);
}
private void selectRenderer(Device<?, ?, ?> renderer) {
if (mCurrentRenderer != renderer) {
if (mCurrentRenderer != null)
mPlayService.getService().pause();
if (mSubscriptionCallback != null)
mSubscriptionCallback.end();
mCurrentRenderer = renderer;
mPlayService.getService().setRenderer(renderer);
mSubscriptionCallback = new SubscriptionCallback(
getService("AVTransport"), 600) {
@ -311,19 +300,13 @@ public class RendererFragment extends Fragment implements
pollTimePosition();
break;
case STOPPED:
if (!mManuallyStopped &&
(mPlaylist.size() > mCurrentTrack + 1)) {
Log.d(TAG, "next");
mManuallyStopped = false;
playTrack(mCurrentTrack +1);
break;
}
// fallthrough
case PAUSED_PLAYBACK:
mManuallyStopped = false;
mPlayPause.setText(R.string.play);
mPlaying = false;
break;
default:
break;
}
} catch (Exception e) {
@ -347,16 +330,8 @@ public class RendererFragment extends Fragment implements
}
};
mUpnpService.getControlPoint().execute(mSubscriptionCallback);
if (mCachedStart != -1) {
setPlaylist(mPlaylist, mCachedStart);
mCachedStart = -1;
}
mListView.setAdapter(mPlaylistAdapter);
mControls.setVisibility(View.VISIBLE);
}
else if (mListView.getAdapter() == mPlaylistAdapter)
playTrack(position);
}
/**
@ -365,37 +340,13 @@ public class RendererFragment extends Fragment implements
@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) {
pause();
exitPlaylistMode();
}
})
.setNegativeButton(android.R.string.no, null)
.show();
}
else
exitPlaylistMode();
mControls.setVisibility(View.GONE);
mListView.setAdapter(mRendererAdapter);
return true;
}
return false;
}
private void exitPlaylistMode() {
mCurrentRenderer = null;
mSubscriptionCallback.end();
mListView.setAdapter(mRendererAdapter);
mControls.setVisibility(View.GONE);
}
/**
* Plays/pauses playback on button click.
*/
@ -404,62 +355,19 @@ public class RendererFragment extends Fragment implements
switch (v.getId()) {
case R.id.playpause:
if (mPlaying)
pause();
mPlayService.getService().pause();
else
play();
mPlayService.getService().play();
break;
case R.id.previous:
if (mCurrentTrack != 0 && !mPlaylist.isEmpty())
playTrack(mCurrentTrack - 1);
mPlayService.getService().playPrevious();
break;
case R.id.next:
if (mPlaylist.size() > mCurrentTrack + 1)
playTrack(mCurrentTrack + 1);
mPlayService.getService().playNext();
break;
}
}
/**
* Sends 'pause' signal to current renderer.
*/
private void pause() {
mManuallyStopped = true;
final Service<?, ?> service = getService("AVTransport");
mUpnpService.getControlPoint().execute(new Stop(service) {
@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.
mUpnpService.getControlPoint().execute(new Stop(service) {
@Override
public void failure(ActionInvocation invocation,
UpnpResponse operation, String defaultMessage) {
Log.w(TAG, "Stop failed: " + defaultMessage);
}
});
}
});
}
/**
* Sends 'play' signal to current renderer.
*/
private void play() {
mUpnpService.getControlPoint().execute(new Play(getService("AVTransport")) {
@SuppressWarnings("rawtypes")
@Override
public void failure(ActionInvocation invocation,
UpnpResponse operation, String defaultMessage) {
Log.w(TAG, "Play failed: " + defaultMessage);
}
});
}
/**
* Sends manual seek on progress bar to renderer.
*/
@ -496,10 +404,14 @@ public class RendererFragment extends Fragment implements
*/
@SuppressWarnings("rawtypes")
public void changeVolume(final boolean increase) {
if (mCurrentRenderer == null)
if (mCurrentRenderer == null) {
Toast.makeText(getActivity(), R.string.select_renderer,
Toast.LENGTH_SHORT).show();
return;
}
final Service<?, ?> service = getService("RenderingControl");
mUpnpService.getControlPoint().execute(new GetVolume(service) {
mUpnpService.getControlPoint().execute(
new GetVolume(service) {
@Override
public void failure(ActionInvocation invocation,
@ -511,8 +423,8 @@ public class RendererFragment extends Fragment implements
public void received(ActionInvocation invocation, int volume) {
int newVolume = volume + ((increase) ? 4 : -4);
if (newVolume < 0) newVolume = 0;
mUpnpService.getControlPoint().execute(new SetVolume(service,
newVolume) {
mUpnpService.getControlPoint().execute(
new SetVolume(service, newVolume) {
@Override
public void failure(ActionInvocation invocation,

View file

@ -74,7 +74,7 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene
/**
* Connection Cling to UPNP service.
*/
private ServiceConnection mServiceConnection= new ServiceConnection() {
private ServiceConnection mUpnpServiceConnection = new ServiceConnection() {
@SuppressWarnings("unchecked")
public void onServiceConnected(ComponentName className, IBinder service) {
@ -105,7 +105,7 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene
getActivity().getApplicationContext().bindService(
new Intent(getActivity(), AndroidUpnpServiceImpl.class),
mServiceConnection,
mUpnpServiceConnection,
Context.BIND_AUTO_CREATE
);
}
@ -118,7 +118,7 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene
super.onDestroy();
if (mUpnpService != null)
mUpnpService.getRegistry().removeListener(mServerAdapter);
getActivity().getApplicationContext().unbindService(mServiceConnection);
getActivity().getApplicationContext().unbindService(mUpnpServiceConnection);
}
/**

View file

@ -0,0 +1,326 @@
package com.github.nutomic.controldlna.service;
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.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.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.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.RendererFragment;
public class PlayService extends Service {
private static final String TAG = "PlayService";
private static final int mNotificationId = 1;
private final PlayServiceBinder mBinder = new PlayServiceBinder(this);
/**
* The DLNA media renderer device that is currently active.
*/
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;
/**
* Connection Cling 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;
@Override
public void onCreate() {
super.onCreate();
getApplicationContext().bindService(
new Intent(this, AndroidUpnpServiceImpl.class),
mUpnpServiceConnection,
Context.BIND_AUTO_CREATE
);
}
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "test2");
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";
}
mUpnpService.getControlPoint().execute(new SetAVTransportURI(
mAvTransportService,
mPlaylist.get(track).getFirstResource().getValue(), 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() {
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(this)
.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(PendingIntent.getActivity(this, 0,
new Intent(this, RendererFragment.class), 0))
.setContentTitle(title)
.setContentText(artist)
.build();
NotificationManager notificationManager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notificationManager.notify(mNotificationId, notification);
notification.flags |= Notification.FLAG_ONGOING_EVENT;
}
/**
* 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 Stop(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.
mUpnpService.getControlPoint().execute(
new Stop(mAvTransportService) {
@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)
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(mNotificationId);
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);
}
/**
* 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);
}
}

View file

@ -0,0 +1,16 @@
package com.github.nutomic.controldlna.service;
import android.os.Binder;
public class PlayServiceBinder extends Binder {
PlayService mService;
public PlayServiceBinder(PlayService service) {
mService = service;
}
public PlayService getService() {
return mService;
}
}