/*
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 List<String> removing = new ArrayList<String>();
private AndroidUpnpService dlnaService;
private ServiceConnection dlnaServiceConnection;
private RegistryListener registryListener;
private boolean searchOnConnect = false;
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(registryListener = new RegistryListener() {
@Override
public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice remoteDevice) {
}
@Override
public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice remoteDevice, Exception e) {
// Error is displayed in log anyways under W/trieveRemoteDescriptors
}
@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);
}
if(searchOnConnect) {
dlnaService.getControlPoint().search();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
dlnaService = null;
registryListener = 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 volume;
if(device.volumeMax <= 0) {
volume = 5;
} else {
int increments = (int) Math.ceil(device.volumeMax / 10.0);
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()) {
if(dlnaService != null) {
dlnaService.getControlPoint().search();
} else {
searchOnConnect = true;
}
}
}
@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;
}
// Just a temp disconnect, already have it's info
if(removing.contains(id)) {
removing.remove(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) {
final String id = device.getIdentity().getUdn().toString();
removing.add(id);
// Delay removal for a few seconds to make sure that it isn't just a temp disconnect
dlnaService.getControlPoint().search();
downloadService.postDelayed(new Runnable() {
@Override
public void run() {
if(removing.contains(id)) {
devices.remove(id);
removing.remove(id);
broadcastDescriptors();
}
}
}, 5000L);
}
}
public void destroy() {
if(dlnaService != null) {
dlnaService.getRegistry().removeListener(registryListener);
registryListener = null;
}
if(dlnaServiceConnection != null) {
getContext().getApplicationContext().unbindService(dlnaServiceConnection);
}
}
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) {
}
}
}