Added service to handle playback.
This commit is contained in:
parent
c0608b0d0d
commit
5dcd13d126
6 changed files with 475 additions and 218 deletions
|
@ -34,6 +34,8 @@
|
||||||
|
|
||||||
<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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -9,5 +9,6 @@
|
||||||
<string name="exit_renderer">Do you really want to exit the renderer? Playback will be stopped.</string>
|
<string name="exit_renderer">Do you really want to exit the renderer? Playback will be stopped.</string>
|
||||||
<string name="previous">Previous</string>
|
<string name="previous">Previous</string>
|
||||||
<string name="next">Next</string>
|
<string name="next">Next</string>
|
||||||
|
<string name="select_renderer">Please select a renderer</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -16,21 +16,17 @@ import org.teleal.cling.model.meta.Service;
|
||||||
import org.teleal.cling.model.state.StateVariableValue;
|
import org.teleal.cling.model.state.StateVariableValue;
|
||||||
import org.teleal.cling.model.types.ServiceType;
|
import org.teleal.cling.model.types.ServiceType;
|
||||||
import org.teleal.cling.support.avtransport.callback.GetPositionInfo;
|
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.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.AVTransportLastChangeParser;
|
||||||
import org.teleal.cling.support.avtransport.lastchange.AVTransportVariable;
|
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.lastchange.LastChange;
|
||||||
import org.teleal.cling.support.model.DIDLContent;
|
|
||||||
import org.teleal.cling.support.model.PositionInfo;
|
import org.teleal.cling.support.model.PositionInfo;
|
||||||
import org.teleal.cling.support.model.SeekMode;
|
import org.teleal.cling.support.model.SeekMode;
|
||||||
import org.teleal.cling.support.model.item.Item;
|
import org.teleal.cling.support.model.item.Item;
|
||||||
import org.teleal.cling.support.renderingcontrol.callback.GetVolume;
|
import org.teleal.cling.support.renderingcontrol.callback.GetVolume;
|
||||||
import org.teleal.cling.support.renderingcontrol.callback.SetVolume;
|
import org.teleal.cling.support.renderingcontrol.callback.SetVolume;
|
||||||
|
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
@ -55,6 +51,8 @@ import android.widget.SeekBar.OnSeekBarChangeListener;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.github.nutomic.controldlna.MainActivity.OnBackPressedListener;
|
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.
|
* 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 boolean mPlaying = false;
|
||||||
|
|
||||||
private int mCurrentTrack;
|
private Device<?, ?, ?> mCurrentRenderer;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ListView adapter of media renderers.
|
* ListView adapter of media renderers.
|
||||||
|
@ -93,18 +83,22 @@ public class RendererFragment extends Fragment implements
|
||||||
|
|
||||||
private FileArrayAdapter mPlaylistAdapter;
|
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 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.
|
* Cling UPNP service.
|
||||||
*/
|
*/
|
||||||
|
@ -113,7 +107,7 @@ public class RendererFragment extends Fragment implements
|
||||||
/**
|
/**
|
||||||
* Connection Cling to UPNP service.
|
* Connection Cling to UPNP service.
|
||||||
*/
|
*/
|
||||||
private ServiceConnection mServiceConnection= new ServiceConnection() {
|
private ServiceConnection mUpnpServiceConnection = new ServiceConnection() {
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
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.previous).setOnClickListener(this);
|
||||||
getView().findViewById(R.id.next).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(
|
getActivity().getApplicationContext().bindService(
|
||||||
new Intent(getActivity(), AndroidUpnpServiceImpl.class),
|
new Intent(getActivity(), AndroidUpnpServiceImpl.class),
|
||||||
mServiceConnection,
|
mUpnpServiceConnection,
|
||||||
Context.BIND_AUTO_CREATE
|
Context.BIND_AUTO_CREATE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Polls the renderer for the current play progress as long as
|
||||||
|
* mPlaying is true.
|
||||||
|
*/
|
||||||
private void pollTimePosition() {
|
private void pollTimePosition() {
|
||||||
final Service<?, ?> service = mCurrentRenderer.findService(
|
final Service<?, ?> service = mCurrentRenderer.findService(
|
||||||
new ServiceType("schemas-upnp-org", "AVTransport"));
|
new ServiceType("schemas-upnp-org", "AVTransport"));
|
||||||
mUpnpService.getControlPoint().execute(new GetPositionInfo(service) {
|
mUpnpService.getControlPoint().execute(
|
||||||
|
new GetPositionInfo(service) {
|
||||||
|
|
||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings("rawtypes")
|
||||||
@Override
|
@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.
|
* Closes Cling UPNP service.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
if (mUpnpService != null)
|
|
||||||
mUpnpService.getRegistry().removeListener(mRendererAdapter);
|
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) {
|
public void setPlaylist(List<Item> playlist, int start) {
|
||||||
mPlaylist = playlist;
|
|
||||||
mPlaylistAdapter.clear();
|
mPlaylistAdapter.clear();
|
||||||
mPlaylistAdapter.addAll(playlist);
|
mPlaylistAdapter.addAll(playlist);
|
||||||
playTrack(start);
|
mPlayService.getService().setPlaylist(playlist, 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects a media renderer.
|
* Selects a media renderer.
|
||||||
*/
|
*/
|
||||||
@Override
|
@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) {
|
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(
|
mSubscriptionCallback = new SubscriptionCallback(
|
||||||
getService("AVTransport"), 600) {
|
getService("AVTransport"), 600) {
|
||||||
|
|
||||||
|
@ -311,19 +300,13 @@ public class RendererFragment extends Fragment implements
|
||||||
pollTimePosition();
|
pollTimePosition();
|
||||||
break;
|
break;
|
||||||
case STOPPED:
|
case STOPPED:
|
||||||
if (!mManuallyStopped &&
|
// fallthrough
|
||||||
(mPlaylist.size() > mCurrentTrack + 1)) {
|
|
||||||
Log.d(TAG, "next");
|
|
||||||
mManuallyStopped = false;
|
|
||||||
playTrack(mCurrentTrack +1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case PAUSED_PLAYBACK:
|
case PAUSED_PLAYBACK:
|
||||||
mManuallyStopped = false;
|
|
||||||
mPlayPause.setText(R.string.play);
|
mPlayPause.setText(R.string.play);
|
||||||
mPlaying = false;
|
mPlaying = false;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -347,16 +330,8 @@ public class RendererFragment extends Fragment implements
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
mUpnpService.getControlPoint().execute(mSubscriptionCallback);
|
mUpnpService.getControlPoint().execute(mSubscriptionCallback);
|
||||||
if (mCachedStart != -1) {
|
|
||||||
setPlaylist(mPlaylist, mCachedStart);
|
|
||||||
mCachedStart = -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mListView.setAdapter(mPlaylistAdapter);
|
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
|
@Override
|
||||||
public boolean onBackPressed() {
|
public boolean onBackPressed() {
|
||||||
if (mListView.getAdapter() == mPlaylistAdapter) {
|
if (mListView.getAdapter() == mPlaylistAdapter) {
|
||||||
if (mPlaying) {
|
mControls.setVisibility(View.GONE);
|
||||||
new AlertDialog.Builder(getActivity())
|
mListView.setAdapter(mRendererAdapter);
|
||||||
.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();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exitPlaylistMode() {
|
|
||||||
mCurrentRenderer = null;
|
|
||||||
mSubscriptionCallback.end();
|
|
||||||
|
|
||||||
mListView.setAdapter(mRendererAdapter);
|
|
||||||
mControls.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays/pauses playback on button click.
|
* Plays/pauses playback on button click.
|
||||||
*/
|
*/
|
||||||
|
@ -404,62 +355,19 @@ public class RendererFragment extends Fragment implements
|
||||||
switch (v.getId()) {
|
switch (v.getId()) {
|
||||||
case R.id.playpause:
|
case R.id.playpause:
|
||||||
if (mPlaying)
|
if (mPlaying)
|
||||||
pause();
|
mPlayService.getService().pause();
|
||||||
else
|
else
|
||||||
play();
|
mPlayService.getService().play();
|
||||||
break;
|
break;
|
||||||
case R.id.previous:
|
case R.id.previous:
|
||||||
if (mCurrentTrack != 0 && !mPlaylist.isEmpty())
|
mPlayService.getService().playPrevious();
|
||||||
playTrack(mCurrentTrack - 1);
|
|
||||||
break;
|
break;
|
||||||
case R.id.next:
|
case R.id.next:
|
||||||
if (mPlaylist.size() > mCurrentTrack + 1)
|
mPlayService.getService().playNext();
|
||||||
playTrack(mCurrentTrack + 1);
|
|
||||||
break;
|
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.
|
* Sends manual seek on progress bar to renderer.
|
||||||
*/
|
*/
|
||||||
|
@ -496,10 +404,14 @@ public class RendererFragment extends Fragment implements
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings("rawtypes")
|
||||||
public void changeVolume(final boolean increase) {
|
public void changeVolume(final boolean increase) {
|
||||||
if (mCurrentRenderer == null)
|
if (mCurrentRenderer == null) {
|
||||||
|
Toast.makeText(getActivity(), R.string.select_renderer,
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
final Service<?, ?> service = getService("RenderingControl");
|
final Service<?, ?> service = getService("RenderingControl");
|
||||||
mUpnpService.getControlPoint().execute(new GetVolume(service) {
|
mUpnpService.getControlPoint().execute(
|
||||||
|
new GetVolume(service) {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void failure(ActionInvocation invocation,
|
public void failure(ActionInvocation invocation,
|
||||||
|
@ -511,8 +423,8 @@ public class RendererFragment extends Fragment implements
|
||||||
public void received(ActionInvocation invocation, int volume) {
|
public void received(ActionInvocation invocation, int volume) {
|
||||||
int newVolume = volume + ((increase) ? 4 : -4);
|
int newVolume = volume + ((increase) ? 4 : -4);
|
||||||
if (newVolume < 0) newVolume = 0;
|
if (newVolume < 0) newVolume = 0;
|
||||||
mUpnpService.getControlPoint().execute(new SetVolume(service,
|
mUpnpService.getControlPoint().execute(
|
||||||
newVolume) {
|
new SetVolume(service, newVolume) {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void failure(ActionInvocation invocation,
|
public void failure(ActionInvocation invocation,
|
||||||
|
|
|
@ -74,7 +74,7 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene
|
||||||
/**
|
/**
|
||||||
* Connection Cling to UPNP service.
|
* Connection Cling to UPNP service.
|
||||||
*/
|
*/
|
||||||
private ServiceConnection mServiceConnection= new ServiceConnection() {
|
private ServiceConnection mUpnpServiceConnection = new ServiceConnection() {
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||||
|
@ -105,7 +105,7 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene
|
||||||
|
|
||||||
getActivity().getApplicationContext().bindService(
|
getActivity().getApplicationContext().bindService(
|
||||||
new Intent(getActivity(), AndroidUpnpServiceImpl.class),
|
new Intent(getActivity(), AndroidUpnpServiceImpl.class),
|
||||||
mServiceConnection,
|
mUpnpServiceConnection,
|
||||||
Context.BIND_AUTO_CREATE
|
Context.BIND_AUTO_CREATE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -118,7 +118,7 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
if (mUpnpService != null)
|
if (mUpnpService != null)
|
||||||
mUpnpService.getRegistry().removeListener(mServerAdapter);
|
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