diff --git a/src/com/github/nutomic/controldlna/gui/MainActivity.java b/src/com/github/nutomic/controldlna/gui/MainActivity.java index 115d208..9b8bf00 100644 --- a/src/com/github/nutomic/controldlna/gui/MainActivity.java +++ b/src/com/github/nutomic/controldlna/gui/MainActivity.java @@ -33,6 +33,7 @@ import org.teleal.cling.support.model.item.Item; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.app.FragmentTransaction; import android.support.v4.view.ViewPager; @@ -66,7 +67,7 @@ public class MainActivity extends ActionBarActivity { public Fragment getItem(int position) { switch (position) { case 0: return mServerFragment; - case 1: return mRendererFragment; + case 1: return mRouteFragment; default: return null; } } @@ -78,9 +79,9 @@ public class MainActivity extends ActionBarActivity { }; - private ServerFragment mServerFragment = new ServerFragment(); + private ServerFragment mServerFragment; - private RouteFragment mRendererFragment = new RouteFragment(); + private RouteFragment mRouteFragment; ViewPager mViewPager; @@ -88,7 +89,7 @@ public class MainActivity extends ActionBarActivity { * Initializes tab navigation. */ public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + super.onCreate(savedInstanceState); final ActionBar actionBar = getSupportActionBar(); actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); @@ -125,7 +126,30 @@ public class MainActivity extends ActionBarActivity { .setTabListener(tabListener)); actionBar.addTab(actionBar.newTab() .setText(R.string.title_renderer) - .setTabListener(tabListener)); + .setTabListener(tabListener)); + + if (savedInstanceState != null) { + FragmentManager fm = getSupportFragmentManager(); + mServerFragment = (ServerFragment) fm.getFragment( + savedInstanceState, ServerFragment.class.getName()); + mRouteFragment = (RouteFragment) fm.getFragment( + savedInstanceState, RouteFragment.class.getName()); + } + else { + mServerFragment = new ServerFragment(); + mRouteFragment = new RouteFragment(); + } + } + + /** + * Saves fragments. + */ + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + FragmentManager fm = getSupportFragmentManager(); + fm.putFragment(outState, ServerFragment.class.getName(), mServerFragment); + fm.putFragment(outState, RouteFragment.class.getName(), mRouteFragment); } /** @@ -148,11 +172,11 @@ public class MainActivity extends ActionBarActivity { switch (event.getKeyCode()) { case KeyEvent.KEYCODE_VOLUME_UP: if (event.getAction() == KeyEvent.ACTION_DOWN) - mRendererFragment.increaseVolume(); + mRouteFragment.increaseVolume(); return true; case KeyEvent.KEYCODE_VOLUME_DOWN: if (event.getAction() == KeyEvent.ACTION_DOWN) - mRendererFragment.decreaseVolume(); + mRouteFragment.decreaseVolume(); return true; default: return super.dispatchKeyEvent(event); @@ -164,7 +188,7 @@ public class MainActivity extends ActionBarActivity { */ public void play(List playlist, int start) { mViewPager.setCurrentItem(1); - mRendererFragment.play(playlist, start); + mRouteFragment.play(playlist, start); } } diff --git a/src/com/github/nutomic/controldlna/gui/RouteFragment.java b/src/com/github/nutomic/controldlna/gui/RouteFragment.java index 5069a52..a77db44 100644 --- a/src/com/github/nutomic/controldlna/gui/RouteFragment.java +++ b/src/com/github/nutomic/controldlna/gui/RouteFragment.java @@ -107,7 +107,8 @@ public class RouteFragment extends MediaRouteDiscoveryFragment implements public void onServiceConnected(ComponentName className, IBinder service) { mMediaRouterPlayService = (MediaRouterPlayServiceBinder) service; - mMediaRouterPlayService.getService().setRendererFragment(RouteFragment.this); + mMediaRouterPlayService.getService().setRouterFragment(RouteFragment.this); + mPlaylistAdapter.addAll(mMediaRouterPlayService.getService().getPlaylist()); } public void onServiceDisconnected(ComponentName className) { @@ -140,6 +141,8 @@ public class RouteFragment extends MediaRouteDiscoveryFragment implements super.onActivityCreated(savedInstanceState); mRouteAdapter = new RouteAdapter(getActivity()); + mRouteAdapter.addAll(MediaRouter.getInstance(getActivity()).getRoutes()); + mRouteAdapter.remove(MediaRouter.getInstance(getActivity()).getDefaultRoute()); mPlaylistAdapter = new FileArrayAdapter(getActivity()); mListView = (ListView) getView().findViewById(R.id.listview); @@ -163,11 +166,36 @@ public class RouteFragment extends MediaRouteDiscoveryFragment implements mPlayPause.setOnClickListener(this); mPlayPause.setImageResource(R.drawable.ic_media_play); - getActivity().bindService( - new Intent(getActivity(), MediaRouterPlayService.class), - mPlayServiceConnection, - Context.BIND_AUTO_CREATE + getActivity().getApplicationContext().startService( + new Intent(getActivity(), MediaRouterPlayService.class)); + getActivity().getApplicationContext().bindService( + new Intent(getActivity(), MediaRouterPlayService.class), + mPlayServiceConnection, + Context.BIND_AUTO_CREATE ); + + if (savedInstanceState != null) { + mListView.onRestoreInstanceState(savedInstanceState.getParcelable("list_state")); + if (savedInstanceState.getBoolean("route_selected")) { + mRouteSelected = true; + mListView.setAdapter(mPlaylistAdapter); + mControls.setVisibility(View.VISIBLE); + } + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putBoolean("route_selected", mRouteSelected); + outState.putParcelable("list_state", mListView.onSaveInstanceState()); + } + + @Override + public void onDestroy() { + super.onDestroy(); + getActivity().getApplicationContext().unbindService(mPlayServiceConnection); } /** @@ -241,9 +269,7 @@ public class RouteFragment extends MediaRouteDiscoveryFragment implements mRouteSelected = true; mListView.setAdapter(mPlaylistAdapter); mControls.setVisibility(View.VISIBLE); - if (mStartPlayingOnSelect == -1) - mPlaylistAdapter.clear(); - else { + if (mStartPlayingOnSelect != -1) { mMediaRouterPlayService.getService().play(mStartPlayingOnSelect); mStartPlayingOnSelect = -1; } @@ -256,7 +282,7 @@ public class RouteFragment extends MediaRouteDiscoveryFragment implements * Sets colored background on the item that is currently playing. */ private void enableTrackHighlight() { - if (mListView.getAdapter() == mRouteAdapter) + if (mListView.getAdapter() == mRouteAdapter || mMediaRouterPlayService == null || !isVisible()) return; disableTrackHighlight(); diff --git a/src/com/github/nutomic/controldlna/gui/ServerFragment.java b/src/com/github/nutomic/controldlna/gui/ServerFragment.java index 7706051..54b153c 100644 --- a/src/com/github/nutomic/controldlna/gui/ServerFragment.java +++ b/src/com/github/nutomic/controldlna/gui/ServerFragment.java @@ -31,18 +31,28 @@ import java.util.ArrayList; import java.util.List; import java.util.Stack; +import org.teleal.cling.android.AndroidUpnpService; +import org.teleal.cling.android.AndroidUpnpServiceImpl; 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.LocalDevice; +import org.teleal.cling.model.meta.RemoteDevice; import org.teleal.cling.model.meta.Service; import org.teleal.cling.model.types.ServiceType; +import org.teleal.cling.model.types.UDN; import org.teleal.cling.support.contentdirectory.callback.Browse; import org.teleal.cling.support.model.BrowseFlag; import org.teleal.cling.support.model.DIDLContent; import org.teleal.cling.support.model.container.Container; import org.teleal.cling.support.model.item.Item; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; import android.os.Bundle; +import android.os.IBinder; import android.os.Parcelable; import android.support.v4.app.ListFragment; import android.util.Log; @@ -50,7 +60,6 @@ import android.view.View; import android.widget.ListView; 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.FileArrayAdapter; @@ -78,6 +87,8 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene */ private Device mCurrentServer; + private String mRestoreServer; + /** * ListView adapter for showing a list of files/folders. */ @@ -94,10 +105,40 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene */ private Stack mListState = new Stack(); - /** - * Manages all UPNP connections including playback. - */ - private UpnpController mController = new UpnpController(); + protected AndroidUpnpService mUpnpService; + + private ServiceConnection mUpnpServiceConnection = new ServiceConnection() { + + /** + * Registers DeviceListener, adds known devices and starts search if requested. + */ + public void onServiceConnected(ComponentName className, IBinder service) { + mUpnpService = (AndroidUpnpService) service; + mUpnpService.getRegistry().addListener(mServerAdapter); + for (Device d : mUpnpService.getControlPoint().getRegistry().getDevices()) { + if (d instanceof LocalDevice) + mServerAdapter.localDeviceAdded(mUpnpService.getRegistry(), (LocalDevice) d); + else + mServerAdapter.remoteDeviceAdded(mUpnpService.getRegistry(), (RemoteDevice) d); + } + Log.i(TAG, "Starting device search"); + mUpnpService.getControlPoint().search(); + + if (mRestoreServer != null) { + mCurrentServer = mUpnpService.getControlPoint().getRegistry() + .getDevice(new UDN(mRestoreServer.replace("uuid:", "")), false); + if (mCurrentServer != null) { + setListAdapter(mFileAdapter); + getFiles(true); + } + getListView().onRestoreInstanceState(mListState.lastElement()); + } + } + + public void onServiceDisconnected(ComponentName className) { + mUpnpService = null; + } + }; /** * Initializes ListView adapters, launches Cling UPNP service. @@ -110,26 +151,44 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene mServerAdapter = new DeviceArrayAdapter( getActivity(), DeviceArrayAdapter.SERVER); setListAdapter(mServerAdapter); - mController.open(getActivity()); - mController.addCallback(mServerAdapter); + getActivity().getApplicationContext().bindService( + new Intent(getActivity(), AndroidUpnpServiceImpl.class), + mUpnpServiceConnection, + Context.BIND_AUTO_CREATE + ); + + if (savedInstanceState != null) { + mRestoreServer = savedInstanceState.getString("current_server"); + mCurrentPath.addAll(savedInstanceState.getStringArrayList("path")); + mListState.addAll(savedInstanceState.getParcelableArrayList("list_state")); + } + else + mListState.push(getListView().onSaveInstanceState()); } @Override - public void onResume() { - super.onResume(); - mController.startSearch(); + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString("current_server", (mCurrentServer != null) + ? mCurrentServer.getIdentity().getUdn().toString() + : ""); + outState.putStringArrayList("path", new ArrayList(mCurrentPath)); + mListState.pop(); + mListState.push(getListView().onSaveInstanceState()); + outState.putParcelableArrayList("list_state", new ArrayList(mListState)); } @Override public void onPause() { super.onPause(); - mController.stopSearch(); + mListState.pop(); + mListState.push(getListView().onSaveInstanceState()); } @Override public void onDestroy() { super.onDestroy(); - mController.close(getActivity()); + getActivity().getApplicationContext().unbindService(mUpnpServiceConnection); } /** @@ -175,7 +234,7 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene private void getFiles(final boolean restoreListState) { Service service = mCurrentServer.findService( new ServiceType("schemas-upnp-org", "ContentDirectory")); - mController.execute(new Browse(service, + mUpnpService.getControlPoint().execute(new Browse(service, mCurrentPath.peek(), BrowseFlag.DIRECT_CHILDREN) { @SuppressWarnings("rawtypes") @@ -192,7 +251,7 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene for (Item i : didl.getItems()) mFileAdapter.add(i); if (restoreListState) - getListView().onRestoreInstanceState(mListState.pop()); + getListView().onRestoreInstanceState(mListState.peek()); else getListView().setSelectionFromTop(0, 0); } @@ -223,14 +282,14 @@ public class ServerFragment extends ListFragment implements OnBackPressedListene return false; mCurrentPath.pop(); + mListState.pop(); if (mCurrentPath.empty()) { setListAdapter(mServerAdapter); - getListView().onRestoreInstanceState(mListState.pop()); + getListView().onRestoreInstanceState(mListState.peek()); mCurrentServer = null; } - else { + else getFiles(true); - } return true; } diff --git a/src/com/github/nutomic/controldlna/mediarouter/MediaRouterPlayService.java b/src/com/github/nutomic/controldlna/mediarouter/MediaRouterPlayService.java index 21fcc62..50e24d4 100644 --- a/src/com/github/nutomic/controldlna/mediarouter/MediaRouterPlayService.java +++ b/src/com/github/nutomic/controldlna/mediarouter/MediaRouterPlayService.java @@ -90,10 +90,12 @@ public class MediaRouterPlayService extends Service { private String mSessionId; - private WeakReference mRendererFragment = new WeakReference(null); + private WeakReference mRouterFragment = new WeakReference(null); private boolean mPollingStatus = false; + private boolean mBound; + /** * Creates a notification after the icon bitmap is loaded. */ @@ -118,10 +120,8 @@ public class MediaRouterPlayService extends Service { .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; + startForeground(NOTIFICATION_ID, notification); } } @@ -136,11 +136,28 @@ public class MediaRouterPlayService extends Service { @Override public IBinder onBind(Intent intent) { + mBound = true; return mBinder; } - public void setRendererFragment(RouteFragment rf) { - mRendererFragment = new WeakReference(rf); + /** + * Stops service after a delay if no media is playing (delay in case the + * fragment is recreated for screen rotation). + */ + @Override + public boolean onUnbind(Intent intent) { + new Handler().postDelayed(new Runnable() { + public void run() { + if (!mPollingStatus && !mBound) + stopSelf(); + } + }, 5000); + mBound = false; + return super.onUnbind(intent); + } + + public void setRouterFragment(RouteFragment rf) { + mRouterFragment = new WeakReference(rf); } public void selectRoute(RouteInfo route) { @@ -256,11 +273,6 @@ public class MediaRouterPlayService extends Service { 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. @@ -276,13 +288,15 @@ public class MediaRouterPlayService extends Service { @Override public void onResult(Bundle data) { MediaItemStatus status = MediaItemStatus.fromBundle(data); - if (mRendererFragment.get() != null) - mRendererFragment.get().receivePlaybackStatus(status); + if (mRouterFragment.get() != null) + mRouterFragment.get().receivePlaybackStatus(status); if (status.getPlaybackState() == MediaItemStatus.PLAYBACK_STATE_FINISHED) { if (mCurrentTrack + 1 < mPlaylist.size()) playNext(); else { + if (!mBound) + stopSelf(); mPollingStatus = false; NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); @@ -309,5 +323,9 @@ public class MediaRouterPlayService extends Service { public void decreaseVolume() { mMediaRouter.getSelectedRoute().requestUpdateVolume(-1); } + + public List getPlaylist() { + return mPlaylist; + } } diff --git a/src/com/github/nutomic/controldlna/upnp/UpnpController.java b/src/com/github/nutomic/controldlna/upnp/UpnpController.java index d2996ad..533dca3 100644 --- a/src/com/github/nutomic/controldlna/upnp/UpnpController.java +++ b/src/com/github/nutomic/controldlna/upnp/UpnpController.java @@ -97,11 +97,8 @@ public class UpnpController implements RegistryListener { else remoteDeviceAdded(mUpnpService.getRegistry(), (RemoteDevice) d); } - if (mStartSearchImmediately) { - Log.i(TAG, "Starting device search"); - mUpnpService.getControlPoint().search(); - mStartSearchImmediately = false; - } + if (mStartSearchImmediately) + startSearch(); } public void onServiceDisconnected(ComponentName className) { @@ -139,6 +136,7 @@ public class UpnpController implements RegistryListener { if (mUpnpService != null) { Log.i(TAG, "Starting device search"); mUpnpService.getControlPoint().search(); + mStartSearchImmediately = false; } else mStartSearchImmediately = true; diff --git a/src/com/github/nutomic/controldlna/utility/DeviceArrayAdapter.java b/src/com/github/nutomic/controldlna/utility/DeviceArrayAdapter.java index 7e5bb02..7e1d7a2 100644 --- a/src/com/github/nutomic/controldlna/utility/DeviceArrayAdapter.java +++ b/src/com/github/nutomic/controldlna/utility/DeviceArrayAdapter.java @@ -31,7 +31,10 @@ import java.net.URI; import java.net.URISyntaxException; 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.app.Activity; import android.content.Context; @@ -43,7 +46,6 @@ import android.widget.ArrayAdapter; import android.widget.TextView; import com.github.nutomic.controldlna.R; -import com.github.nutomic.controldlna.upnp.UpnpController.DeviceListenerCallback; /** @@ -54,7 +56,7 @@ import com.github.nutomic.controldlna.upnp.UpnpController.DeviceListenerCallback * */ public class DeviceArrayAdapter extends ArrayAdapter> - implements DeviceListenerCallback { + implements RegistryListener { private static final String TAG = "DeviceArrayAdapter"; @@ -106,8 +108,7 @@ public class DeviceArrayAdapter extends ArrayAdapter> /** * Adds a new device to the list if its type equals mDeviceType. */ - @Override - public void deviceAdded(final Device device) { + private void deviceAdded(final Device device) { mActivity.runOnUiThread(new Runnable() { @Override @@ -121,8 +122,7 @@ public class DeviceArrayAdapter extends ArrayAdapter> /** * Removes the device from the list (if it is an element). */ - @Override - public void deviceRemoved(final Device device) { + private void deviceRemoved(final Device device) { mActivity.runOnUiThread(new Runnable() { @Override @@ -133,8 +133,7 @@ public class DeviceArrayAdapter extends ArrayAdapter> }); } - @Override - public void deviceUpdated(Device device) { + private void deviceUpdated(Device device) { mActivity.runOnUiThread(new Runnable() { @Override @@ -143,4 +142,47 @@ public class DeviceArrayAdapter extends ArrayAdapter> } }); } + + @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) { + deviceUpdated(device); + } + + @Override + public void afterShutdown() { + } }