Added service to handle playback.
This commit is contained in:
parent
c0608b0d0d
commit
5dcd13d126
6 changed files with 475 additions and 218 deletions
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,103 +201,86 @@ 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);
|
||||
mUpnpService.getRegistry().removeListener(mRendererAdapter);
|
||||
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) {
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Override
|
||||
protected void established(GENASubscription sub) {
|
||||
}
|
||||
@Override
|
||||
protected void established(GENASubscription sub) {
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Override
|
||||
protected void ended(GENASubscription sub, CancelReason reason,
|
||||
UpnpResponse response) {
|
||||
}
|
||||
@Override
|
||||
protected void ended(GENASubscription sub, CancelReason reason,
|
||||
UpnpResponse response) {
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Override
|
||||
protected void eventReceived(final GENASubscription sub) {
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Override
|
||||
protected void eventReceived(final GENASubscription sub) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
|
||||
@Override
|
||||
|
@ -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:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
|
@ -331,32 +314,24 @@ public class RendererFragment extends Fragment implements
|
|||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Override
|
||||
protected void eventsMissed(GENASubscription sub,
|
||||
int numberOfMissedEvents) {
|
||||
}
|
||||
@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);
|
||||
}
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Override
|
||||
protected void failed(GENASubscription sub, UpnpResponse responseStatus,
|
||||
Exception exception, String defaultMsg) {
|
||||
Log.d(TAG, defaultMsg);
|
||||
}
|
||||
};
|
||||
mUpnpService.getControlPoint().execute(mSubscriptionCallback);
|
||||
if (mCachedStart != -1) {
|
||||
setPlaylist(mPlaylist, mCachedStart);
|
||||
mCachedStart = -1;
|
||||
}
|
||||
|
||||
mListView.setAdapter(mPlaylistAdapter);
|
||||
mControls.setVisibility(View.VISIBLE);
|
||||
mUpnpService.getControlPoint().execute(mSubscriptionCallback);
|
||||
}
|
||||
else if (mListView.getAdapter() == mPlaylistAdapter)
|
||||
playTrack(position);
|
||||
mListView.setAdapter(mPlaylistAdapter);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -467,7 +375,7 @@ public class RendererFragment extends Fragment implements
|
|||
public void onProgressChanged(SeekBar seekBar, int progress,
|
||||
boolean fromUser) {
|
||||
if (fromUser) {
|
||||
mUpnpService.getControlPoint().execute(new Seek(
|
||||
mUpnpService.getControlPoint().execute(new Seek(
|
||||
getService("AVTransport"), SeekMode.REL_TIME,
|
||||
Integer.toString(progress)) {
|
||||
|
||||
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
326
src/com/github/nutomic/controldlna/service/PlayService.java
Normal file
326
src/com/github/nutomic/controldlna/service/PlayService.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
Reference in a new issue