diff options
author | Scott Jackson <daneren2005@gmail.com> | 2015-01-24 17:59:40 -0800 |
---|---|---|
committer | Scott Jackson <daneren2005@gmail.com> | 2015-01-24 17:59:40 -0800 |
commit | fb4aa30c2eda131d94f38f991f27f955eff6e7b1 (patch) | |
tree | 32e5980a7b05bbe76d3c2da29d78ea6467ccfef7 | |
parent | 85846d4c01d30e9600e60308eda62f71f470afeb (diff) | |
parent | cde390b7ed975c620c4aa3cb3336214313971a7d (diff) | |
download | dsub-fb4aa30c2eda131d94f38f991f27f955eff6e7b1.tar.gz dsub-fb4aa30c2eda131d94f38f991f27f955eff6e7b1.tar.bz2 dsub-fb4aa30c2eda131d94f38f991f27f955eff6e7b1.zip |
Merge branch 'DLNA'
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 Binary files differnew file mode 100644 index 00000000..632d3038 --- /dev/null +++ b/libs/cling-core-2.0.1.jar diff --git a/libs/cling-support-2.0.1.jar b/libs/cling-support-2.0.1.jar Binary files differnew file mode 100644 index 00000000..7fa28604 --- /dev/null +++ b/libs/cling-support-2.0.1.jar diff --git a/libs/javax.servlet-3.0.0.v201112011016.jar b/libs/javax.servlet-3.0.0.v201112011016.jar Binary files differnew file mode 100644 index 00000000..b1354096 --- /dev/null +++ b/libs/javax.servlet-3.0.0.v201112011016.jar diff --git a/libs/jetty-all-8.1.16.v20140903.jar b/libs/jetty-all-8.1.16.v20140903.jar Binary files differnew file mode 100644 index 00000000..25b1d324 --- /dev/null +++ b/libs/jetty-all-8.1.16.v20140903.jar diff --git a/libs/seamless-http-1.1.0.jar b/libs/seamless-http-1.1.0.jar Binary files differnew file mode 100644 index 00000000..98ec884a --- /dev/null +++ b/libs/seamless-http-1.1.0.jar diff --git a/libs/seamless-util-1.1.0.jar b/libs/seamless-util-1.1.0.jar Binary files differnew file mode 100644 index 00000000..12026b7f --- /dev/null +++ b/libs/seamless-util-1.1.0.jar diff --git a/libs/seamless-xml-1.1.0.jar b/libs/seamless-xml-1.1.0.jar Binary files differnew file mode 100644 index 00000000..1e740877 --- /dev/null +++ b/libs/seamless-xml-1.1.0.jar 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)); } |