Changed playback to work entirely through MediaRouter.
This allows playing to non-UPNP-devices.
This commit is contained in:
parent
d19b9aaf39
commit
838ad69360
20 changed files with 1613 additions and 1622 deletions
|
@ -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>
|
||||||
|
|
3
src/android/os/Messenger.aidl
Normal file
3
src/android/os/Messenger.aidl
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package android.os;
|
||||||
|
|
||||||
|
parcelable Messenger;
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
414
src/com/github/nutomic/controldlna/gui/RouteFragment.java
Normal file
414
src/com/github/nutomic/controldlna/gui/RouteFragment.java
Normal 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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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")
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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.
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
469
src/com/github/nutomic/controldlna/upnp/RemotePlayService.java
Normal file
469
src/com/github/nutomic/controldlna/upnp/RemotePlayService.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
36
src/com/github/nutomic/controldlna/utility/RouteAdapter.java
Normal file
36
src/com/github/nutomic/controldlna/utility/RouteAdapter.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in a new issue