aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml6
m---------ServerProxy0
-rw-r--r--libs/cling-core-2.0.1.jarbin0 -> 686501 bytes
-rw-r--r--libs/cling-support-2.0.1.jarbin0 -> 490043 bytes
-rw-r--r--libs/javax.servlet-3.0.0.v201112011016.jarbin0 -> 200387 bytes
-rw-r--r--libs/jetty-all-8.1.16.v20140903.jarbin0 -> 1880786 bytes
-rw-r--r--libs/seamless-http-1.1.0.jarbin0 -> 21646 bytes
-rw-r--r--libs/seamless-util-1.1.0.jarbin0 -> 94456 bytes
-rw-r--r--libs/seamless-xml-1.1.0.jarbin0 -> 63142 bytes
-rw-r--r--proguard.cfg14
-rw-r--r--res/xml/changelog.xml29
-rw-r--r--src/github/daneren2005/dsub/domain/DLNADevice.java78
-rw-r--r--src/github/daneren2005/dsub/domain/RemoteControlState.java3
-rw-r--r--src/github/daneren2005/dsub/fragments/NowPlayingFragment.java28
-rw-r--r--src/github/daneren2005/dsub/provider/DLNARouteProvider.java402
-rw-r--r--src/github/daneren2005/dsub/service/ChromeCastController.java21
-rw-r--r--src/github/daneren2005/dsub/service/DLNAController.java521
-rw-r--r--src/github/daneren2005/dsub/service/DownloadService.java71
-rw-r--r--src/github/daneren2005/dsub/service/RemoteController.java3
-rw-r--r--src/github/daneren2005/dsub/util/MediaRouteManager.java23
-rw-r--r--src/github/daneren2005/dsub/util/Util.java16
21 files changed, 1159 insertions, 56 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 98eb63b4..a89d122f 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="github.daneren2005.dsub"
android:installLocation="internalOnly"
- android:versionCode="140"
- android:versionName="4.8.6">
+ android:versionCode="141"
+ android:versionName="4.9 Beta 3">
<instrumentation android:name="android.test.InstrumentationTestRunner"
android:targetPackage="github.daneren2005.dsub"
@@ -23,6 +23,7 @@
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
@@ -85,6 +86,7 @@
<service android:name=".service.DownloadService"
android:label="Subsonic Download Service"/>
+ <service android:name="org.fourthline.cling.android.AndroidUpnpServiceImpl"/>
<service android:name="github.daneren2005.dsub.service.sync.AuthenticatorService">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
diff --git a/ServerProxy b/ServerProxy
-Subproject a0bfee5ce4c9ad8c480b4d09e027bf48a71b75e
+Subproject 3149417702f5a66bd99feb412c9023f4c6c160b
diff --git a/libs/cling-core-2.0.1.jar b/libs/cling-core-2.0.1.jar
new file mode 100644
index 00000000..632d3038
--- /dev/null
+++ b/libs/cling-core-2.0.1.jar
Binary files differ
diff --git a/libs/cling-support-2.0.1.jar b/libs/cling-support-2.0.1.jar
new file mode 100644
index 00000000..7fa28604
--- /dev/null
+++ b/libs/cling-support-2.0.1.jar
Binary files differ
diff --git a/libs/javax.servlet-3.0.0.v201112011016.jar b/libs/javax.servlet-3.0.0.v201112011016.jar
new file mode 100644
index 00000000..b1354096
--- /dev/null
+++ b/libs/javax.servlet-3.0.0.v201112011016.jar
Binary files differ
diff --git a/libs/jetty-all-8.1.16.v20140903.jar b/libs/jetty-all-8.1.16.v20140903.jar
new file mode 100644
index 00000000..25b1d324
--- /dev/null
+++ b/libs/jetty-all-8.1.16.v20140903.jar
Binary files differ
diff --git a/libs/seamless-http-1.1.0.jar b/libs/seamless-http-1.1.0.jar
new file mode 100644
index 00000000..98ec884a
--- /dev/null
+++ b/libs/seamless-http-1.1.0.jar
Binary files differ
diff --git a/libs/seamless-util-1.1.0.jar b/libs/seamless-util-1.1.0.jar
new file mode 100644
index 00000000..12026b7f
--- /dev/null
+++ b/libs/seamless-util-1.1.0.jar
Binary files differ
diff --git a/libs/seamless-xml-1.1.0.jar b/libs/seamless-xml-1.1.0.jar
new file mode 100644
index 00000000..1e740877
--- /dev/null
+++ b/libs/seamless-xml-1.1.0.jar
Binary files differ
diff --git a/proguard.cfg b/proguard.cfg
index b7df6dcf..7db1cdbd 100644
--- a/proguard.cfg
+++ b/proguard.cfg
@@ -45,4 +45,16 @@
-keep class android.support.v7.app.MediaRouteButton { *; }
--dontwarn android.support.** \ No newline at end of file
+-dontwarn android.support.**
+
+# DLNA/Cling
+-keep class org.fourthline.cling.android.AndroidUpnpServiceImpl { *; }
+-dontwarn javax.**
+-dontwarn org.objectweb.**
+-dontwarn org.slf4j.**
+-dontwarn org.mortbay.**
+-dontwarn org.fourthline.**
+-dontwarn org.seamless.**
+-dontwarn org.eclipse.**
+-dontwarn java.**
+-keepattributes *Annotation*, InnerClasses \ No newline at end of file
diff --git a/res/xml/changelog.xml b/res/xml/changelog.xml
index 282e2d00..843b811a 100644
--- a/res/xml/changelog.xml
+++ b/res/xml/changelog.xml
@@ -1,7 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<changelog>
+ <release version="4.9 Beta 3" versioncode="141" releasedate="1/23/2014">
+ <change>WARNING: Changing position while casting to DLNA doesn't work well</change>
+ <change>DLNA casts metadata</change>
+ <change>Offline DLNA casting</change>
+ <change>DLNA works with more devices</change>
+ <change>On starred list, load artist image if one exists</change>
+ <change>Hide folder selection if user only has one</change>
+ <change>Fix seeking after file finished downloading restarting the song</change>
+ <change>Fix more issues with artist headers</change>
+ <change>Fix issues when browsing offline</change>
+ <change>Fix bookmarks not being auto deleted while casting</change>
+ <change>Fix search with tag browsing on Ampache servers</change>
+ </release>
+ <release version="4.9 Beta 2" versioncode="136" releasedate="1/15/2014">
+ <change>Fixed issue of DLNA not showing up for some people</change>
+ <change>Add artist info header (Subsonic 5.1+)</change>
+ <change>Add Similar Artists option (Subsonic 5.1+)</change>
+ <change>View similar artists missing from Subsonic (Subsonic 5.1+)</change>
+ <change>Podcasts: clicking on description wraps around image to display everything</change>
+ <change>Delete artwork/avatars on Clean Cache</change>
+ <change>Fix sleep timer incrementing on it's own</change>
+ <change>Fixed various crashes</change>
+ </release>
+ <release version="4.9 Beta 1" versioncode="135" releasedate="12/27/2014">
+ <change>Early DLNA support</change>
+ <change>I have only tested against XBMC. I need feedback on what does and doesn't work</change>
+ <change>I know no metadata shows up. I haven't been able to figure that part out yet.</change>
+ </release>
<release version="4.8.6" versioncode="134" releasedate="12/27/2014">
- <change>Join the Beta channel to try out DLNA casting!</change>
<change>Play/shuffle quick album lists such as Recently Added or Random</change>
<change>Change download status to a percentage</change>
<change>Improved unknown album art</change>
diff --git a/src/github/daneren2005/dsub/domain/DLNADevice.java b/src/github/daneren2005/dsub/domain/DLNADevice.java
new file mode 100644
index 00000000..2de84013
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/DLNADevice.java
@@ -0,0 +1,78 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2014 (C) Scott Jackson
+*/
+
+package github.daneren2005.dsub.domain;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.fourthline.cling.model.meta.Device;
+
+/**
+ * Created by Scott on 11/1/2014.
+ */
+public class DLNADevice implements Parcelable {
+ public Device renderer;
+ public String id;
+ public String name;
+ public String description;
+ public int volume;
+ public int volumeMax;
+
+ public static final Parcelable.Creator<DLNADevice> CREATOR = new Parcelable.Creator<DLNADevice>() {
+ public DLNADevice createFromParcel(Parcel in) {
+ return new DLNADevice(in);
+ }
+
+ public DLNADevice[] newArray(int size) {
+ return new DLNADevice[size];
+ }
+ };
+
+ private DLNADevice(Parcel in) {
+ id = in.readString();
+ name = in.readString();
+ description = in.readString();
+ volume = in.readInt();
+ volumeMax = in.readInt();
+ }
+
+ public DLNADevice(Device renderer, String id, String name, String description, int volume, int volumeMax) {
+ this.renderer = renderer;
+ this.id = id;
+ this.name = name;
+ this.description = description;
+ this.volume = volume;
+ this.volumeMax = volumeMax;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeString(name);
+ dest.writeString(description);
+ dest.writeInt(volume);
+ dest.writeInt(volumeMax);
+ }
+}
diff --git a/src/github/daneren2005/dsub/domain/RemoteControlState.java b/src/github/daneren2005/dsub/domain/RemoteControlState.java
index 9bf3bf91..47895984 100644
--- a/src/github/daneren2005/dsub/domain/RemoteControlState.java
+++ b/src/github/daneren2005/dsub/domain/RemoteControlState.java
@@ -23,7 +23,8 @@ public enum RemoteControlState {
LOCAL(0),
JUKEBOX_SERVER(1),
CHROMECAST(2),
- REMOTE_CLIENT(3);
+ REMOTE_CLIENT(3),
+ DLNA(4);
private final int mRemoteControlState;
diff --git a/src/github/daneren2005/dsub/fragments/NowPlayingFragment.java b/src/github/daneren2005/dsub/fragments/NowPlayingFragment.java
index d3dea3bd..c0f528de 100644
--- a/src/github/daneren2005/dsub/fragments/NowPlayingFragment.java
+++ b/src/github/daneren2005/dsub/fragments/NowPlayingFragment.java
@@ -84,12 +84,9 @@ import github.daneren2005.dsub.activity.SubsonicActivity;
public class NowPlayingFragment extends SubsonicFragment implements OnGestureListener {
private static final String TAG = NowPlayingFragment.class.getSimpleName();
-
- public static final int DIALOG_SAVE_PLAYLIST = 100;
private static final int PERCENTAGE_OF_SCREEN_FOR_SWIPE = 10;
- private static final int COLOR_BUTTON_ENABLED = Color.rgb(51, 181, 229);
- private static final int COLOR_BUTTON_DISABLED = Color.rgb(206, 213, 211);
private static final int INCREMENT_TIME = 5000;
+ private static final int SERVICE_BACKOFF = 200;
private static final int ACTION_PREVIOUS = 1;
private static final int ACTION_NEXT = 2;
@@ -873,6 +870,21 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
}
if(downloadService != null) {
downloadService.startRemoteScan();
+ } else {
+ // Make sure to call remote scan once the service is ready
+ final Runnable waitForService = new Runnable() {
+ @Override
+ public void run() {
+ DownloadService service = getDownloadService();
+ if(service != null) {
+ service.startRemoteScan();
+ } else {
+ handler.postDelayed(this, SERVICE_BACKOFF);
+ }
+ }
+ };
+
+ handler.postDelayed(waitForService, SERVICE_BACKOFF);
}
}
@@ -1009,7 +1021,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
lengthBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
- if(fromUser) {
+ if (fromUser) {
int length = getMinutes(progress);
lengthBox.setText(Util.formatDuration(length));
seekBar.setProgress(progress);
@@ -1250,18 +1262,18 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
onProgressChangedTask = new SilentBackgroundTask<Void>(context) {
DownloadService downloadService;
- boolean isJukeboxEnabled;
int millisPlayed;
Integer duration;
PlayerState playerState;
+ boolean isSeekable;
@Override
protected Void doInBackground() throws Throwable {
downloadService = getDownloadService();
- isJukeboxEnabled = downloadService.isRemoteEnabled();
millisPlayed = Math.max(0, downloadService.getPlayerPosition());
duration = downloadService.getPlayerDuration();
playerState = getDownloadService().getPlayerState();
+ isSeekable = downloadService.isSeekable();
return null;
}
@@ -1280,7 +1292,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
if(!seekInProgress) {
progressBar.setProgress(millisPlayed);
}
- progressBar.setEnabled((currentPlaying.isWorkDone() || isJukeboxEnabled) && playerState != PlayerState.PREPARING);
+ progressBar.setEnabled(isSeekable);
} else {
positionTextView.setText("0:00");
durationTextView.setText("-:--");
diff --git a/src/github/daneren2005/dsub/provider/DLNARouteProvider.java b/src/github/daneren2005/dsub/provider/DLNARouteProvider.java
new file mode 100644
index 00000000..e7f1afb3
--- /dev/null
+++ b/src/github/daneren2005/dsub/provider/DLNARouteProvider.java
@@ -0,0 +1,402 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2014 (C) Scott Jackson
+*/
+package github.daneren2005.dsub.provider;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.media.AudioManager;
+import android.media.MediaRouter;
+import android.os.IBinder;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaRouteDescriptor;
+import android.support.v7.media.MediaRouteDiscoveryRequest;
+import android.support.v7.media.MediaRouteProvider;
+import android.support.v7.media.MediaRouteProviderDescriptor;
+import android.util.Log;
+
+import org.eclipse.jetty.util.log.Logger;
+import org.fourthline.cling.android.AndroidUpnpService;
+import org.fourthline.cling.android.AndroidUpnpServiceImpl;
+import org.fourthline.cling.model.action.ActionInvocation;
+import org.fourthline.cling.model.message.UpnpResponse;
+import org.fourthline.cling.model.meta.Device;
+import org.fourthline.cling.model.meta.LocalDevice;
+import org.fourthline.cling.model.meta.RemoteDevice;
+import org.fourthline.cling.model.meta.StateVariable;
+import org.fourthline.cling.model.meta.StateVariableAllowedValueRange;
+import org.fourthline.cling.model.types.ServiceType;
+import org.fourthline.cling.registry.Registry;
+import org.fourthline.cling.registry.RegistryListener;
+import org.fourthline.cling.support.renderingcontrol.callback.GetVolume;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import github.daneren2005.dsub.domain.DLNADevice;
+import github.daneren2005.dsub.domain.RemoteControlState;
+import github.daneren2005.dsub.service.DLNAController;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.RemoteController;
+
+public class DLNARouteProvider extends MediaRouteProvider {
+ private static final String TAG = DLNARouteProvider.class.getSimpleName();
+ public static final String CATEGORY_DLNA = "github.daneren2005.dsub.DLNA";
+
+ private DownloadService downloadService;
+ private RemoteController controller;
+
+ private HashMap<String, DLNADevice> devices = new HashMap<String, DLNADevice>();
+ private List<String> adding = new ArrayList<String>();
+ private AndroidUpnpService dlnaService;
+ private ServiceConnection dlnaServiceConnection;
+
+ public DLNARouteProvider(Context context) {
+ super(context);
+
+ // Use custom logger
+ org.eclipse.jetty.util.log.Log.setLog(new JettyAndroidLog());
+
+ this.downloadService = (DownloadService) context;
+ dlnaServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ dlnaService = (AndroidUpnpService) service;
+ dlnaService.getRegistry().addListener(new RegistryListener() {
+ @Override
+ public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice remoteDevice) {
+ Log.i(TAG, "Stared DLNA discovery");
+ }
+
+ @Override
+ public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice remoteDevice, Exception e) {
+ Log.w(TAG, "Failed to discover DLNA devices");
+ }
+
+ @Override
+ public void remoteDeviceAdded(Registry registry, RemoteDevice remoteDevice) {
+ deviceAdded(remoteDevice);
+ }
+
+ @Override
+ public void remoteDeviceUpdated(Registry registry, RemoteDevice remoteDevice) {
+ deviceAdded(remoteDevice);
+ }
+
+ @Override
+ public void remoteDeviceRemoved(Registry registry, RemoteDevice remoteDevice) {
+ deviceRemoved(remoteDevice);
+ }
+
+ @Override
+ public void localDeviceAdded(Registry registry, LocalDevice localDevice) {
+ deviceAdded(localDevice);
+ }
+
+ @Override
+ public void localDeviceRemoved(Registry registry, LocalDevice localDevice) {
+ deviceRemoved(localDevice);
+ }
+
+ @Override
+ public void beforeShutdown(Registry registry) {
+
+ }
+
+ @Override
+ public void afterShutdown() {
+
+ }
+ });
+
+ for (Device<?, ?, ?> device : dlnaService.getControlPoint().getRegistry().getDevices()) {
+ deviceAdded(device);
+ }
+ dlnaService.getControlPoint().search();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ dlnaService = null;
+ }
+ };
+
+ if(!context.getApplicationContext().bindService(new Intent(context, AndroidUpnpServiceImpl.class), dlnaServiceConnection, Context.BIND_AUTO_CREATE)) {
+ Log.e(TAG, "Failed to bind to DLNA service");
+ }
+ }
+
+ private void broadcastDescriptors() {
+ // Create intents
+ IntentFilter routeIntentFilter = new IntentFilter();
+ routeIntentFilter.addCategory(CATEGORY_DLNA);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_START_SESSION);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_GET_SESSION_STATUS);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_END_SESSION);
+
+ // Create descriptor
+ MediaRouteProviderDescriptor.Builder providerBuilder = new MediaRouteProviderDescriptor.Builder();
+
+ // Create route descriptor
+ for(Map.Entry<String, DLNADevice> deviceEntry: devices.entrySet()) {
+ DLNADevice device = deviceEntry.getValue();
+
+ int increments = device.volumeMax / 10;
+ int volume = controller == null ? device.volume : (int) controller.getVolume();
+ volume = volume / increments;
+
+ MediaRouteDescriptor.Builder routeBuilder = new MediaRouteDescriptor.Builder(device.id, device.name);
+ routeBuilder.addControlFilter(routeIntentFilter)
+ .setPlaybackStream(AudioManager.STREAM_MUSIC)
+ .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
+ .setDescription(device.description)
+ .setVolume(volume)
+ .setVolumeMax(10)
+ .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE);
+ providerBuilder.addRoute(routeBuilder.build());
+ }
+
+ setDescriptor(providerBuilder.build());
+ }
+
+ @Override
+ public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
+ if (request != null && request.isActiveScan()) {
+
+ }
+ }
+
+ @Override
+ public RouteController onCreateRouteController(String routeId) {
+ DLNADevice device = devices.get(routeId);
+ if(device == null) {
+ Log.w(TAG, "No device exists for " + routeId);
+ return null;
+ }
+
+ return new DLNARouteController(device);
+ }
+
+ private void deviceAdded(final Device device) {
+ final org.fourthline.cling.model.meta.Service renderingControl = device.findService(new ServiceType("schemas-upnp-org", "RenderingControl"));
+ if(renderingControl == null) {
+ return;
+ }
+
+ final String id = device.getIdentity().getUdn().toString();
+ // In the process of looking up it's details already
+ if(adding.contains(id)) {
+ return;
+ }
+ adding.add(id);
+
+ if(device.getType().getType().equals("MediaRenderer") && device instanceof RemoteDevice) {
+ try {
+ dlnaService.getControlPoint().execute(new GetVolume(renderingControl) {
+ @Override
+ public void received(ActionInvocation actionInvocation, int currentVolume) {
+ int maxVolume = 100;
+ StateVariable volume = renderingControl.getStateVariable("Volume");
+ if (volume != null) {
+ StateVariableAllowedValueRange volumeRange = volume.getTypeDetails().getAllowedValueRange();
+ maxVolume = (int) volumeRange.getMaximum();
+ }
+
+ // Create a new DLNADevice to represent this item
+ String id = device.getIdentity().getUdn().toString();
+ String name = device.getDetails().getFriendlyName();
+ String displayName = device.getDisplayString();
+
+ DLNADevice newDevice = new DLNADevice(device, id, name, displayName, currentVolume, maxVolume);
+ devices.put(id, newDevice);
+ downloadService.post(new Runnable() {
+ @Override
+ public void run() {
+ broadcastDescriptors();
+ }
+ });
+ adding.remove(id);
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String s) {
+ Log.w(TAG, "Failed to get default volume for DLNA route");
+ Log.w(TAG, "Reason: " + s);
+ adding.remove(id);
+ }
+ });
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to add device", e);
+ }
+ } else {
+ adding.remove(id);
+ }
+ }
+ private void deviceRemoved(Device device) {
+ if(device.getType().getType().equals("MediaRenderer") && device instanceof RemoteDevice) {
+ String id = device.getIdentity().getUdn().toString();
+ devices.remove(id);
+
+ // Make sure we do this on the main thread
+ downloadService.post(new Runnable() {
+ @Override
+ public void run() {
+ broadcastDescriptors();
+ }
+ });
+ }
+ }
+
+ private class DLNARouteController extends RouteController {
+ private DLNADevice device;
+
+ public DLNARouteController(DLNADevice device) {
+ this.device = device;
+ }
+
+ @Override
+ public boolean onControlRequest(Intent intent, android.support.v7.media.MediaRouter.ControlRequestCallback callback) {
+ if (intent.hasCategory(CATEGORY_DLNA)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onRelease() {
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL);
+ controller = null;
+ }
+
+ @Override
+ public void onSelect() {
+ controller = new DLNAController(downloadService, dlnaService.getControlPoint(), device);
+ downloadService.setRemoteEnabled(RemoteControlState.DLNA, controller);
+ }
+
+ @Override
+ public void onUnselect() {
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL);
+ controller = null;
+ }
+
+ @Override
+ public void onUpdateVolume(int delta) {
+ if(controller != null) {
+ controller.updateVolume(delta > 0);
+ }
+ broadcastDescriptors();
+ }
+
+ @Override
+ public void onSetVolume(int volume) {
+ if(controller != null) {
+ controller.setVolume(volume);
+ }
+ broadcastDescriptors();
+ }
+ }
+
+ public static class JettyAndroidLog implements Logger {
+ final private static java.util.logging.Logger log = java.util.logging.Logger.getLogger("Jetty");
+
+ public static boolean __isIgnoredEnabled = false;
+ public String _name;
+
+ public JettyAndroidLog() {
+ this (JettyAndroidLog.class.getName());
+ }
+
+ public JettyAndroidLog(String name) {
+ _name = name;
+ }
+
+ public String getName () {
+ return _name;
+ }
+
+ public void debug(Throwable th) {
+ // Log.d(TAG, "", th);
+ }
+
+ public void debug(String msg, Throwable th) {
+ // Log.d(TAG, msg, th);
+ }
+
+ public void debug(String msg, Object... args) {
+ // Log.d(TAG, msg);
+ }
+
+ public Logger getLogger(String name) {
+ return new JettyAndroidLog(name);
+ }
+
+ public void info(String msg, Object... args) {
+ // Log.i(TAG, msg);
+ }
+
+ public void info(Throwable th) {
+ // Log.i(TAG, "", th);
+ }
+
+ public void info(String msg, Throwable th) {
+ // Log.i(TAG, msg, th);
+ }
+
+ public boolean isDebugEnabled() {
+ return false;
+ }
+
+ public void warn(Throwable th) {
+ // Log.w(TAG, "", th);
+ }
+
+ public void warn(String msg, Object... args) {
+ // Log.w(TAG, msg);
+ }
+
+ public void warn(String msg, Throwable th) {
+ // Log.w(TAG, msg, th);
+ }
+
+ public boolean isIgnoredEnabled () {
+ return __isIgnoredEnabled;
+ }
+
+
+ public void ignore(Throwable ignored) {
+ if (__isIgnoredEnabled) {
+ warn("IGNORED", ignored);
+ }
+ }
+
+ public void setIgnoredEnabled(boolean enabled) {
+ __isIgnoredEnabled = enabled;
+ }
+
+ public void setDebugEnabled(boolean enabled) {
+
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/ChromeCastController.java b/src/github/daneren2005/dsub/service/ChromeCastController.java
index 0c8f38a6..40f5d73d 100644
--- a/src/github/daneren2005/dsub/service/ChromeCastController.java
+++ b/src/github/daneren2005/dsub/service/ChromeCastController.java
@@ -283,7 +283,7 @@ public class ChromeCastController extends RemoteController {
url = musicService.getMusicUrl(downloadService, song, currentPlaying.getBitRate());
}
- url = fixURLs(url);
+ url = Util.replaceInternalUrl(downloadService, url);
}
// Setup song/video information
@@ -300,7 +300,7 @@ public class ChromeCastController extends RemoteController {
String coverArt = "";
if(proxy == null) {
coverArt = musicService.getCoverArtUrl(downloadService, song);
- coverArt = fixURLs(coverArt);
+ coverArt = Util.replaceInternalUrl(downloadService, coverArt);
meta.addImage(new WebImage(Uri.parse(coverArt)));
} else {
File coverArtFile = FileUtil.getAlbumArtFile(downloadService, song);
@@ -354,22 +354,6 @@ public class ChromeCastController extends RemoteController {
}
}
- private String fixURLs(String url) {
- // Only change to internal when using https
- if(url.indexOf("https") != -1) {
- SharedPreferences prefs = Util.getPreferences(downloadService);
- int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
- String internalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance, null);
- if(internalUrl != null && !"".equals(internalUrl)) {
- String externalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
- url = url.replace(internalUrl, externalUrl);
- }
- }
-
- // Use separate profile for Chromecast so users can do ogg on phone, mp3 for CC
- return url.replace(Constants.REST_CLIENT_ID, Constants.CHROMECAST_CLIENT_ID);
- }
-
private void failedLoad() {
Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load));
downloadService.setPlayerState(PlayerState.STOPPED);
@@ -466,6 +450,7 @@ public class ChromeCastController extends RemoteController {
case MediaStatus.PLAYER_STATE_IDLE:
if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_FINISHED) {
downloadService.setPlayerState(PlayerState.COMPLETED);
+ downloadService.postPlayCleanup();
downloadService.onSongCompleted();
} else if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_INTERRUPTED) {
if (downloadService.getPlayerState() != PlayerState.PREPARING) {
diff --git a/src/github/daneren2005/dsub/service/DLNAController.java b/src/github/daneren2005/dsub/service/DLNAController.java
new file mode 100644
index 00000000..5d87e478
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/DLNAController.java
@@ -0,0 +1,521 @@
+/*
+ This file is part of Subsonic.
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+ Copyright 2014 (C) Scott Jackson
+*/
+
+package github.daneren2005.dsub.service;
+
+import android.content.SharedPreferences;
+import android.os.Looper;
+import android.util.Log;
+
+import org.fourthline.cling.controlpoint.ControlPoint;
+import org.fourthline.cling.controlpoint.SubscriptionCallback;
+import org.fourthline.cling.model.action.ActionInvocation;
+import org.fourthline.cling.model.gena.CancelReason;
+import org.fourthline.cling.model.gena.GENASubscription;
+import org.fourthline.cling.model.message.UpnpResponse;
+import org.fourthline.cling.model.meta.Action;
+import org.fourthline.cling.model.meta.Service;
+import org.fourthline.cling.model.meta.StateVariable;
+import org.fourthline.cling.model.state.StateVariableValue;
+import org.fourthline.cling.model.types.ServiceType;
+import org.fourthline.cling.support.avtransport.callback.GetPositionInfo;
+import org.fourthline.cling.support.avtransport.callback.Pause;
+import org.fourthline.cling.support.avtransport.callback.Play;
+import org.fourthline.cling.support.avtransport.callback.Seek;
+import org.fourthline.cling.support.avtransport.callback.SetAVTransportURI;
+import org.fourthline.cling.support.avtransport.callback.Stop;
+import org.fourthline.cling.support.avtransport.lastchange.AVTransportLastChangeParser;
+import org.fourthline.cling.support.avtransport.lastchange.AVTransportVariable;
+import org.fourthline.cling.support.contentdirectory.DIDLParser;
+import org.fourthline.cling.support.lastchange.LastChange;
+import org.fourthline.cling.support.model.DIDLContent;
+import org.fourthline.cling.support.model.DIDLObject;
+import org.fourthline.cling.support.model.PositionInfo;
+import org.fourthline.cling.support.model.Res;
+import org.fourthline.cling.support.model.SeekMode;
+import org.fourthline.cling.support.model.item.Item;
+import org.fourthline.cling.support.model.item.MusicTrack;
+import org.fourthline.cling.support.model.item.VideoItem;
+import org.fourthline.cling.support.renderingcontrol.callback.SetVolume;
+import org.seamless.util.MimeType;
+
+import java.io.File;
+import java.net.URI;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.concurrent.atomic.AtomicLong;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.DLNADevice;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.serverproxy.FileProxy;
+
+public class DLNAController extends RemoteController {
+ private static final String TAG = DLNAController.class.getSimpleName();
+ private static final long STATUS_UPDATE_INTERVAL_SECONDS = 3000L;
+
+ DLNADevice device;
+ ControlPoint controlPoint;
+ SubscriptionCallback callback;
+ boolean supportsSeek = false;
+
+ private FileProxy proxy;
+ String rootLocation = "";
+ boolean error = false;
+
+ final AtomicLong lastUpdate = new AtomicLong();
+ int currentPosition = 0;
+ String currentPlayingURI;
+ boolean running = true;
+ boolean hasDuration = false;
+
+ public DLNAController(DownloadService downloadService, ControlPoint controlPoint, DLNADevice device) {
+ this.downloadService = downloadService;
+ this.controlPoint = controlPoint;
+ this.device = device;
+
+ SharedPreferences prefs = Util.getPreferences(downloadService);
+ rootLocation = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null);
+ }
+
+ @Override
+ public void create(final boolean playing, final int seconds) {
+ downloadService.setPlayerState(PlayerState.PREPARING);
+
+ callback = new SubscriptionCallback(getTransportService(), 600) {
+ @Override
+ protected void failed(GENASubscription genaSubscription, UpnpResponse upnpResponse, Exception e, String msg) {
+ Log.w(TAG, "Register subscription callback failed: " + msg, e);
+ }
+
+ @Override
+ protected void established(GENASubscription genaSubscription) {
+ Action seekAction = genaSubscription.getService().getAction("Seek");
+ if(seekAction != null) {
+ StateVariable seekMode = genaSubscription.getService().getStateVariable("A_ARG_TYPE_SeekMode");
+ for(String allowedValue: seekMode.getTypeDetails().getAllowedValues()) {
+ if("REL_TIME".equals(allowedValue)) {
+ supportsSeek = true;
+ }
+ }
+ }
+
+ startSong(downloadService.getCurrentPlaying(), playing, seconds);
+ }
+
+ @Override
+ protected void ended(GENASubscription genaSubscription, CancelReason cancelReason, UpnpResponse upnpResponse) {
+
+ }
+
+ @Override
+ protected void eventReceived(GENASubscription genaSubscription) {
+ Map<String, StateVariableValue> m = genaSubscription.getCurrentValues();
+ try {
+ LastChange lastChange = new LastChange(new AVTransportLastChangeParser(), m.get("LastChange").toString());
+ if (playing || lastChange.getEventedValue(0, AVTransportVariable.TransportState.class) == null) {
+ return;
+ }
+
+ switch (lastChange.getEventedValue(0, AVTransportVariable.TransportState.class).getValue()) {
+ case PLAYING:
+ downloadService.setPlayerState(PlayerState.STARTED);
+ break;
+ case PAUSED_PLAYBACK:
+ downloadService.setPlayerState(PlayerState.PAUSED);
+ break;
+ case STOPPED:
+ boolean failed = false;
+ for(StateVariableValue val: m.values()) {
+ if(val.toString().indexOf("TransportStatus val=\"ERROR_OCCURRED\"") != -1) {
+ Log.w(TAG, "Failed to load with event: " + val.toString());
+ failed = true;
+ }
+ }
+
+ if(failed) {
+ failedLoad();
+ } else if(downloadService.getPlayerState() == PlayerState.STARTED) {
+ // Played until the end
+ downloadService.setPlayerState(PlayerState.COMPLETED);
+ downloadService.postPlayCleanup();
+ downloadService.onSongCompleted();
+ } else {
+ downloadService.setPlayerState(PlayerState.STOPPED);
+ }
+ break;
+ case TRANSITIONING:
+ downloadService.setPlayerState(PlayerState.PREPARING);
+ break;
+ case NO_MEDIA_PRESENT:
+ downloadService.setPlayerState(PlayerState.IDLE);
+ break;
+ default:
+ }
+ }
+ catch (Exception e) {
+ Log.w(TAG, "Failed to parse UPNP event", e);
+ failedLoad();
+ }
+ }
+
+ @Override
+ protected void eventsMissed(GENASubscription genaSubscription, int i) {
+
+ }
+ };
+ controlPoint.execute(callback);
+ }
+
+ @Override
+ public void start() {
+ if(error) {
+ Log.w(TAG, "Attempting to restart song");
+ startSong(downloadService.getCurrentPlaying(), true, 0);
+ return;
+ }
+
+ controlPoint.execute(new Play(getTransportService()) {
+ @Override
+ public void success(ActionInvocation invocation) {
+ lastUpdate.set(System.currentTimeMillis());
+ downloadService.setPlayerState(PlayerState.STARTED);
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) {
+ Log.w(TAG, "Failed to start playing: " + msg);
+ failedLoad();
+ }
+ });
+ }
+
+ @Override
+ public void stop() {
+ controlPoint.execute(new Pause(getTransportService()) {
+ @Override
+ public void success(ActionInvocation invocation) {
+ int secondsSinceLastUpdate = (int) ((System.currentTimeMillis() - lastUpdate.get()) / 1000L);
+ currentPosition += secondsSinceLastUpdate;
+
+ downloadService.setPlayerState(PlayerState.PAUSED);
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) {
+ Log.w(TAG, "Failed to pause playing: " + msg);
+ }
+ });
+ }
+
+ @Override
+ public void shutdown() {
+ controlPoint.execute(new Stop(getTransportService()) {
+ @Override
+ public void failure(ActionInvocation invocation, org.fourthline.cling.model.message.UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Stop failed: " + defaultMessage);
+ }
+ });
+
+ if(callback != null) {
+ callback.end();
+ callback = null;
+ }
+
+ if(proxy != null) {
+ proxy.stop();
+ proxy = null;
+ }
+
+ running = false;
+ }
+
+ @Override
+ public void updatePlaylist() {
+ if(downloadService.getCurrentPlaying() == null) {
+ startSong(null, false, 0);
+ }
+ }
+
+ @Override
+ public void changePosition(int seconds) {
+ SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss");
+ df.setTimeZone(TimeZone.getTimeZone("UTC"));
+ controlPoint.execute(new Seek(getTransportService(), SeekMode.REL_TIME, df.format(new Date(seconds * 1000))) {
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Seek failed: " + defaultMessage);
+ }
+ });
+ }
+
+ @Override
+ public void changeTrack(int index, DownloadFile song) {
+ startSong(song, true, 0);
+ }
+
+ @Override
+ public void setVolume(int volume) {
+ if(volume < 0) {
+ volume = 0;
+ } else if(volume > device.volumeMax) {
+ volume = device.volumeMax;
+ }
+
+ device.volume = volume;
+ controlPoint.execute(new SetVolume(device.renderer.findService(new ServiceType("schemas-upnp-org", "RenderingControl")), volume) {
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Set volume failed: " + defaultMessage);
+ }
+ });
+ }
+
+ @Override
+ public void updateVolume(boolean up) {
+ int increment = device.volumeMax / 10;
+ setVolume(device.volume + (up ? increment : -increment));
+ }
+
+ @Override
+ public double getVolume() {
+ return device.volume;
+ }
+
+ @Override
+ public int getRemotePosition() {
+ if(downloadService.getPlayerState() == PlayerState.STARTED) {
+ int secondsSinceLastUpdate = (int) ((System.currentTimeMillis() - lastUpdate.get()) / 1000L);
+ return currentPosition + secondsSinceLastUpdate;
+ } else {
+ return currentPosition;
+ }
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return supportsSeek && hasDuration;
+ }
+
+ private void startSong(final DownloadFile currentPlaying, final boolean autoStart, final int position) {
+ if(currentPlaying == null) {
+ downloadService.setPlayerState(PlayerState.IDLE);
+ return;
+ }
+ error = false;
+
+ downloadService.setPlayerState(PlayerState.PREPARING);
+ MusicDirectory.Entry song = currentPlaying.getSong();
+
+ try {
+ // Get url for entry
+ MusicService musicService = MusicServiceFactory.getMusicService(downloadService);
+ String url;
+ // In offline mode or playing offline song
+ if(Util.isOffline(downloadService) || song.getId().indexOf(rootLocation) != -1) {
+ if(proxy == null) {
+ proxy = new FileProxy(downloadService);
+ proxy.start();
+ }
+
+ // Offline song
+ if(song.getId().indexOf(rootLocation) != -1) {
+ url = proxy.getPublicAddress(song.getId());
+ } else {
+ // Playing online song in offline mode
+ url = proxy.getPublicAddress(currentPlaying.getCompleteFile().getPath());
+ }
+ } else {
+ if(proxy != null) {
+ proxy.stop();
+ proxy = null;
+ }
+
+ if(song.isVideo()) {
+ url = musicService.getHlsUrl(song.getId(), currentPlaying.getBitRate(), downloadService);
+ } else {
+ url = musicService.getMusicUrl(downloadService, song, currentPlaying.getBitRate());
+ }
+
+ url = Util.replaceInternalUrl(downloadService, url);
+ }
+
+ // Create metadata for entry
+ Item track;
+ if(song.isVideo()) {
+ track = new VideoItem(song.getId(), song.getParent(), song.getTitle(), song.getArtist());
+ } else {
+ String contentType = null;
+ if(song.getTranscodedContentType() != null) {
+ contentType = song.getTranscodedContentType();
+ } else if(song.getContentType() != null) {
+ contentType = song.getContentType();
+ }
+
+ MimeType mimeType;
+ // If we can parse the content type, use it instead of hard coding
+ if(contentType != null && contentType.indexOf("/") != -1 && contentType.indexOf("/") != (contentType.length() - 1)) {
+ String[] typeParts = contentType.split("/");
+ mimeType = new MimeType(typeParts[0], typeParts[1]);
+ } else {
+ mimeType = new MimeType("audio", "mpeg");
+ }
+
+ Res res = new Res(mimeType, song.getSize(), url);
+
+ if(song.getDuration() != null) {
+ SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss");
+ df.setTimeZone(TimeZone.getTimeZone("UTC"));
+ res.setDuration(df.format(new Date(song.getDuration() * 1000)));
+ }
+
+ MusicTrack musicTrack = new MusicTrack(song.getId(), song.getParent(), song.getTitle(), song.getArtist(), song.getAlbum(), song.getArtist(), res);
+ musicTrack.setOriginalTrackNumber(song.getTrack());
+
+ if(song.getCoverArt() != null) {
+ String coverArt = null;
+ if(proxy == null) {
+ coverArt = musicService.getCoverArtUrl(downloadService, song);
+ coverArt = Util.replaceInternalUrl(downloadService, coverArt);
+ } else {
+ File coverArtFile = FileUtil.getAlbumArtFile(downloadService, song);
+ if(coverArtFile != null && coverArtFile.exists()) {
+ coverArt = proxy.getPublicAddress(coverArtFile.getPath());
+ }
+ }
+
+ if(coverArt != null) {
+ DIDLObject.Property.UPNP.ALBUM_ART_URI albumArtUri = new DIDLObject.Property.UPNP.ALBUM_ART_URI(URI.create(coverArt));
+ musicTrack.addProperty(albumArtUri);
+ }
+ }
+
+ track = musicTrack;
+ }
+
+ DIDLParser parser = new DIDLParser();
+ DIDLContent didl = new DIDLContent();
+ didl.addItem(track);
+
+ String metadata = "";
+ try {
+ metadata = parser.generate(didl);
+ } catch(Exception e) {
+ Log.w(TAG, "Metadata generation failed", e);
+ }
+
+ currentPlayingURI = url;
+ controlPoint.execute(new SetAVTransportURI(getTransportService(), url, metadata) {
+ @Override
+ public void success(ActionInvocation invocation) {
+ if(position != 0) {
+ changePosition(position);
+ }
+
+ if (autoStart) {
+ start();
+ } else {
+ downloadService.setPlayerState(PlayerState.PAUSED);
+ }
+
+ currentPosition = position;
+ lastUpdate.set(System.currentTimeMillis());
+ getUpdatedStatus();
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) {
+ Log.w(TAG, "Set URI failed: " + msg);
+ failedLoad();
+ }
+ });
+ } catch (Exception e) {
+ Log.w(TAG, "Failed startSong", e);
+ failedLoad();
+ }
+ }
+
+ private void failedLoad() {
+ downloadService.setPlayerState(PlayerState.STOPPED);
+ error = true;
+
+ if(Looper.myLooper() != Looper.getMainLooper()) {
+ downloadService.post(new Runnable() {
+ @Override
+ public void run() {
+ Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load));
+ }
+ });
+ } else {
+ Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load));
+ }
+ }
+
+ private Service getTransportService() {
+ return device.renderer.findService(new ServiceType("schemas-upnp-org", "AVTransport"));
+ }
+
+ private void getUpdatedStatus() {
+ // Don't care if shutdown in the meantime
+ if(!running) {
+ return;
+ }
+
+ controlPoint.execute(new GetPositionInfo(getTransportService()) {
+ @Override
+ public void received(ActionInvocation actionInvocation, PositionInfo positionInfo) {
+ // Don't care if shutdown in the meantime
+ if(!running) {
+ return;
+ }
+
+ long duration = positionInfo.getTrackDurationSeconds();
+ hasDuration = duration > 0;
+
+ lastUpdate.set(System.currentTimeMillis());
+
+ // Let's get the updated position
+ currentPosition = (int) positionInfo.getTrackElapsedSeconds();
+
+ downloadService.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ getUpdatedStatus();
+ }
+ }, STATUS_UPDATE_INTERVAL_SECONDS);
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String s) {
+ Log.w(TAG, "Failed to get an update");
+
+ downloadService.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ getUpdatedStatus();
+ }
+ }, STATUS_UPDATE_INTERVAL_SECONDS);
+ }
+ });
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/DownloadService.java b/src/github/daneren2005/dsub/service/DownloadService.java
index 5732ec83..473815dc 100644
--- a/src/github/daneren2005/dsub/service/DownloadService.java
+++ b/src/github/daneren2005/dsub/service/DownloadService.java
@@ -54,7 +54,6 @@ import github.daneren2005.dsub.view.UpdateView;
import github.daneren2005.serverproxy.BufferProxy;
import java.io.File;
-import java.io.IOError;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
@@ -63,6 +62,7 @@ import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
+import android.annotation.TargetApi;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
@@ -71,7 +71,6 @@ import android.content.SharedPreferences;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.audiofx.AudioEffect;
-import android.media.audiofx.Equalizer;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
@@ -301,6 +300,13 @@ public class DownloadService extends Service {
public IBinder onBind(Intent intent) {
return binder;
}
+
+ public void post(Runnable r) {
+ handler.post(r);
+ }
+ public void postDelayed(Runnable r, long millis) {
+ handler.postDelayed(r, millis);
+ }
public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle) {
download(songs, save, autoplay, playNext, shuffle, 0, 0);
@@ -401,17 +407,17 @@ public class DownloadService extends Service {
}
private void updateJukeboxPlaylist() {
- if (remoteState != LOCAL) {
+ if (remoteState != LOCAL && remoteController != null) {
remoteController.updatePlaylist();
}
}
public synchronized void restore(List<MusicDirectory.Entry> songs, List<MusicDirectory.Entry> toDelete, int currentPlayingIndex, int currentPlayingPosition) {
SharedPreferences prefs = Util.getPreferences(this);
- remoteState = RemoteControlState.values()[prefs.getInt(Constants.PREFERENCES_KEY_CONTROL_MODE, 0)];
- if(remoteState != LOCAL) {
+ RemoteControlState newState = RemoteControlState.values()[prefs.getInt(Constants.PREFERENCES_KEY_CONTROL_MODE, 0)];
+ if(newState != LOCAL) {
String id = prefs.getString(Constants.PREFERENCES_KEY_CONTROL_ID, null);
- setRemoteState(remoteState, null, id);
+ setRemoteState(newState, null, id);
}
if(prefs.getBoolean(Constants.PREFERENCES_KEY_REMOVE_PLAYED, false)) {
removePlayed = true;
@@ -574,9 +580,9 @@ public class DownloadService extends Service {
public void setOnline(final boolean online) {
if(online) {
- mediaRouter.addOfflineProviders();
+ mediaRouter.addOnlineProviders();
} else {
- mediaRouter.removeOfflineProviders();
+ mediaRouter.removeOnlineProviders();
}
if(shufflePlay) {
setShufflePlayEnabled(false);
@@ -702,6 +708,7 @@ public class DownloadService extends Service {
this.currentPlaying = currentPlaying;
if(currentPlaying == null) {
currentPlayingIndex = -1;
+ setPlayerState(IDLE);
} else {
currentPlayingIndex = downloadList.indexOf(currentPlaying);
}
@@ -866,7 +873,7 @@ public class DownloadService extends Service {
nextMediaPlayer = tmp;
setCurrentPlaying(nextPlaying, true);
setPlayerState(PlayerState.STARTED);
- setupHandlers(currentPlaying, false);
+ setupHandlers(currentPlaying, false, start);
setNextPlaying();
// Proxy should not be being used here since the next player was already setup to play
@@ -1046,6 +1053,7 @@ public class DownloadService extends Service {
}
}
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public synchronized void reset() {
if (bufferTask != null) {
bufferTask.cancel();
@@ -1069,6 +1077,7 @@ public class DownloadService extends Service {
}
}
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public synchronized void resetNext() {
try {
if (nextMediaPlayer != null) {
@@ -1302,6 +1311,16 @@ public class DownloadService extends Service {
return mediaRouter.getSelector();
}
+ public boolean isSeekable() {
+ if(remoteState == LOCAL) {
+ return currentPlaying != null && currentPlaying.isWorkDone() && playerState != PREPARING;
+ } else if(remoteController != null) {
+ return remoteController.isSeekable();
+ } else {
+ return false;
+ }
+ }
+
public boolean isRemoteEnabled() {
return remoteState != LOCAL;
}
@@ -1330,6 +1349,11 @@ public class DownloadService extends Service {
setRemoteState(newState, ref, null);
}
private void setRemoteState(final RemoteControlState newState, final Object ref, final String routeId) {
+ // Don't try to do anything if already in the correct state
+ if(remoteState == newState) {
+ return;
+ }
+
boolean isPlaying = playerState == STARTED;
int position = getPlayerPosition();
@@ -1344,12 +1368,13 @@ public class DownloadService extends Service {
}
}
+ Log.i(TAG, remoteState.name() + " => " + newState.name() + " (" + currentPlaying + ")");
remoteState = newState;
switch(newState) {
case JUKEBOX_SERVER:
remoteController = new JukeboxController(this, handler);
break;
- case CHROMECAST:
+ case CHROMECAST: case DLNA:
if(ref == null) {
remoteState = LOCAL;
break;
@@ -1470,6 +1495,7 @@ public class DownloadService extends Service {
subtractPosition = 0;
mediaPlayer.setOnCompletionListener(null);
+ mediaPlayer.setOnPreparedListener(null);
mediaPlayer.reset();
setPlayerState(IDLE);
try {
@@ -1537,7 +1563,7 @@ public class DownloadService extends Service {
}
});
- setupHandlers(downloadFile, isPartial);
+ setupHandlers(downloadFile, isPartial, start);
mediaPlayer.prepareAsync();
} catch (Exception x) {
@@ -1545,6 +1571,7 @@ public class DownloadService extends Service {
}
}
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private synchronized void setupNext(final DownloadFile downloadFile) {
try {
final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile();
@@ -1595,7 +1622,7 @@ public class DownloadService extends Service {
}
}
- private void setupHandlers(final DownloadFile downloadFile, final boolean isPartial) {
+ private void setupHandlers(final DownloadFile downloadFile, final boolean isPartial, final boolean isPlaying) {
final int duration = downloadFile.getSong().getDuration() == null ? 0 : downloadFile.getSong().getDuration() * 1000;
mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
public boolean onError(MediaPlayer mediaPlayer, int what, int extra) {
@@ -1606,7 +1633,7 @@ public class DownloadService extends Service {
playNext();
} else {
downloadFile.setPlaying(false);
- doPlay(downloadFile, pos, true);
+ doPlay(downloadFile, pos, isPlaying);
downloadFile.setPlaying(true);
}
return true;
@@ -1627,12 +1654,7 @@ public class DownloadService extends Service {
Log.i(TAG, "Ending position " + pos + " of " + duration);
if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 10000)) || nextSetup) {
playNext();
-
- // Finished loading, delete when list is cleared
- if (downloadFile.getSong() instanceof PodcastEpisode) {
- toDelete.add(downloadFile);
- }
- clearCurrentBookmark(downloadFile.getSong(), true);
+ postPlayCleanup(downloadFile);
} else {
// If file is not completely downloaded, restart the playback from the current position.
synchronized (DownloadService.this) {
@@ -1914,6 +1936,17 @@ public class DownloadService extends Service {
}
}
}
+
+ public void postPlayCleanup() {
+ postPlayCleanup(currentPlaying);
+ }
+ public void postPlayCleanup(DownloadFile downloadFile) {
+ // Finished loading, delete when list is cleared
+ if (downloadFile.getSong() instanceof PodcastEpisode) {
+ toDelete.add(downloadFile);
+ }
+ clearCurrentBookmark(downloadFile.getSong(), true);
+ }
private boolean isPastCutoff() {
return isPastCutoff(getPlayerPosition(), getPlayerDuration());
diff --git a/src/github/daneren2005/dsub/service/RemoteController.java b/src/github/daneren2005/dsub/service/RemoteController.java
index 89d4f4fd..02deaf85 100644
--- a/src/github/daneren2005/dsub/service/RemoteController.java
+++ b/src/github/daneren2005/dsub/service/RemoteController.java
@@ -48,6 +48,9 @@ public abstract class RemoteController {
public abstract void setVolume(int volume);
public abstract void updateVolume(boolean up);
public abstract double getVolume();
+ public boolean isSeekable() {
+ return true;
+ }
public abstract int getRemotePosition();
public int getRemoteDuration() {
diff --git a/src/github/daneren2005/dsub/util/MediaRouteManager.java b/src/github/daneren2005/dsub/util/MediaRouteManager.java
index 347b0376..9aa54c4b 100644
--- a/src/github/daneren2005/dsub/util/MediaRouteManager.java
+++ b/src/github/daneren2005/dsub/util/MediaRouteManager.java
@@ -15,6 +15,7 @@
package github.daneren2005.dsub.util;
+import android.os.Build;
import android.support.v7.media.MediaRouteProvider;
import android.support.v7.media.MediaRouteSelector;
import android.support.v7.media.MediaRouter;
@@ -27,6 +28,7 @@ import java.util.ArrayList;
import java.util.List;
import github.daneren2005.dsub.domain.RemoteControlState;
+import github.daneren2005.dsub.provider.DLNARouteProvider;
import github.daneren2005.dsub.provider.JukeboxRouteProvider;
import github.daneren2005.dsub.service.DownloadService;
import github.daneren2005.dsub.service.RemoteController;
@@ -45,7 +47,7 @@ public class MediaRouteManager extends MediaRouter.Callback {
private MediaRouter router;
private MediaRouteSelector selector;
private List<MediaRouteProvider> providers = new ArrayList<MediaRouteProvider>();
- private List<MediaRouteProvider> offlineProviders = new ArrayList<MediaRouteProvider>();
+ private List<MediaRouteProvider> onlineProviders = new ArrayList<MediaRouteProvider>();
static {
try {
@@ -140,21 +142,27 @@ public class MediaRouteManager extends MediaRouter.Callback {
}
}
- public void addOfflineProviders() {
+ public void addOnlineProviders() {
JukeboxRouteProvider jukeboxProvider = new JukeboxRouteProvider(downloadService);
router.addProvider(jukeboxProvider);
providers.add(jukeboxProvider);
- offlineProviders.add(jukeboxProvider);
+ onlineProviders.add(jukeboxProvider);
}
- public void removeOfflineProviders() {
- for(MediaRouteProvider provider: offlineProviders) {
+ public void removeOnlineProviders() {
+ for(MediaRouteProvider provider: onlineProviders) {
router.removeProvider(provider);
}
}
private void addProviders() {
if(!Util.isOffline(downloadService)) {
- addOfflineProviders();
+ addOnlineProviders();
+ }
+
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ DLNARouteProvider dlnaProvider = new DLNARouteProvider(downloadService);
+ router.addProvider(dlnaProvider);
+ providers.add(dlnaProvider);
}
}
public void buildSelector() {
@@ -165,6 +173,9 @@ public class MediaRouteManager extends MediaRouter.Callback {
if(castAvailable) {
builder.addControlCategory(CastCompat.getCastControlCategory());
}
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ builder.addControlCategory(DLNARouteProvider.CATEGORY_DLNA);
+ }
selector = builder.build();
}
}
diff --git a/src/github/daneren2005/dsub/util/Util.java b/src/github/daneren2005/dsub/util/Util.java
index f899b9e4..83bedfc8 100644
--- a/src/github/daneren2005/dsub/util/Util.java
+++ b/src/github/daneren2005/dsub/util/Util.java
@@ -357,6 +357,22 @@ public final class Util {
return builder.toString();
}
+ public static String replaceInternalUrl(Context context, String url) {
+ // Only change to internal when using https
+ if(url.indexOf("https") != -1) {
+ SharedPreferences prefs = Util.getPreferences(context);
+ int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ String internalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance, null);
+ if(internalUrl != null && !"".equals(internalUrl)) {
+ String externalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
+ url = url.replace(internalUrl, externalUrl);
+ }
+ }
+
+ // Use separate profile for Chromecast so users can do ogg on phone, mp3 for CC
+ return url.replace(Constants.REST_CLIENT_ID, Constants.CHROMECAST_CLIENT_ID);
+ }
+
public static boolean isTagBrowsing(Context context) {
return isTagBrowsing(context, Util.getActiveServer(context));
}