/*
* DLNAService
* Connect SDK
*
* Copyright (c) 2014 LG Electronics.
* Created by Hyun Kook Khang on 19 Jan 2014
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.connectsdk.service;
import android.content.Context;
import android.text.Html;
import android.util.Log;
import android.util.Xml;
import com.connectsdk.core.ImageInfo;
import com.connectsdk.core.MediaInfo;
import com.connectsdk.core.SubtitleInfo;
import com.connectsdk.core.Util;
import com.connectsdk.discovery.DiscoveryFilter;
import com.connectsdk.discovery.DiscoveryManager;
import com.connectsdk.discovery.provider.ssdp.Service;
import com.connectsdk.etc.helper.DeviceServiceReachability;
import com.connectsdk.etc.helper.HttpConnection;
import com.connectsdk.service.capability.CapabilityMethods;
import com.connectsdk.service.capability.MediaControl;
import com.connectsdk.service.capability.MediaPlayer;
import com.connectsdk.service.capability.PlaylistControl;
import com.connectsdk.service.capability.VolumeControl;
import com.connectsdk.service.capability.listeners.ResponseListener;
import com.connectsdk.service.command.ServiceCommand;
import com.connectsdk.service.command.ServiceCommandError;
import com.connectsdk.service.command.ServiceSubscription;
import com.connectsdk.service.command.URLServiceSubscription;
import com.connectsdk.service.config.ServiceConfig;
import com.connectsdk.service.config.ServiceDescription;
import com.connectsdk.service.sessions.LaunchSession;
import com.connectsdk.service.sessions.LaunchSession.LaunchSessionType;
import com.connectsdk.service.upnp.DLNAHttpServer;
import com.connectsdk.service.upnp.DLNAMediaInfoParser;
import org.json.JSONException;
import org.json.JSONObject;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xmlpull.v1.XmlPullParser;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
public class DLNAService extends DeviceService implements PlaylistControl, MediaControl, MediaPlayer, VolumeControl {
public static final String ID = "DLNA";
protected static final String SUBSCRIBE = "SUBSCRIBE";
protected static final String UNSUBSCRIBE = "UNSUBSCRIBE";
public static final String AV_TRANSPORT_URN = "urn:schemas-upnp-org:service:AVTransport:1";
public static final String CONNECTION_MANAGER_URN = "urn:schemas-upnp-org:service:ConnectionManager:1";
public static final String RENDERING_CONTROL_URN = "urn:schemas-upnp-org:service:RenderingControl:1";
protected static final String AV_TRANSPORT = "AVTransport";
protected static final String CONNECTION_MANAGER = "ConnectionManager";
protected static final String RENDERING_CONTROL = "RenderingControl";
protected static final String GROUP_RENDERING_CONTROL = "GroupRenderingControl";
public static final String PLAY_STATE = "playState";
public static final String DEFAULT_SUBTITLE_MIMETYPE = "text/srt";
public static final String DEFAULT_SUBTITLE_TYPE = "srt";
Context context;
String avTransportURL, renderingControlURL, connectionControlURL;
DLNAHttpServer httpServer;
Map<String, String> SIDList;
Timer resubscriptionTimer;
private static int TIMEOUT = 300;
interface PositionInfoListener {
public void onGetPositionInfoSuccess(String positionInfoXml);
public void onGetPositionInfoFailed(ServiceCommandError error);
}
public DLNAService(ServiceDescription serviceDescription, ServiceConfig serviceConfig) {
this(serviceDescription, serviceConfig, DiscoveryManager.getInstance().getContext(), new DLNAHttpServer());
}
public DLNAService(ServiceDescription serviceDescription, ServiceConfig serviceConfig, Context context, DLNAHttpServer dlnaServer) {
super(serviceDescription, serviceConfig);
this.context = context;
SIDList = new HashMap<String, String>();
updateControlURL();
httpServer = dlnaServer;
}
public static DiscoveryFilter discoveryFilter() {
return new DiscoveryFilter(ID, "urn:schemas-upnp-org:device:MediaRenderer:1");
}
@Override
public CapabilityPriorityLevel getPriorityLevel(Class<? extends CapabilityMethods> clazz) {
if (clazz.equals(MediaPlayer.class)) {
return getMediaPlayerCapabilityLevel();
}
else if (clazz.equals(MediaControl.class)) {
return getMediaControlCapabilityLevel();
}
else if (clazz.equals(VolumeControl.class)) {
return getVolumeControlCapabilityLevel();
}
else if (clazz.equals(PlaylistControl.class)) {
return getPlaylistControlCapabilityLevel();
}
return CapabilityPriorityLevel.NOT_SUPPORTED;
}
@Override
public void setServiceDescription(ServiceDescription serviceDescription) {
super.setServiceDescription(serviceDescription);
updateControlURL();
}
private void updateControlURL() {
List<Service> serviceList = serviceDescription.getServiceList();
if (serviceList != null) {
for (int i = 0; i < serviceList.size(); i++) {
if(!serviceList.get(i).baseURL.endsWith("/")) {
serviceList.get(i).baseURL += "/";
}
if (serviceList.get(i).serviceType.contains(AV_TRANSPORT)) {
avTransportURL = makeControlURL(serviceList.get(i).baseURL, serviceList.get(i).controlURL);
}
else if ((serviceList.get(i).serviceType.contains(RENDERING_CONTROL)) && !(serviceList.get(i).serviceType.contains(GROUP_RENDERING_CONTROL))) {
renderingControlURL = makeControlURL(serviceList.get(i).baseURL, serviceList.get(i).controlURL);
}
else if ((serviceList.get(i).serviceType.contains(CONNECTION_MANAGER)) ) {
connectionControlURL = makeControlURL(serviceList.get(i).baseURL, serviceList.get(i).controlURL);
}
}
}
}
String makeControlURL(String base, String path) {
if (base == null || path == null) {
return null;
}
if (path.startsWith("/")) {
return base + path.substring(1);
}
return base + path;
}
/******************
MEDIA PLAYER
*****************/
@Override
public MediaPlayer getMediaPlayer() {
return this;
}
@Override
public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() {
return CapabilityPriorityLevel.NORMAL;
}
@Override
public void getMediaInfo(final MediaInfoListener listener) {
getPositionInfo(new PositionInfoListener() {
@Override
public void onGetPositionInfoSuccess(final String positionInfoXml) {
Util.runInBackground(new Runnable() {
@Override
public void run() {
String baseUrl = "http://" + getServiceDescription().getIpAddress() + ":" + getServiceDescription().getPort();
String trackMetaData = parseData(positionInfoXml, "TrackMetaData");
MediaInfo info = DLNAMediaInfoParser.getMediaInfo(trackMetaData, baseUrl);
Util.postSuccess(listener, info);
}
});
}
@Override
public void onGetPositionInfoFailed(ServiceCommandError error) {
Util.postError(listener, error);
}
});
}
@Override
public ServiceSubscription<MediaInfoListener> subscribeMediaInfo(MediaInfoListener listener) {
URLServiceSubscription<MediaInfoListener> request = new URLServiceSubscription<MediaInfoListener>(this, "info", null, null);
request.addListener(listener);
addSubscription(request);
return request;
}
@Deprecated
public void displayMedia(String url, String mimeType, String title, String description, String iconSrc, final LaunchListener listener) {
displayMedia(url, null, mimeType, title, description, iconSrc, listener);
}
private void displayMedia(String url, SubtitleInfo subtitle, String mimeType, String title, String description, String iconSrc, final LaunchListener listener) {
final String instanceId = "0";
String[] mediaElements = mimeType.split("/");
String mediaType = mediaElements[0];
String mediaFormat = mediaElements[1];
if (mediaType == null || mediaType.length() == 0 || mediaFormat == null || mediaFormat.length() == 0) {
Util.postError(listener, new ServiceCommandError(0, "You must provide a valid mimeType (audio/*, video/*, etc)", null));
return;
}
mediaFormat = "mp3".equals(mediaFormat) ? "mpeg" : mediaFormat;
String mMimeType = String.format("%s/%s", mediaType, mediaFormat);
ResponseListener<Object> responseListener = new ResponseListener<Object>() {
@Override
public void onSuccess(Object response) {
String method = "Play";
Map<String, String> parameters = new HashMap<String, String>();
parameters.put("Speed", "1");
String payload = getMessageXml(AV_TRANSPORT_URN, method, "0", parameters);
ResponseListener<Object> playResponseListener = new ResponseListener<Object> () {
@Override
public void onSuccess(Object response) {
LaunchSession launchSession = new LaunchSession();
launchSession.setService(DLNAService.this);
launchSession.setSessionType(LaunchSessionType.Media);
Util.postSuccess(listener, new MediaLaunchObject(launchSession, DLNAService.this, DLNAService.this));
}
@Override
public void onError(ServiceCommandError error) {
Util.postError(listener, error);
}
};
ServiceCommand<ResponseListener<Object>> request = new ServiceCommand<ResponseListener<Object>>(DLNAService.this, method, payload, playResponseListener);
request.send();
}
@Override
public void onError(ServiceCommandError error) {
Util.postError(listener, error);
}
};
String method = "SetAVTransportURI";
String metadata = getMetadata(url, subtitle, mMimeType, title, description, iconSrc);
if (metadata == null) {
Util.postError(listener, ServiceCommandError.getError(500));
return;
}
Map<String, String> params = new LinkedHashMap<String, String>();
try {
params.put("CurrentURI", encodeURL(url));
} catch (Exception e) {
Util.postError(listener, ServiceCommandError.getError(500));
return;
}
params.put("CurrentURIMetaData", metadata);
String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, params);
ServiceCommand<ResponseListener<Object>> request = new ServiceCommand<ResponseListener<Object>>(DLNAService.this, method, payload, responseListener);
request.send();
}
@Override
public void displayImage(String url, String mimeType, String title, String description, String iconSrc, LaunchListener listener) {
displayMedia(url, null, mimeType, title, description, iconSrc, listener);
}
@Override
public void displayImage(MediaInfo mediaInfo, LaunchListener listener) {
String mediaUrl = null;
String mimeType = null;
String title = null;
String desc = null;
String iconSrc = null;
if (mediaInfo != null) {
mediaUrl = mediaInfo.getUrl();
mimeType = mediaInfo.getMimeType();
title = mediaInfo.getTitle();
desc = mediaInfo.getDescription();
if (mediaInfo.getImages() != null && mediaInfo.getImages().size() > 0) {
ImageInfo imageInfo = mediaInfo.getImages().get(0);
iconSrc = imageInfo.getUrl();
}
}
displayImage(mediaUrl, mimeType, title, desc, iconSrc, listener);
}
@Override
public void playMedia(String url, String mimeType, String title, String description, String iconSrc, boolean shouldLoop, LaunchListener listener) {
displayMedia(url, null, mimeType, title, description, iconSrc, listener);
}
@Override
public void playMedia(MediaInfo mediaInfo, boolean shouldLoop,
LaunchListener listener) {
String mediaUrl = null;
SubtitleInfo subtitle = null;
String mimeType = null;
String title = null;
String desc = null;
String iconSrc = null;
if (mediaInfo != null) {
mediaUrl = mediaInfo.getUrl();
subtitle = mediaInfo.getSubtitleInfo();
mimeType = mediaInfo.getMimeType();
title = mediaInfo.getTitle();
desc = mediaInfo.getDescription();
if (mediaInfo.getImages() != null && mediaInfo.getImages().size() > 0) {
ImageInfo imageInfo = mediaInfo.getImages().get(0);
iconSrc = imageInfo.getUrl();
}
}
displayMedia(mediaUrl, subtitle, mimeType, title, desc, iconSrc, listener);
}
@Override
public void closeMedia(LaunchSession launchSession, ResponseListener<Object> listener) {
if (launchSession.getService() instanceof DLNAService)
((DLNAService) launchSession.getService()).stop(listener);
}
/******************
MEDIA CONTROL
*****************/
@Override
public MediaControl getMediaControl() {
return this;
}
@Override
public CapabilityPriorityLevel getMediaControlCapabilityLevel() {
return CapabilityPriorityLevel.NORMAL;
}
@Override
public void play(ResponseListener<Object> listener) {
String method = "Play";
String instanceId = "0";
Map<String, String> parameters = new LinkedHashMap<String, String>();
parameters.put("Speed", "1");
String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, parameters);
ServiceCommand<ResponseListener<Object>> request = new ServiceCommand<ResponseListener<Object>>(this, method, payload, listener);
request.send();
}
@Override
public void pause(ResponseListener<Object> listener) {
String method = "Pause";
String instanceId = "0";
String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, null);
ServiceCommand<ResponseListener<Object>> request = new ServiceCommand<ResponseListener<Object>>(this, method, payload, listener);
request.send();
}
@Override
public void stop(ResponseListener<Object> listener) {
String method = "Stop";
String instanceId = "0";
String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, null);
ServiceCommand<ResponseListener<Object>> request = new ServiceCommand<ResponseListener<Object>>(this, method, payload, listener);
request.send();
}
@Override
public void rewind(ResponseListener<Object> listener) {
Util.postError(listener, ServiceCommandError.notSupported());
}
@Override
public void fastForward(ResponseListener<Object> listener) {
Util.postError(listener, ServiceCommandError.notSupported());
}
/******************
PLAYLIST CONTROL
*****************/
@Override
public PlaylistControl getPlaylistControl() {
return this;
}
@Override
public CapabilityPriorityLevel getPlaylistControlCapabilityLevel() {
return CapabilityPriorityLevel.NORMAL;
}
@Override
public void previous(ResponseListener<Object> listener) {
String method = "Previous";
String instanceId = "0";
String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, null);
ServiceCommand<ResponseListener<Object>> request = new ServiceCommand<ResponseListener<Object>>(this, method, payload, listener);
request.send();
}
@Override
public void next(ResponseListener<Object> listener) {
String method = "Next";
String instanceId = "0";
String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, null);
ServiceCommand<ResponseListener<Object>> request = new ServiceCommand<ResponseListener<Object>>(this, method, payload, listener);
request.send();
}
@Override
public void jumpToTrack(long index, ResponseListener<Object> listener) {
// DLNA requires start index from 1. 0 is a special index which means the end of media.
++index;
seek("TRACK_NR", Long.toString(index), listener);
}
@Override
public void setPlayMode(PlayMode playMode, ResponseListener<Object> listener) {
String method = "SetPlayMode";
String instanceId = "0";
String mode;
switch (playMode) {
case RepeatAll:
mode = "REPEAT_ALL";
break;
case RepeatOne:
mode = "REPEAT_ONE";
break;
case Shuffle:
mode = "SHUFFLE";
break;
default:
mode = "NORMAL";
}
Map<String, String> parameters = new LinkedHashMap<String, String>();
parameters.put("NewPlayMode", mode);
String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, parameters);
ServiceCommand<ResponseListener<Object>> request = new ServiceCommand<ResponseListener<Object>>(this, method, payload, listener);
request.send();
}
@Override
public void seek(long position, ResponseListener<Object> listener) {
long second = (position / 1000) % 60;
long minute = (position / (1000 * 60)) % 60;
long hour = (position / (1000 * 60 * 60)) % 24;
String time = String.format(Locale.US, "%02d:%02d:%02d", hour, minute, second);
seek("REL_TIME", time, listener);
}
private void getPositionInfo(final PositionInfoListener listener) {
String method = "GetPositionInfo";
String instanceId = "0";
String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, null);
ResponseListener<Object> responseListener = new ResponseListener<Object>() {
@Override
public void onSuccess(Object response) {
if (listener != null) {
listener.onGetPositionInfoSuccess((String)response);
}
}
@Override
public void onError(ServiceCommandError error) {
if (listener != null) {
listener.onGetPositionInfoFailed(error);
}
}
};
ServiceCommand<ResponseListener<Object>> request = new ServiceCommand<ResponseListener<Object>>(this, method, payload, responseListener);
request.send();
}
@Override
public void getDuration(final DurationListener listener) {
getPositionInfo(new PositionInfoListener() {
@Override
public void onGetPositionInfoSuccess(String positionInfoXml) {
String strDuration = parseData(positionInfoXml, "TrackDuration");
String trackMetaData = parseData(positionInfoXml, "TrackMetaData");
MediaInfo info = DLNAMediaInfoParser.getMediaInfo(trackMetaData);
// Check if duration we get not equals 0 or media is image, otherwise wait 1 second and try again
if ((!strDuration.equals("0:00:00")) || (info.getMimeType().contains("image"))) {
long milliTimes = convertStrTimeFormatToLong(strDuration);
Util.postSuccess(listener, milliTimes);
} else new Timer().schedule(new TimerTask() {
@Override
public void run() {
getDuration(listener);
}
}, 1000);
}
@Override
public void onGetPositionInfoFailed(ServiceCommandError error) {
Util.postError(listener, error);
}
});
}
@Override
public void getPosition(final PositionListener listener) {
getPositionInfo(new PositionInfoListener() {
@Override
public void onGetPositionInfoSuccess(String positionInfoXml) {
String strDuration = parseData(positionInfoXml, "RelTime");
long milliTimes = convertStrTimeFormatToLong(strDuration);
Util.postSuccess(listener, milliTimes);
}
@Override
public void onGetPositionInfoFailed(ServiceCommandError error) {
Util.postError(listener, error);
}
});
}
protected void seek(String unit, String target, ResponseListener<Object> listener) {
String method = "Seek";
String instanceId = "0";
Map<String, String> parameters = new LinkedHashMap<String, String>();
parameters.put("Unit", unit);
parameters.put("Target", target);
String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, parameters);
ServiceCommand<ResponseListener<Object>> request = new ServiceCommand<ResponseListener<Object>>(this, method, payload, listener);
request.send();
}
protected String getMessageXml(String serviceURN, String method, String instanceId, Map<String, String> params) {
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.newDocument();
doc.setXmlStandalone(true);
doc.setXmlVersion("1.0");
Element root = doc.createElement("s:Envelope");
Element bodyElement = doc.createElement("s:Body");
Element methodElement = doc.createElementNS(serviceURN, "u:" + method);
Element instanceElement = doc.createElement("InstanceID");
root.setAttribute("s:encodingStyle", "http://schemas.xmlsoap.org/soap/encoding/");
root.setAttribute("xmlns:s", "http://schemas.xmlsoap.org/soap/envelope/");
doc.appendChild(root);
root.appendChild(bodyElement);
bodyElement.appendChild(methodElement);
if (instanceId != null) {
instanceElement.setTextContent(instanceId);
methodElement.appendChild(instanceElement);
}
if (params != null) {
for (Map.Entry<String, String> entry : params.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
Element element = doc.createElement(key);
element.setTextContent(value);
methodElement.appendChild(element);
}
}
return xmlToString(doc, true);
} catch (Exception e) {
return null;
}
}
protected String getMetadata(String mediaURL, SubtitleInfo subtitle, String mime, String title, String description, String iconUrl) {
try {
String objectClass = "";
if (mime.startsWith("image")) {
objectClass = "object.item.imageItem";
} else if (mime.startsWith("video")) {
objectClass = "object.item.videoItem";
} else if (mime.startsWith("audio")) {
objectClass = "object.item.audioItem";
}
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.newDocument();
Element didlRoot = doc.createElement("DIDL-Lite");
Element itemElement = doc.createElement("item");
Element titleElement = doc.createElement("dc:title");
Element descriptionElement = doc.createElement("dc:description");
Element resElement = doc.createElement("res");
Element albumArtElement = doc.createElement("upnp:albumArtURI");
Element clazzElement = doc.createElement("upnp:class");
didlRoot.appendChild(itemElement);
itemElement.appendChild(titleElement);
itemElement.appendChild(descriptionElement);
itemElement.appendChild(resElement);
itemElement.appendChild(albumArtElement);
itemElement.appendChild(clazzElement);
didlRoot.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns", "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/");
didlRoot.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:upnp", "urn:schemas-upnp-org:metadata-1-0/upnp/");
didlRoot.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:dc", "http://purl.org/dc/elements/1.1/");
didlRoot.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:sec", "http://www.sec.co.kr/");
titleElement.setTextContent(title);
descriptionElement.setTextContent(description);
resElement.setTextContent(encodeURL(mediaURL));
albumArtElement.setTextContent(encodeURL(iconUrl));
clazzElement.setTextContent(objectClass);
itemElement.setAttribute("id", "1000");
itemElement.setAttribute("parentID", "0");
itemElement.setAttribute("restricted", "0");
resElement.setAttribute("protocolInfo", "http-get:*:" + mime + ":DLNA.ORG_OP=01");
if (subtitle != null) {
String mimeType = (subtitle.getMimeType() == null) ? DEFAULT_SUBTITLE_TYPE : subtitle.getMimeType();
String type;
String[] typeParts = mimeType.split("/");
if (typeParts != null && typeParts.length == 2) {
type = typeParts[1];
} else {
mimeType = DEFAULT_SUBTITLE_MIMETYPE;
type = DEFAULT_SUBTITLE_TYPE;
}
resElement.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:pv", "http://www.pv.com/pvns/");
resElement.setAttribute("pv:subtitleFileUri", subtitle.getUrl());
resElement.setAttribute("pv:subtitleFileType", type);
Element smiResElement = doc.createElement("res");
smiResElement.setAttribute("protocolInfo", "http-get:*:smi/caption");
smiResElement.setTextContent(subtitle.getUrl());
itemElement.appendChild(smiResElement);
Element srtResElement = doc.createElement("res");
srtResElement.setAttribute("protocolInfo", "http-get:*:"+mimeType+":");
srtResElement.setTextContent(subtitle.getUrl());
itemElement.appendChild(srtResElement);
Element captionInfoExElement = doc.createElement("sec:CaptionInfoEx");
captionInfoExElement.setAttribute("sec:type", type);
captionInfoExElement.setTextContent(subtitle.getUrl());
itemElement.appendChild(captionInfoExElement);
Element captionInfoElement = doc.createElement("sec:CaptionInfo");
captionInfoElement.setAttribute("sec:type", type);
captionInfoElement.setTextContent(subtitle.getUrl());
itemElement.appendChild(captionInfoElement);
}
doc.appendChild(didlRoot);
return xmlToString(doc, false);
} catch (Exception e) {
return null;
}
}
String encodeURL(String mediaURL) throws MalformedURLException, URISyntaxException, UnsupportedEncodingException {
if (mediaURL == null || mediaURL.isEmpty()) {
return "";
}
String decodedURL = URLDecoder.decode(mediaURL, "UTF-8");
if (decodedURL.equals(mediaURL)) {
URL url = new URL(mediaURL);
URI uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef());
return uri.toASCIIString();
}
return mediaURL;
}
String xmlToString(Node source, boolean xmlDeclaration) throws TransformerException {
DOMSource domSource = new DOMSource(source);
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
if (!xmlDeclaration) {
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
}
transformer.transform(domSource, result);
return writer.toString();
}
@Override
public void sendCommand(final ServiceCommand<?> mCommand) {
Util.runInBackground(new Runnable() {
@SuppressWarnings("unchecked")
@Override
public void run() {
ServiceCommand<ResponseListener<Object>> command = (ServiceCommand<ResponseListener<Object>>) mCommand;
String method = command.getTarget();
String payload = (String) command.getPayload();
String targetURL = null;
String serviceURN = null;
if (payload == null) {
Util.postError(command.getResponseListener(), new ServiceCommandError(0, "Cannot process the command, \"payload\" is missed", null));
return;
}
if (payload.contains(AV_TRANSPORT_URN)) {
targetURL = avTransportURL;
serviceURN = AV_TRANSPORT_URN;
} else if (payload.contains(RENDERING_CONTROL_URN)) {
targetURL = renderingControlURL;
serviceURN = RENDERING_CONTROL_URN;
} else if (payload.contains(CONNECTION_MANAGER_URN)) {
targetURL = connectionControlURL;
serviceURN = CONNECTION_MANAGER_URN;
}
if (serviceURN == null) {
Util.postError(command.getResponseListener(), new ServiceCommandError(0, "Cannot process the command, \"serviceURN\" is missed", null));
return;
}
if (targetURL == null) {
Util.postError(command.getResponseListener(), new ServiceCommandError(0, "Cannot process the command, \"targetURL\" is missed", null));
return;
}
try {
HttpConnection connection = createHttpConnection(targetURL);
connection.setHeader("Content-Type", "text/xml; charset=utf-8");
connection.setHeader("SOAPAction", String.format("\"%s#%s\"", serviceURN, method));
connection.setMethod(HttpConnection.Method.POST);
connection.setPayload(payload);
connection.execute();
int code = connection.getResponseCode();
if (code == 200) {
Util.postSuccess(command.getResponseListener(), connection.getResponseString());
} else {
Util.postError(command.getResponseListener(), ServiceCommandError.getError(code));
}
} catch (IOException e) {
Util.postError(command.getResponseListener(), new ServiceCommandError(0, e.getMessage(), null));
}
}
});
}
HttpConnection createHttpConnection(String targetURL) throws IOException {
return HttpConnection.newInstance(URI.create(targetURL));
}
@Override
protected void updateCapabilities() {
List<String> capabilities = new ArrayList<String>();
capabilities.add(Display_Image);
capabilities.add(Play_Video);
capabilities.add(Play_Audio);
capabilities.add(Play_Playlist);
capabilities.add(Close);
capabilities.add(Subtitle_SRT);
capabilities.add(MetaData_Title);
capabilities.add(MetaData_MimeType);
capabilities.add(MediaInfo_Get);
capabilities.add(MediaInfo_Subscribe);
capabilities.add(Play);
capabilities.add(Pause);
capabilities.add(Stop);
capabilities.add(Seek);
capabilities.add(Position);
capabilities.add(Duration);
capabilities.add(PlayState);
capabilities.add(PlayState_Subscribe);
// for supporting legacy apps. it might be removed in future releases
capabilities.add(MediaControl.Next);
capabilities.add(MediaControl.Previous);
// playlist capabilities
capabilities.add(PlaylistControl.Next);
capabilities.add(PlaylistControl.Previous);
capabilities.add(PlaylistControl.JumpToTrack);
capabilities.add(PlaylistControl.SetPlayMode);
capabilities.add(Volume_Set);
capabilities.add(Volume_Get);
capabilities.add(Volume_Up_Down);
capabilities.add(Volume_Subscribe);
capabilities.add(Mute_Get);
capabilities.add(Mute_Set);
capabilities.add(Mute_Subscribe);
setCapabilities(capabilities);
}
@Override
public LaunchSession decodeLaunchSession(String type, JSONObject sessionObj) throws JSONException {
if (type.equals("dlna")) {
LaunchSession launchSession = LaunchSession.launchSessionFromJSONObject(sessionObj);
launchSession.setService(this);
return launchSession;
}
return null;
}
private boolean isXmlEncoded(final String xml) {
if (xml == null || xml.length() < 4) {
return false;
}
return xml.trim().substring(0, 4).equals("<");
}
String parseData(String response, String key) {
if (isXmlEncoded(response)) {
response = Html.fromHtml(response).toString();
}
XmlPullParser parser = Xml.newPullParser();
try {
parser.setInput(new StringReader(response));
int event;
boolean isFound = false;
do {
event = parser.next();
if (event == XmlPullParser.START_TAG) {
String tag = parser.getName();
if (key.equals(tag)) {
isFound = true;
}
} else if (event == XmlPullParser.TEXT && isFound) {
return parser.getText();
}
} while (event != XmlPullParser.END_DOCUMENT);
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
long convertStrTimeFormatToLong(String strTime) {
long time = 0;
SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss");
try {
Date d = df.parse(strTime);
Date d2 = df.parse("00:00:00");
time = d.getTime() - d2.getTime();
} catch (ParseException e) {
Log.w(Util.T, "Invalid Time Format: " + strTime);
} catch (NullPointerException e) {
Log.w(Util.T, "Null time argument");
}
return time;
}
@Override
public void getPlayState(final PlayStateListener listener) {
String method = "GetTransportInfo";
String instanceId = "0";
String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, null);
ResponseListener<Object> responseListener = new ResponseListener<Object>() {
@Override
public void onSuccess(Object response) {
String transportState = parseData((String)response, "CurrentTransportState");
PlayStateStatus status = PlayStateStatus.convertTransportStateToPlayStateStatus(transportState);
Util.postSuccess(listener, status);
}
@Override
public void onError(ServiceCommandError error) {
Util.postError(listener, error);
}
};
ServiceCommand<ResponseListener<Object>> request = new ServiceCommand<ResponseListener<Object>>(this, method, payload, responseListener);
request.send();
}
@Override
public ServiceSubscription<PlayStateListener> subscribePlayState(PlayStateListener listener) {
URLServiceSubscription<PlayStateListener> request = new URLServiceSubscription<MediaControl.PlayStateListener>(this, PLAY_STATE, null, null);
request.addListener(listener);
addSubscription(request);
return request;
}
private void addSubscription(URLServiceSubscription<?> subscription) {
if (!httpServer.isRunning()) {
Util.runInBackground(new Runnable() {
@Override
public void run() {
httpServer.start();
}
});
subscribeServices();
}
httpServer.getSubscriptions().add(subscription);
}
@Override
public void unsubscribe(URLServiceSubscription<?> subscription) {
httpServer.getSubscriptions().remove(subscription);
if (httpServer.getSubscriptions().isEmpty()) {
unsubscribeServices();
}
}
@Override
public boolean isConnectable() {
return true;
}
@Override
public boolean isConnected() {
return connected;
}
@Override
public void connect() {
// TODO: Fix this for roku. Right now it is using the InetAddress reachable function. Need to use an HTTP Method.
// mServiceReachability = DeviceServiceReachability.getReachability(serviceDescription.getIpAddress(), this);
// mServiceReachability.start();
connected = true;
reportConnected(true);
}
private void getDeviceCapabilities(final PositionInfoListener listener) {
String method = "GetDeviceCapabilities";
String instanceId = "0";
String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, null);
ResponseListener<Object> responseListener = new ResponseListener<Object>() {
@Override
public void onSuccess(Object response) {
if (listener != null) {
listener.onGetPositionInfoSuccess((String)response);
}
}
@Override
public void onError(ServiceCommandError error) {
if (listener != null) {
listener.onGetPositionInfoFailed(error);
}
}
};
ServiceCommand<ResponseListener<Object>> request = new ServiceCommand<ResponseListener<Object>>(this, method, payload, responseListener);
request.send();
}
private void getProtocolInfo(final PositionInfoListener listener) {
String method = "GetProtocolInfo";
String instanceId = null;
String payload = getMessageXml(CONNECTION_MANAGER_URN, method, instanceId, null);
ResponseListener<Object> responseListener = new ResponseListener<Object>() {
@Override
public void onSuccess(Object response) {
if (listener != null) {
listener.onGetPositionInfoSuccess((String) response);
}
}
@Override
public void onError(ServiceCommandError error) {
if (listener != null) {
listener.onGetPositionInfoFailed(error);
}
}
};
ServiceCommand<ResponseListener<Object>> request = new ServiceCommand<ResponseListener<Object>>(this, method, payload, responseListener);
request.send();
}
@Override
public void disconnect() {
connected = false;
if (mServiceReachability != null)
mServiceReachability.stop();
Util.runOnUI(new Runnable() {
@Override
public void run() {
if (listener != null) {
listener.onDisconnect(DLNAService.this, null);
}
}
});
Util.runInBackground(new Runnable() {
@Override
public void run() {
httpServer.stop();
}
}, true);
}
@Override
public void onLoseReachability(DeviceServiceReachability reachability) {
if (connected) {
disconnect();
} else {
mServiceReachability.stop();
}
}
public void subscribeServices() {
Util.runInBackground(new Runnable() {
@Override
public void run() {
String myIpAddress = null;
try {
myIpAddress = Util.getIpAddress(context).getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
List<Service> serviceList = serviceDescription.getServiceList();
if (serviceList != null) {
for (int i = 0; i < serviceList.size(); i++) {
String eventSubURL = makeControlURL("/", serviceList.get(i).eventSubURL);
if (eventSubURL == null) {
continue;
}
try {
HttpConnection connection = HttpConnection.newSubscriptionInstance(
new URI("http", "", serviceDescription.getIpAddress(), serviceDescription.getPort(), eventSubURL, "", ""));
connection.setMethod(HttpConnection.Method.SUBSCRIBE);
connection.setHeader("CALLBACK", "<http://" + myIpAddress + ":" + httpServer.getPort() + eventSubURL + ">");
connection.setHeader("NT", "upnp:event");
connection.setHeader("TIMEOUT", "Second-" + TIMEOUT);
connection.setHeader("Connection", "close");
connection.setHeader("Content-length", "0");
connection.setHeader("USER-AGENT", "Android UPnp/1.1 ConnectSDK");
connection.execute();
if (connection.getResponseCode() == 200) {
SIDList.put(serviceList.get(i).serviceType, connection.getResponseHeader("SID"));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
});
resubscribeServices();
}
public void resubscribeServices() {
resubscriptionTimer = new Timer();
resubscriptionTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
Util.runInBackground(new Runnable() {
@Override
public void run() {
List<Service> serviceList = serviceDescription.getServiceList();
if (serviceList != null) {
for (int i = 0; i < serviceList.size(); i++) {
String eventSubURL = makeControlURL("/", serviceList.get(i).eventSubURL);
if (eventSubURL == null) {
continue;
}
String SID = SIDList.get(serviceList.get(i).serviceType);
try {
HttpConnection connection = HttpConnection.newSubscriptionInstance(
new URI("http", "", serviceDescription.getIpAddress(), serviceDescription.getPort(), eventSubURL, "", ""));
connection.setMethod(HttpConnection.Method.SUBSCRIBE);
connection.setHeader("TIMEOUT", "Second-" + TIMEOUT);
connection.setHeader("SID", SID);
connection.execute();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
});
}
}, TIMEOUT/2*1000, TIMEOUT/2*1000);
}
public void unsubscribeServices() {
if (resubscriptionTimer != null) {
resubscriptionTimer.cancel();
}
Util.runInBackground(new Runnable() {
@Override
public void run() {
final List<Service> serviceList = serviceDescription.getServiceList();
if (serviceList != null) {
for (int i = 0; i < serviceList.size(); i++) {
String eventSubURL = makeControlURL("/", serviceList.get(i).eventSubURL);
if (eventSubURL == null) {
continue;
}
String sid = SIDList.get(serviceList.get(i).serviceType);
try {
HttpConnection connection = HttpConnection.newSubscriptionInstance(
new URI("http", "", serviceDescription.getIpAddress(), serviceDescription.getPort(), eventSubURL, "", ""));
connection.setMethod(HttpConnection.Method.UNSUBSCRIBE);
connection.setHeader("SID", sid);
connection.execute();
if (connection.getResponseCode() == 200) {
SIDList.remove(serviceList.get(i).serviceType);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
});
}
@Override
public VolumeControl getVolumeControl() {
return this;
}
@Override
public CapabilityPriorityLevel getVolumeControlCapabilityLevel() {
return CapabilityPriorityLevel.NORMAL;
}
@Override
public void volumeUp(final ResponseListener<Object> listener) {
getVolume(new VolumeListener() {
@Override
public void onSuccess(final Float volume) {
if (volume >= 1.0) {
Util.postSuccess(listener, null);
}
else {
float newVolume = (float) (volume + 0.01);
if (newVolume > 1.0)
newVolume = (float) 1.0;
setVolume(newVolume, listener);
Util.postSuccess(listener, null);
}
}
@Override
public void onError(ServiceCommandError error) {
Util.postError(listener, error);
}
});
}
@Override
public void volumeDown(final ResponseListener<Object> listener) {
getVolume(new VolumeListener() {
@Override
public void onSuccess(final Float volume) {
if (volume <= 0.0) {
Util.postSuccess(listener, null);
}
else {
float newVolume = (float) (volume - 0.01);
if (newVolume < 0.0)
newVolume = (float) 0.0;
setVolume(newVolume, listener);
Util.postSuccess(listener, null);
}
}
@Override
public void onError(ServiceCommandError error) {
Util.postError(listener, error);
}
});
}
@Override
public void setVolume(float volume, ResponseListener<Object> listener) {
String method = "SetVolume";
String instanceId = "0";
String channel = "Master";
String value = String.valueOf((int)(volume*100));
Map<String, String> params = new LinkedHashMap<String, String>();
params.put("Channel", channel);
params.put("DesiredVolume", value);
String payload = getMessageXml(RENDERING_CONTROL_URN, method, instanceId, params);
ServiceCommand<ResponseListener<Object>> request = new ServiceCommand<ResponseListener<Object>>(this, method, payload, listener);
request.send();
}
@Override
public void getVolume(final VolumeListener listener) {
String method = "GetVolume";
String instanceId = "0";
String channel = "Master";
Map<String, String> params = new LinkedHashMap<String, String>();
params.put("Channel", channel);
String payload = getMessageXml(RENDERING_CONTROL_URN, method, instanceId, params);
ResponseListener<Object> responseListener = new ResponseListener<Object>() {
@Override
public void onSuccess(Object response) {
String currentVolume = parseData((String) response, "CurrentVolume");
int iVolume = 0;
try {
//noinspection ResultOfMethodCallIgnored
Integer.parseInt(currentVolume);
} catch (RuntimeException ex) {
ex.printStackTrace();
}
float fVolume = (float) (iVolume / 100.0);
Util.postSuccess(listener, fVolume);
}
@Override
public void onError(ServiceCommandError error) {
Util.postError(listener, error);
}
};
ServiceCommand<VolumeListener> request = new ServiceCommand<VolumeListener>(this, method, payload, responseListener);
request.send();
}
@Override
public void setMute(boolean isMute, ResponseListener<Object> listener) {
String method = "SetMute";
String instanceId = "0";
String channel = "Master";
int muteStatus = (isMute) ? 1 : 0;
Map<String, String> params = new LinkedHashMap<String, String>();
params.put("Channel", channel);
params.put("DesiredMute", String.valueOf(muteStatus));
String payload = getMessageXml(RENDERING_CONTROL_URN, method, instanceId, params);
ServiceCommand<ResponseListener<Object>> request = new ServiceCommand<ResponseListener<Object>>(this, method, payload, listener);
request.send();
}
@Override
public void getMute(final MuteListener listener) {
String method = "GetMute";
String instanceId = "0";
String channel = "Master";
Map<String, String> params = new LinkedHashMap<String, String>();
params.put("Channel", channel);
String payload = getMessageXml(RENDERING_CONTROL_URN, method, instanceId, params);
ResponseListener<Object> responseListener = new ResponseListener<Object>() {
@Override
public void onSuccess(Object response) {
String currentMute = parseData((String) response, "CurrentMute");
boolean isMute = Boolean.parseBoolean(currentMute);
Util.postSuccess(listener, isMute);
}
@Override
public void onError(ServiceCommandError error) {
Util.postError(listener, error);
}
};
ServiceCommand<ResponseListener<Object>> request = new ServiceCommand<ResponseListener<Object>>(this, method, payload, responseListener);
request.send();
}
@Override
public ServiceSubscription<VolumeListener> subscribeVolume(VolumeListener listener) {
URLServiceSubscription<VolumeListener> request = new URLServiceSubscription<VolumeListener>(this, "volume", null, null);
request.addListener(listener);
addSubscription(request);
return request;
}
@Override
public ServiceSubscription<MuteListener> subscribeMute(MuteListener listener) {
URLServiceSubscription<MuteListener> request = new URLServiceSubscription<MuteListener>(this, "mute", null, null);
request.addListener(listener);
addSubscription(request);
return request;
}
}