From 5eda6d303f6956cde0020ba6ba945403b3328f68 Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Fri, 8 Aug 2014 15:22:24 +0900 Subject: [PATCH 01/76] Revert "Stripped out MultiScreen discovery for next 1.3.x release" --- libs/multiscreen-android-1.1.11.jar | Bin 0 -> 171435 bytes .../discovery/DiscoveryManager.java | 13 + .../service/MultiScreenService.java | 481 ++++++++++ .../sessions/MultiScreenWebAppSession.java | 843 ++++++++++++++++++ 4 files changed, 1337 insertions(+) create mode 100644 libs/multiscreen-android-1.1.11.jar create mode 100644 src/com/connectsdk/service/MultiScreenService.java create mode 100644 src/com/connectsdk/service/sessions/MultiScreenWebAppSession.java diff --git a/libs/multiscreen-android-1.1.11.jar b/libs/multiscreen-android-1.1.11.jar new file mode 100644 index 0000000000000000000000000000000000000000..6a71784a1a0608dd33026b7f8c699af3e8b08d24 GIT binary patch literal 171435 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 z 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 literal 0 HcmV?d00001 diff --git a/src/com/connectsdk/discovery/DiscoveryManager.java b/src/com/connectsdk/discovery/DiscoveryManager.java index 2f152595..59905bf4 100644 --- a/src/com/connectsdk/discovery/DiscoveryManager.java +++ b/src/com/connectsdk/discovery/DiscoveryManager.java @@ -59,6 +59,7 @@ import com.connectsdk.service.DLNAService; import com.connectsdk.service.DeviceService; import com.connectsdk.service.DeviceService.PairingType; +import com.connectsdk.service.MultiScreenService; import com.connectsdk.service.NetcastTVService; import com.connectsdk.service.RokuService; import com.connectsdk.service.WebOSTVService; @@ -370,6 +371,7 @@ public void registerDefaultDeviceTypes() { registerDeviceService(RokuService.class, SSDPDiscoveryProvider.class); registerDeviceService(CastService.class, CastDiscoveryProvider.class); registerDeviceService(AirPlayService.class, ZeroconfDiscoveryProvider.class); + registerDeviceService(MultiScreenService.class, SSDPDiscoveryProvider.class); } /** @@ -630,6 +632,17 @@ public boolean isNetcast(ServiceDescription description) { return isNetcastTV; } + public boolean isSamsungMultiScreen(Class deviceServiceClass, ServiceDescription description) { + boolean isSamsungMultiScreen = false; + + String locationXML = description.getLocationXML(); + + if (locationXML != null && (locationXML.contains("samsung:multiscreen:1"))) { + isSamsungMultiScreen = true; + } + + return isSamsungMultiScreen; + } // @endcond /** diff --git a/src/com/connectsdk/service/MultiScreenService.java b/src/com/connectsdk/service/MultiScreenService.java new file mode 100644 index 00000000..07eb7035 --- /dev/null +++ b/src/com/connectsdk/service/MultiScreenService.java @@ -0,0 +1,481 @@ +package com.connectsdk.service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.json.JSONException; +import org.json.JSONObject; + +import com.connectsdk.core.Util; +import com.connectsdk.service.capability.MediaControl; +import com.connectsdk.service.capability.MediaPlayer; +import com.connectsdk.service.capability.WebAppLauncher; +import com.connectsdk.service.capability.listeners.ResponseListener; +import com.connectsdk.service.command.ServiceCommandError; +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.sessions.MultiScreenWebAppSession; +import com.connectsdk.service.sessions.WebAppSession; +import com.samsung.multiscreen.application.Application; +import com.samsung.multiscreen.application.Application.Status; +import com.samsung.multiscreen.application.ApplicationAsyncResult; +import com.samsung.multiscreen.application.ApplicationError; +import com.samsung.multiscreen.device.Device; +import com.samsung.multiscreen.device.DeviceAsyncResult; +import com.samsung.multiscreen.device.DeviceError; +import com.samsung.multiscreen.device.DeviceFactory; + +public class MultiScreenService extends DeviceService implements MediaPlayer, WebAppLauncher { + public static final String ID = "MultiScreen"; + + Device device; + Map sessions; + + public MultiScreenService(ServiceDescription serviceDescription, + ServiceConfig serviceConfig) { + super(serviceDescription, serviceConfig); + + Map map = new HashMap(); + + map.put("DeviceName", serviceDescription.getFriendlyName()); + map.put("DialURI", serviceDescription.getApplicationURL()); + map.put("IP", serviceDescription.getIpAddress()); + map.put("ModelDescription", serviceDescription.getModelDescription()); + map.put("ModelName", serviceDescription.getModelName()); + map.put("ServiceURI", serviceDescription.getServiceURI()); + + this.device = DeviceFactory.createWithMap(map); + } + + public static JSONObject discoveryParameters() { + JSONObject params = new JSONObject(); + + try { + params.put("serviceId", ID); +// params.put("filter", "urn:samsung.com:service:MultiScreenService:1"); + params.put("filter", "urn:dial-multiscreen-org:service:dial:1"); + } catch (JSONException e) { + e.printStackTrace(); + } + + return params; + } + + @Override + public boolean isConnectable() { + return true; + } + + @Override + public boolean isConnected() { + return connected; + } + + @Override + public void connect() { + connected = true; + + sessions = new HashMap(); + + reportConnected(true); + } + + @Override + public void disconnect() { + for (MultiScreenWebAppSession session: sessions.values()) { + session.disconnectFromWebApp(); + } + + connected = false; + + if (mServiceReachability != null) + mServiceReachability.stop(); + + Util.runOnUI(new Runnable() { + + @Override + public void run() { + if (listener != null) + listener.onDisconnect(MultiScreenService.this, null); + } + }); + } + + @Override + public MediaPlayer getMediaPlayer() { + return this; + } + + @Override + public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { + return CapabilityPriorityLevel.HIGH; + } + + @Override + public void displayImage(final String url, final String mimeType, final String title, + final String description, final String iconSrc, final LaunchListener listener) { + final String webAppId = "ConnectSDKSampler"; + + getWebAppLauncher().joinWebApp(webAppId, new WebAppSession.LaunchListener() { + + @Override + public void onSuccess(WebAppSession webAppSession) { + webAppSession.getMediaPlayer().displayImage(url, mimeType, title, description, iconSrc, listener); + } + + @Override + public void onError(ServiceCommandError error) { + getWebAppLauncher().launchWebApp(webAppId, new WebAppSession.LaunchListener() { + + @Override + public void onError(ServiceCommandError error) { + if (listener != null) { + Util.postError(listener, error); + } + } + + @Override + public void onSuccess(final WebAppSession webAppSession) { + webAppSession.connect(new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + Util.postError(listener, error); + } + + @Override + public void onSuccess(Object object) { + webAppSession.getMediaPlayer().displayImage(url, mimeType, title, description, iconSrc, listener); + } + }); + } + }); + } + }); + } + + @Override + public void playMedia(final String url, final String mimeType, final String title, + final String description, final String iconSrc, final boolean shouldLoop, + final LaunchListener listener) { + final String webAppId = "ConnectSDKSampler"; + + getWebAppLauncher().joinWebApp(webAppId, new WebAppSession.LaunchListener() { + + @Override + public void onSuccess(WebAppSession webAppSession) { + webAppSession.playMedia(url, mimeType, title, description, iconSrc, shouldLoop, listener); + } + + @Override + public void onError(ServiceCommandError error) { + getWebAppLauncher().launchWebApp(webAppId, new WebAppSession.LaunchListener() { + + @Override + public void onError(ServiceCommandError error) { + if (listener != null) { + Util.postError(listener, error); + } + } + + @Override + public void onSuccess(final WebAppSession webAppSession) { + webAppSession.connect(new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + Util.postError(listener, error); + } + + @Override + public void onSuccess(Object object) { + webAppSession.playMedia(url, mimeType, title, description, iconSrc, shouldLoop, listener); + } + }); + } + }); + } + }); + } + + @Override + public void closeMedia(LaunchSession launchSession, + ResponseListener listener) { + getWebAppLauncher().closeWebApp(launchSession, listener); + } + + @Override + public WebAppLauncher getWebAppLauncher() { + return this; + } + + @Override + public CapabilityPriorityLevel getWebAppLauncherCapabilityLevel() { + return CapabilityPriorityLevel.HIGH; + } + + @Override + public void launchWebApp(String webAppId, WebAppSession.LaunchListener listener) { + launchWebApp(webAppId, null, true, listener); + } + + @Override + public void launchWebApp( + String webAppId, + JSONObject params, + final com.connectsdk.service.sessions.WebAppSession.LaunchListener listener) { + launchWebApp(webAppId, params, true, listener); + } + + @Override + public void launchWebApp( + String webAppId, + boolean relaunchIfRunning, + com.connectsdk.service.sessions.WebAppSession.LaunchListener listener) { + launchWebApp(webAppId, null, relaunchIfRunning, listener); + } + + @Override + public void launchWebApp( + final String webAppId, + JSONObject params, + boolean relaunchIfRunning, + final com.connectsdk.service.sessions.WebAppSession.LaunchListener listener) { + ServiceCommandError error = null; + + if (webAppId == null || webAppId.length() == 0) { + error = new ServiceCommandError(0, "You must provide a valid web app id", null); + } + + if (device == null) { + error = new ServiceCommandError(0, "Could not find a reference to the native device object", null); + } + + if (error != null) { + if (listener != null) { + Util.postError(listener, error); + } + + return; + } + + if (params == null) { + params = new JSONObject(); + } + final JSONObject fParams = params; + + device.getApplication(webAppId, new DeviceAsyncResult() { + + @Override + public void onResult(final Application application) { + Map parameters = new HashMap(); + + Iterator keys = fParams.keys(); + while (keys.hasNext()) { + String key = (String) keys.next(); + try { + parameters.put(key, fParams.getString(key)); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + application.launch(parameters, new ApplicationAsyncResult() { + + @Override + public void onError(ApplicationError error) { + Util.postError(listener, new ServiceCommandError((int)error.getCode(), error.getMessage(), error)); + } + + @Override + public void onResult(Boolean launchSuccess) { + if (launchSuccess) { + LaunchSession launchSession = LaunchSession.launchSessionForAppId(webAppId); + launchSession.setSessionType(LaunchSessionType.WebApp); + launchSession.setService(MultiScreenService.this); + + MultiScreenWebAppSession webAppSession = sessions.get(webAppId); + + if (webAppSession == null) { + webAppSession = new MultiScreenWebAppSession(launchSession, MultiScreenService.this); + sessions.put(webAppId, webAppSession); + } + + webAppSession.setApplication(application); + + if (listener != null) { + Util.postSuccess(listener, webAppSession); + } + } + else { + if (listener != null) { + Util.postError(listener, new ServiceCommandError(0, "Experienced an unknown error launching app", null)); + } + } + } + }); + } + + @Override + public void onError(DeviceError error) { + Util.postError(listener, new ServiceCommandError((int)error.getCode(), error.getMessage(), error)); + } + }); + } + + @Override + public void joinWebApp( + final LaunchSession webAppLaunchSession, + final com.connectsdk.service.sessions.WebAppSession.LaunchListener listener) { + + device.getApplication(webAppLaunchSession.getAppId(), new DeviceAsyncResult() { + + @Override + public void onResult(Application application) { + final MultiScreenWebAppSession webAppSession; + + if (sessions.containsKey(webAppLaunchSession.getAppId())) { + webAppSession = sessions.get(webAppLaunchSession.getAppId()); + } + else { + webAppSession = new MultiScreenWebAppSession(webAppLaunchSession, MultiScreenService.this); + sessions.put(webAppLaunchSession.getAppId(), webAppSession); + } + + webAppSession.setApplication(application); + webAppSession.join(new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + Util.postError(listener, error); + } + + @Override + public void onSuccess(Object object) { + Util.postSuccess(listener, webAppSession); + } + }); + } + + @Override + public void onError(DeviceError error) { + if (listener != null) { + Util.postError(listener, new ServiceCommandError((int)error.getCode(), error.getMessage(), error)); + } + } + }); + } + + @Override + public void joinWebApp(String webAppId, + com.connectsdk.service.sessions.WebAppSession.LaunchListener listener) { + LaunchSession launchSession = LaunchSession.launchSessionForAppId(webAppId); + launchSession.setSessionType(LaunchSessionType.WebApp); + launchSession.setService(this); + + getWebAppLauncher().joinWebApp(launchSession, listener); + } + + @Override + public void closeWebApp(final LaunchSession launchSession, + final ResponseListener listener) { + ServiceCommandError error = null; + + if (launchSession == null || launchSession.getAppId() == null || launchSession.getAppId().length() == 0) { + error = new ServiceCommandError(0, "You must provide a valid launch session", null); + } + + if (device == null) { + error = new ServiceCommandError(0, "Could not find a reference to the native device object", null); + } + + if (error != null) { + if (listener != null) { + Util.postError(listener, error); + } + + return; + } + + device.getApplication(launchSession.getAppId(), new DeviceAsyncResult() { + + @Override + public void onError(DeviceError error) { + if (listener != null) { + Util.postError(listener, new ServiceCommandError((int)error.getCode(), error.getMessage(), error)); + } + } + + @Override + public void onResult(Application application) { + if (application.getLastKnownStatus() == Status.RUNNING) { + application.terminate(new ApplicationAsyncResult() { + + @Override + public void onResult(Boolean terminateSuccess) { + if (terminateSuccess) { + sessions.remove(launchSession.getAppId()); + + if (listener != null) { + Util.postSuccess(listener, null); + } + } + else { + if (listener != null) { + Util.postError(listener, new ServiceCommandError(0, "Experienced an unknown error terminating app", null)); + } + } + } + + @Override + public void onError(ApplicationError error) { + Util.postError(listener, new ServiceCommandError((int)error.getCode(), error.getMessage(), error)); + } + }); + } + else { + if (listener != null) { + Util.postSuccess(listener, null); + } + } + } + }); + } + + public Device getDevice() { + return device; + } + + @Override + protected void updateCapabilities() { + List capabilities = new ArrayList(); + + for (String capability : MediaPlayer.Capabilities) { capabilities.add(capability); } + + capabilities.add(MediaControl.Play); + capabilities.add(MediaControl.Pause); + capabilities.add(MediaControl.Duration); + capabilities.add(MediaControl.Seek); + capabilities.add(MediaControl.Position); + capabilities.add(MediaControl.PlayState); + capabilities.add(MediaControl.PlayState_Subscribe); + + capabilities.add(Launch); + capabilities.add(Launch_Params); + capabilities.add(Join); + capabilities.add(Connect); + capabilities.add(Disconnect); + capabilities.add(Message_Send); + capabilities.add(Message_Send_JSON); + capabilities.add(Message_Receive); + capabilities.add(Message_Receive_JSON); + capabilities.add(WebAppLauncher.Close); + + setCapabilities(capabilities); + } +} diff --git a/src/com/connectsdk/service/sessions/MultiScreenWebAppSession.java b/src/com/connectsdk/service/sessions/MultiScreenWebAppSession.java new file mode 100644 index 00000000..05e431cc --- /dev/null +++ b/src/com/connectsdk/service/sessions/MultiScreenWebAppSession.java @@ -0,0 +1,843 @@ +package com.connectsdk.service.sessions; + +import java.util.HashMap; +import java.util.Map; + +import org.json.JSONException; +import org.json.JSONObject; + +import com.connectsdk.core.Util; +import com.connectsdk.service.DeviceService; +import com.connectsdk.service.MultiScreenService; +import com.connectsdk.service.capability.MediaControl; +import com.connectsdk.service.capability.MediaPlayer; +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.samsung.multiscreen.application.Application; +import com.samsung.multiscreen.application.Application.Status; +import com.samsung.multiscreen.application.ApplicationAsyncResult; +import com.samsung.multiscreen.application.ApplicationError; +import com.samsung.multiscreen.channel.Channel; +import com.samsung.multiscreen.channel.ChannelAsyncResult; +import com.samsung.multiscreen.channel.ChannelClient; +import com.samsung.multiscreen.channel.ChannelError; +import com.samsung.multiscreen.channel.IChannelListener; +import com.samsung.multiscreen.device.DeviceAsyncResult; +import com.samsung.multiscreen.device.DeviceError; + +public class MultiScreenWebAppSession extends WebAppSession { + protected MultiScreenService service; + protected Application application; + + private final String channelId = "com.connectsdk.MainChannel"; + + ServiceSubscription playStateSubscription; + Map>> activeCommands; + + private int UID; + + private Channel mChannel; + private IChannelListener channelListener = new IChannelListener() { + + @Override + public void onDisconnect() { + mChannel = null; + } + + @Override + public void onConnect() { + // TODO Auto-generated method stub + + } + + @Override + public void onClientMessage(ChannelClient client, String message) { + try { + JSONObject messageJSON = new JSONObject(message); + + String contentType = messageJSON.optString("contentType"); + String str = new String("connectsdk."); + + if (contentType != null && contentType.contains(str)) { + String payloadKey = contentType.substring(str.length()); + + if (payloadKey == null || payloadKey.length() == 0) + return; + + JSONObject messagePayload = messageJSON.optJSONObject(payloadKey); + + if (messagePayload == null) + return; + + if (payloadKey.equals("mediaEvent")) { + handleMediaEvent(messagePayload); + } + else if (payloadKey.equals("mediaCommandResponse")) { + handleMediaCommandResponse(messagePayload); + } + + } + else { + handleMessage(messageJSON); + } + } catch (JSONException e) { + handleMessage(message); + } + } + + @Override + public void onClientDisconnected(ChannelClient client) { + // TODO Auto-generated method stub + + } + + @Override + public void onClientConnected(ChannelClient client) { + // TODO Auto-generated method stub + + } + }; + + public void handleMediaEvent(JSONObject payload) { + String type = payload.optString("type", null); + + if (type.equals("playState")) { + if (playStateSubscription == null) + return; + + String playStateString = payload.optString("playState"); + PlayStateStatus playState = parsePlayState(playStateString); + + for (PlayStateListener listener: playStateSubscription.getListeners()) { + Util.postSuccess(listener, playState); + } + } + } + + public void handleMediaCommandResponse(JSONObject payload) { + String requestId = payload.optString("requestId"); + + ServiceCommand command = activeCommands.get(requestId); + + if (command == null) + return; + + String error = payload.optString("error", null); + + if (error != null) { + if (command.getResponseListener() != null) { + command.getResponseListener().onError(new ServiceCommandError(0, error, null)); + } + } + else { + if (command.getResponseListener() != null) { + command.getResponseListener().onSuccess(payload); + } + } + + activeCommands.remove(requestId); + } + + public void handleMessage(Object message) { + if (getWebAppSessionListener() != null) { + getWebAppSessionListener().onReceiveMessage(this, message); + } + } + + public MultiScreenWebAppSession(LaunchSession launchSession, + DeviceService service) { + super(launchSession, service); + + this.service = (MultiScreenService) service; + } + + public PlayStateStatus parsePlayState(String playStateString) { + PlayStateStatus playState = PlayStateStatus.Unknown; + + if (playStateString.equals("playing")) + playState = PlayStateStatus.Playing; + else if (playStateString.equals("paused")) + playState = PlayStateStatus.Paused; + else if (playStateString.equals("idle")) + playState = PlayStateStatus.Idle; + else if (playStateString.equals("buffering")) + playState = PlayStateStatus.Buffering; + else if (playStateString.equals("finished")) + playState = PlayStateStatus.Finished; + + return playState; + + } + + public int getNextId() { + return UID++; + } + + @Override + public void connect(final ResponseListener listener) { + if (service == null || service.getDevice() == null) { + if (listener != null) { + Util.postError(listener, new ServiceCommandError(0, "You can only connect to a valid WebAppSession object.", null)); + } + + return; + } + + activeCommands = new HashMap>>(); + UID = 0; + + service.getDevice().connectToChannel(channelId, new DeviceAsyncResult() { + + @Override + public void onResult(Channel channel) { + mChannel= channel; + mChannel.setListener(channelListener); + + if (listener != null) { + Util.postSuccess(listener, null); + } + } + + @Override + public void onError(DeviceError error) { + if (listener != null) { + Util.postError(listener, new ServiceCommandError((int)error.getCode(), error.getMessage(), error)); + } + } + }); + } + + @Override + public void join(final ResponseListener listener) { + application.updateStatus(new ApplicationAsyncResult() { + + @Override + public void onResult(Status status) { + if (status == Status.RUNNING) { + connect(listener); + } + else { + if (listener != null) { + listener.onError(new ServiceCommandError(0, "Cannot join a web app that is not running", null)); + } + } + } + + @Override + public void onError(ApplicationError error) { + if (listener != null) { + Util.postError(listener, new ServiceCommandError((int)error.getCode(), error.getMessage(), error)); + } + } + }); + } + + @Override + public void sendMessage(String message, ResponseListener listener) { + if (message == null || message.length() == 0) { + if (listener != null) { + Util.postError(listener, new ServiceCommandError(0, "Cannot send an empty message", null)); + } + + return; + } + + if (mChannel != null && mChannel.isConnected()) { + mChannel.sendToHost(message); + + if (listener != null) { + Util.postSuccess(listener, null); + } + } + else { + if (listener != null) { + Util.postError(listener, new ServiceCommandError(0, "Connection has not been established or has been lost", null)); + } + } + } + + @Override + public void sendMessage(JSONObject message, ResponseListener listener) { + if (message == null || message.length() == 0) { + if (listener != null) { + Util.postError(listener, new ServiceCommandError(0, "Cannot send an empty message", null)); + } + + return; + } + + sendMessage(message.toString(), listener); + } + + @Override + public void disconnectFromWebApp() { + if (mChannel == null) { + return; + } + + mChannel.disconnect(new ChannelAsyncResult() { + + @Override + public void onResult(Boolean result) { + mChannel.setListener(null); + + if (getWebAppSessionListener() != null) { + getWebAppSessionListener().onWebAppSessionDisconnect(MultiScreenWebAppSession.this); + } + } + + @Override + public void onError(ChannelError error) {} + }); + } + + @Override + public void close(final ResponseListener listener) { + if (mChannel != null && mChannel.isConnected()) { +// // This is a hack to enable closing of bridged web apps that we didn't open + JSONObject closeCommand = new JSONObject(); + JSONObject type = new JSONObject(); + + try { + type.put("type", "close"); + + closeCommand.put("contentType", "connectsdk.serviceCommand"); + closeCommand.put("serviceCommand", type); + } catch (JSONException e) { + e.printStackTrace(); + } + + sendMessage(closeCommand, new ResponseListener() { + + @Override + public void onSuccess(Object object) { + disconnectFromWebApp(); + + if (listener != null) { + Util.postSuccess(listener, null); + } + } + + @Override + public void onError(ServiceCommandError error) { + disconnectFromWebApp(); + + if (listener != null) { + Util.postError(listener, error); + } + } + }); + } + else { + service.getWebAppLauncher().closeWebApp(launchSession, listener); + } + } + + @Override + public MediaPlayer getMediaPlayer() { + return this; + } + + @Override + public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { + return CapabilityPriorityLevel.HIGH; + } + + @Override + public void displayImage(String url, String mimeType, String title, + String description, String iconSrc, final MediaPlayer.LaunchListener listener) { + int requestIdNumber = getNextId(); + String requestId = String.format("req%d", requestIdNumber); + + JSONObject message = new JSONObject(); + JSONObject mediaCommand = new JSONObject(); + + try { + mediaCommand.put("type", "displayImage"); + mediaCommand.put("mediaURL", url); + mediaCommand.put("iconURL", iconSrc); + mediaCommand.put("title", title); + mediaCommand.put("description", description); + mediaCommand.put("mimeType", mimeType); + mediaCommand.put("requestId", requestId); + + message.put("contentType", "connectsdk.mediaCommand"); + message.put("mediaCommand", mediaCommand); + } catch (JSONException e) { + e.printStackTrace(); + } + + ResponseListener responseListener = new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + if (listener != null) { + Util.postError(listener, error); + } + } + + @Override + public void onSuccess(Object object) { + if (listener != null) { + Util.postSuccess(listener, new MediaLaunchObject(launchSession, getMediaControl())); + } + } + }; + ServiceCommand> command = new ServiceCommand>(null, null, null, responseListener); + activeCommands.put(requestId, command); + + sendMessage(message.toString(), new ResponseListener() { + + @Override + public void onSuccess(Object object) { + // TODO Auto-generated method stub + + } + + @Override + public void onError(ServiceCommandError error) { + // TODO Auto-generated method stub + + } + }); + } + + @Override + public void playMedia(String url, String mimeType, String title, + String description, String iconSrc, boolean shouldLoop, + final MediaPlayer.LaunchListener listener) { + int requestIdNumber = getNextId(); + String requestId = String.format("req%d", requestIdNumber); + + JSONObject message = new JSONObject(); + JSONObject mediaCommand = new JSONObject(); + + try { + mediaCommand.put("type", "playMedia"); + mediaCommand.put("mediaURL", url); + mediaCommand.put("iconURL", iconSrc); + mediaCommand.put("title", title); + mediaCommand.put("description", description); + mediaCommand.put("mimeType", mimeType); + mediaCommand.put("shouldLoop", shouldLoop); + mediaCommand.put("requestId", requestId); + + message.put("contentType", "connectsdk.mediaCommand"); + message.put("mediaCommand", mediaCommand); + } catch (JSONException e) { + e.printStackTrace(); + } + + ResponseListener responseListener = new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + if (listener != null) { + Util.postError(listener, error); + } + } + + @Override + public void onSuccess(Object object) { + if (listener != null) { + Util.postSuccess(listener, new MediaLaunchObject(launchSession, getMediaControl())); + } + } + }; + ServiceCommand> command = new ServiceCommand>(null, null, null, responseListener); + activeCommands.put(requestId, command); + + sendMessage(message.toString(), new ResponseListener() { + + @Override + public void onSuccess(Object object) { + // TODO Auto-generated method stub + + } + + @Override + public void onError(ServiceCommandError error) { + // TODO Auto-generated method stub + + } + }); + } + + @Override + public void closeMedia(LaunchSession launchSession, ResponseListener listener) { + close(listener); + } + + @Override + public MediaControl getMediaControl() { + return this; + } + + @Override + public CapabilityPriorityLevel getMediaControlCapabilityLevel() { + return CapabilityPriorityLevel.HIGH; + } + + @Override + public void play(final ResponseListener listener) { + int requestIdNumber = getNextId(); + String requestId = String.format("req%d", requestIdNumber); + + JSONObject message = new JSONObject(); + JSONObject mediaCommand = new JSONObject(); + + try { + mediaCommand.put("type", "play"); + mediaCommand.put("requestId", requestId); + + message.put("contentType", "connectsdk.mediaCommand"); + message.put("mediaCommand", mediaCommand); + } catch (JSONException e) { + e.printStackTrace(); + } + + ResponseListener responseListener = new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + if (listener != null) { + Util.postError(listener, error); + } + } + + @Override + public void onSuccess(Object object) { + if (listener != null) { + Util.postSuccess(listener, null); + } + } + }; + ServiceCommand> command = new ServiceCommand>(null, null, null, responseListener); + activeCommands.put(requestId, command); + + sendMessage(message.toString(), new ResponseListener() { + + @Override + public void onSuccess(Object object) { + // TODO Auto-generated method stub + + } + + @Override + public void onError(ServiceCommandError error) { + // TODO Auto-generated method stub + + } + }); + } + + @Override + public void pause(final ResponseListener listener) { + int requestIdNumber = getNextId(); + String requestId = String.format("req%d", requestIdNumber); + + JSONObject message = new JSONObject(); + JSONObject mediaCommand = new JSONObject(); + + try { + mediaCommand.put("type", "pause"); + mediaCommand.put("requestId", requestId); + + message.put("contentType", "connectsdk.mediaCommand"); + message.put("mediaCommand", mediaCommand); + } catch (JSONException e) { + e.printStackTrace(); + } + + ResponseListener responseListener = new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + if (listener != null) { + Util.postError(listener, error); + } + } + + @Override + public void onSuccess(Object object) { + if (listener != null) { + Util.postSuccess(listener, null); + } + } + }; + ServiceCommand> command = new ServiceCommand>(null, null, null, responseListener); + activeCommands.put(requestId, command); + + sendMessage(message.toString(), new ResponseListener() { + + @Override + public void onSuccess(Object object) { + // TODO Auto-generated method stub + + } + + @Override + public void onError(ServiceCommandError error) { + // TODO Auto-generated method stub + + } + }); + } + + @Override + public void seek(long position, final ResponseListener listener) { + int requestIdNumber = getNextId(); + String requestId = String.format("req%d", requestIdNumber); + + JSONObject message = new JSONObject(); + JSONObject mediaCommand = new JSONObject(); + + try { + mediaCommand.put("type", "seek"); + mediaCommand.put("position", position); + mediaCommand.put("requestId", requestId); + + message.put("contentType", "connectsdk.mediaCommand"); + message.put("mediaCommand", mediaCommand); + } catch (JSONException e) { + e.printStackTrace(); + } + + ResponseListener responseListener = new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + if (listener != null) { + Util.postError(listener, error); + } + } + + @Override + public void onSuccess(Object object) { + if (listener != null) { + Util.postSuccess(listener, null); + } + } + }; + ServiceCommand> command = new ServiceCommand>(null, null, null, responseListener); + activeCommands.put(requestId, command); + + sendMessage(message.toString(), new ResponseListener() { + + @Override + public void onSuccess(Object object) { + // TODO Auto-generated method stub + + } + + @Override + public void onError(ServiceCommandError error) { + // TODO Auto-generated method stub + + } + }); + } + + @Override + public void getPosition(final PositionListener listener) { + int requestIdNumber = getNextId(); + String requestId = String.format("req%d", requestIdNumber); + + JSONObject message = new JSONObject(); + JSONObject mediaCommand = new JSONObject(); + + try { + mediaCommand.put("type", "getPosition"); + mediaCommand.put("requestId", requestId); + + message.put("contentType", "connectsdk.mediaCommand"); + message.put("mediaCommand", mediaCommand); + } catch (JSONException e) { + e.printStackTrace(); + } + + ResponseListener responseListener = new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + if (listener != null) { + Util.postError(listener, error); + } + } + + @Override + public void onSuccess(Object object) { + JSONObject responseObject = (JSONObject)object; + String positionString = responseObject.optString("position", null); + float position = 0; + + if (positionString != null) + position = Float.parseFloat(positionString) * 1000; + + if (listener != null) { + Util.postSuccess(listener, (long)position); + } + } + }; + ServiceCommand> command = new ServiceCommand>(null, null, null, responseListener); + activeCommands.put(requestId, command); + + sendMessage(message.toString(), new ResponseListener() { + + @Override + public void onSuccess(Object object) { + // TODO Auto-generated method stub + + } + + @Override + public void onError(ServiceCommandError error) { + // TODO Auto-generated method stub + + } + }); + } + + @Override + public void getDuration(final DurationListener listener) { + int requestIdNumber = getNextId(); + String requestId = String.format("req%d", requestIdNumber); + + JSONObject message = new JSONObject(); + JSONObject mediaCommand = new JSONObject(); + + try { + mediaCommand.put("type", "getDuration"); + mediaCommand.put("requestId", requestId); + + message.put("contentType", "connectsdk.mediaCommand"); + message.put("mediaCommand", mediaCommand); + } catch (JSONException e) { + e.printStackTrace(); + } + + ResponseListener responseListener = new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + if (listener != null) { + Util.postError(listener, error); + } + } + + @Override + public void onSuccess(Object object) { + JSONObject responseObject = (JSONObject)object; + String durationString = responseObject.optString("duration", null); + float duration = 0; + + if (durationString != null) + duration = Float.parseFloat(durationString) * 1000; + + if (listener != null) { + Util.postSuccess(listener, (long)duration); + } + } + }; + ServiceCommand> command = new ServiceCommand>(null, null, null, responseListener); + activeCommands.put(requestId, command); + + sendMessage(message.toString(), new ResponseListener() { + + @Override + public void onSuccess(Object object) { + // TODO Auto-generated method stub + + } + + @Override + public void onError(ServiceCommandError error) { + // TODO Auto-generated method stub + + } + }); + } + + @Override + public void getPlayState(final PlayStateListener listener) { + int requestIdNumber = getNextId(); + String requestId = String.format("req%d", requestIdNumber); + + JSONObject message = new JSONObject(); + JSONObject mediaCommand = new JSONObject(); + + try { + mediaCommand.put("type", "getPlayState"); + mediaCommand.put("requestId", requestId); + + message.put("contentType", "connectsdk.mediaCommand"); + message.put("mediaCommand", mediaCommand); + } catch (JSONException e) { + e.printStackTrace(); + } + + ResponseListener responseListener = new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + if (listener != null) { + Util.postError(listener, error); + } + } + + @Override + public void onSuccess(Object object) { + JSONObject responseObject = (JSONObject)object; + String playStateString = responseObject.optString("playState"); + PlayStateStatus playState = parsePlayState(playStateString); + + if (listener != null) { + Util.postSuccess(listener, playState); + } + } + }; + ServiceCommand> command = new ServiceCommand>(null, null, null, responseListener); + activeCommands.put(requestId, command); + + sendMessage(message.toString(), new ResponseListener() { + + @Override + public void onSuccess(Object object) { + // TODO Auto-generated method stub + + } + + @Override + public void onError(ServiceCommandError error) { + // TODO Auto-generated method stub + + } + }); + } + + @Override + public ServiceSubscription subscribePlayState(PlayStateListener listener) { + if (playStateSubscription == null) { + playStateSubscription = new URLServiceSubscription(null, null, null, null); + } + + if (mChannel == null || !mChannel.isConnected()) { + connect(null); + } + + playStateSubscription.addListener(listener); + + return playStateSubscription; + } + + public Application getApplication() { + return application; + } + + public void setApplication(Application application) { + this.application = application; + } +} From 5de895e8c1b83f915e60fe4386707d27b26075d1 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Sat, 9 Aug 2014 15:17:39 -0700 Subject: [PATCH 02/76] Fixed dependency issue with google play services --- Connect-SDK.iml | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Connect-SDK.iml b/Connect-SDK.iml index 1519593b..75e3c1f8 100644 --- a/Connect-SDK.iml +++ b/Connect-SDK.iml @@ -61,8 +61,8 @@ - + diff --git a/build.gradle b/build.gradle index 3c9ed64e..5519f9fb 100644 --- a/build.gradle +++ b/build.gradle @@ -27,5 +27,5 @@ dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:mediarouter-v7:19.1.0' compile 'com.android.support:appcompat-v7:19.1.0' - compile 'com.google.android.gms:play-services:5.0.77' + compile 'com.google.android.gms:play-services:5.+' } From 57750bbd7588a75e855d2af03e620494c7c7f650 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Sun, 10 Aug 2014 21:36:57 -0700 Subject: [PATCH 03/76] Fixed issue with using too-new Google Play Services --- Connect-SDK.iml | 3 ++- build.gradle | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Connect-SDK.iml b/Connect-SDK.iml index 75e3c1f8..f6c86334 100644 --- a/Connect-SDK.iml +++ b/Connect-SDK.iml @@ -62,8 +62,9 @@ - + + diff --git a/build.gradle b/build.gradle index 5519f9fb..71adc72a 100644 --- a/build.gradle +++ b/build.gradle @@ -27,5 +27,5 @@ dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:mediarouter-v7:19.1.0' compile 'com.android.support:appcompat-v7:19.1.0' - compile 'com.google.android.gms:play-services:5.+' + compile 'com.google.android.gms:play-services:5.0.+' } From 6e77a6a06f06f138b417388754f328aa0e138272 Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Tue, 12 Aug 2014 08:56:10 +0900 Subject: [PATCH 04/76] Added filtering for Samsung MultiScreenService to DiscoveryManager --- src/com/connectsdk/discovery/DiscoveryManager.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/com/connectsdk/discovery/DiscoveryManager.java b/src/com/connectsdk/discovery/DiscoveryManager.java index 59905bf4..9575a383 100644 --- a/src/com/connectsdk/discovery/DiscoveryManager.java +++ b/src/com/connectsdk/discovery/DiscoveryManager.java @@ -632,7 +632,7 @@ public boolean isNetcast(ServiceDescription description) { return isNetcastTV; } - public boolean isSamsungMultiScreen(Class deviceServiceClass, ServiceDescription description) { + public boolean isSamsungMultiScreen(ServiceDescription description) { boolean isSamsungMultiScreen = false; String locationXML = description.getLocationXML(); @@ -803,6 +803,9 @@ public void addServiceDescriptionToDevice(ServiceDescription desc, ConnectableDe } else if (deviceServiceClass == NetcastTVService.class) { if (!isNetcast(desc)) return; + } else if (deviceServiceClass == MultiScreenService.class){ + if (!isSamsungMultiScreen(desc)) + return; } if (deviceServiceClass == null) From 7e892a99fc09670b41639fa529c813117fb23a0e Mon Sep 17 00:00:00 2001 From: Simon Gladkoskok Date: Mon, 11 Aug 2014 22:04:58 -0700 Subject: [PATCH 05/76] Add class MediaInfo to MediaPlayer, refactor code to take mediaInfo object as a parameter for displayImage and playMedia methods. --- .../connectsdk/service/AirPlayService.java | 97 +++ src/com/connectsdk/service/CastService.java | 105 ++- src/com/connectsdk/service/DLNAService.java | 75 ++ .../service/MultiScreenService.java | 85 +++ .../connectsdk/service/NetcastTVService.java | 62 ++ src/com/connectsdk/service/RokuService.java | 677 +++++++++++------- .../connectsdk/service/WebOSTVService.java | 141 ++++ .../service/capability/MediaPlayer.java | 153 ++++ .../service/sessions/WebAppSession.java | 262 ++++--- 9 files changed, 1250 insertions(+), 407 deletions(-) diff --git a/src/com/connectsdk/service/AirPlayService.java b/src/com/connectsdk/service/AirPlayService.java index aee90491..9cadb9fa 100644 --- a/src/com/connectsdk/service/AirPlayService.java +++ b/src/com/connectsdk/service/AirPlayService.java @@ -348,7 +348,55 @@ public void onError(ServiceCommandError error) { } }); } + @Override + public void displayImage(final MediaInfo mediaInfo, final LaunchListener listener) { + Util.runInBackground(new Runnable() { + + @Override + public void run() { + ResponseListener responseListener = new ResponseListener() { + + @Override + public void onSuccess(Object response) { + LaunchSession launchSession = new LaunchSession(); + launchSession.setService(AirPlayService.this); + launchSession.setSessionType(LaunchSessionType.Media); + Util.postSuccess(listener, new MediaLaunchObject(launchSession, AirPlayService.this)); + } + + @Override + public void onError(ServiceCommandError error) { + Util.postError(listener, error); + } + }; + + String uri = getRequestURL("photo"); + HttpEntity entity = null; + + try { + URL imagePath = new URL(mediaInfo.url); + HttpURLConnection connection = (HttpURLConnection) imagePath.openConnection(); + connection.setDoInput(true); + connection.connect(); + InputStream input = connection.getInputStream(); + Bitmap myBitmap = BitmapFactory.decodeStream(input); + + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + myBitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream); + + entity = new ByteArrayEntity(stream.toByteArray()); + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + ServiceCommand> request = new ServiceCommand>(AirPlayService.this, uri, entity, responseListener); + request.send(); + } + }); + } public void playVideo(final String url, String mimeType, String title, String description, String iconSrc, boolean shouldLoop, final LaunchListener listener) { @@ -387,6 +435,43 @@ public void onError(ServiceCommandError error) { request.send(); } + public void playVideo(final MediaInfo mediaInfo, boolean shouldLoop, + final LaunchListener listener) { + + ResponseListener responseListener = new ResponseListener() { + + @Override + public void onSuccess(Object response) { + LaunchSession launchSession = new LaunchSession(); + launchSession.setService(AirPlayService.this); + launchSession.setSessionType(LaunchSessionType.Media); + + Util.postSuccess(listener, new MediaLaunchObject(launchSession, AirPlayService.this)); + } + + @Override + public void onError(ServiceCommandError error) { + Util.postError(listener, error); + } + }; + + String uri = getRequestURL("play"); + HttpEntity entity = null; + + PListBuilder builder = new PListBuilder(); + builder.putString("Content-Location", mediaInfo.getUrl()); + builder.putReal("Start-Position", 0); + + try { + entity = new StringEntity(builder.toString()); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + ServiceCommand> request = new ServiceCommand>(this, uri, entity, responseListener); + request.send(); + } + @Override public void playMedia(String url, String mimeType, String title, String description, String iconSrc, boolean shouldLoop, @@ -399,6 +484,18 @@ public void playMedia(String url, String mimeType, String title, playVideo(url, mimeType, title, description, iconSrc, shouldLoop, listener); } } + + @Override + public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, + LaunchListener listener) { + + if ( mediaInfo.getMimeType().contains("image") ) { + displayImage(mediaInfo, listener); + } + else { + playVideo(mediaInfo, shouldLoop, listener); + } + } @Override public void closeMedia(LaunchSession launchSession, diff --git a/src/com/connectsdk/service/CastService.java b/src/com/connectsdk/service/CastService.java index 15014a7a..a4150ead 100644 --- a/src/com/connectsdk/service/CastService.java +++ b/src/com/connectsdk/service/CastService.java @@ -421,7 +421,7 @@ public void onConnected() { } } - private void playMedia(final MediaInfo media, final LaunchListener listener) { + private void playMedia(final com.google.android.gms.cast.MediaInfo media, final LaunchListener listener) { if (media == null) { Util.postError(listener, new ServiceCommandError(500, "MediaInfo is null", null)); return; @@ -453,7 +453,7 @@ public void onResult(MediaChannelResult result) { Util.postSuccess(listener, new MediaLaunchObject(launchSession, CastService.this)); } else { - Util.postError(listener, new ServiceCommandError(result.getStatus().getStatusCode(), result.getStatus().getStatusMessage(), result)); + Util.postError(listener, new ServiceCommandError(result.getStatus().getStatusCode(), result.getStatus().toString(), result)); } } }); @@ -463,7 +463,7 @@ public void onResult(MediaChannelResult result) { runCommand(connectionListener); } - @Override + @Override public void displayImage(final String url, final String mimeType, final String title, final String description, final String iconSrc, final LaunchListener listener) { @@ -471,8 +471,6 @@ public void displayImage(final String url, final String mimeType, final String t @Override public void onConnected() { - String mediaAppId = CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID; - MediaMetadata mMediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_PHOTO); mMediaMetadata.putString(MediaMetadata.KEY_TITLE, title); mMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, description); @@ -483,30 +481,54 @@ public void onConnected() { mMediaMetadata.addImage(image); } - MediaInfo mediaInfo = new MediaInfo.Builder(url) + com.google.android.gms.cast.MediaInfo mediaInfo = new com.google.android.gms.cast.MediaInfo.Builder(url) .setContentType(mimeType) - .setStreamType(MediaInfo.STREAM_TYPE_NONE) + .setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_NONE) .setMetadata(mMediaMetadata) .build(); + + Cast.CastApi.launchApplication(mApiClient, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, false) + .setResultCallback(new ApplicationConnectionResultCallback(mediaInfo, listener)); + } + }; + + runCommand(connectionListener); + } + + @Override + public void displayImage(final MediaInfo mediaInfo, final LaunchListener listener) { + + ConnectionListener connectionListener = new ConnectionListener() { + + @Override + public void onConnected() { + MediaMetadata mMediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_PHOTO); + mMediaMetadata.putString(MediaMetadata.KEY_TITLE, mediaInfo.getTitle()); + mMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, mediaInfo.getDescription()); + + ImageInfo imageInfo = mediaInfo.allImages.get(0); - boolean relaunchIfRunning = false; - - if (Cast.CastApi.getApplicationStatus(mApiClient) != null && mediaAppId.equals(currentAppId)) { - relaunchIfRunning = false; - } - else { - relaunchIfRunning = true; + if ( imageInfo.url!= null) { + Uri iconUri = Uri.parse(imageInfo.url); + WebImage image = new WebImage(iconUri, 100, 100); + mMediaMetadata.addImage(image); } + + com.google.android.gms.cast.MediaInfo mediaInfo2 = new com.google.android.gms.cast.MediaInfo.Builder(mediaInfo.getUrl()) + .setContentType(mediaInfo.getMimeType()) + .setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_NONE) + .setMetadata(mMediaMetadata) + .build(); - Cast.CastApi.launchApplication(mApiClient, mediaAppId, relaunchIfRunning) - .setResultCallback(new ApplicationConnectionResultCallback(mediaInfo, listener)); + Cast.CastApi.launchApplication(mApiClient, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, false) + .setResultCallback(new ApplicationConnectionResultCallback(mediaInfo2, listener)); } }; runCommand(connectionListener); } - @Override + @Override public void playMedia(final String url, final String mimeType, final String title, final String description, final String iconSrc, final boolean shouldLoop, final LaunchListener listener) { @@ -515,8 +537,6 @@ public void playMedia(final String url, final String mimeType, final String titl @Override public void onConnected() { - String mediaAppId = CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID; - MediaMetadata mMediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); mMediaMetadata.putString(MediaMetadata.KEY_TITLE, title); mMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, description); @@ -527,23 +547,48 @@ public void onConnected() { mMediaMetadata.addImage(image); } - MediaInfo mediaInfo = new MediaInfo.Builder(url) + com.google.android.gms.cast.MediaInfo mediaInfo = new com.google.android.gms.cast.MediaInfo.Builder(url) .setContentType(mimeType) - .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) + .setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_BUFFERED) .setMetadata(mMediaMetadata) .build(); + + Cast.CastApi.launchApplication(mApiClient, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, false) + .setResultCallback(new ApplicationConnectionResultCallback(mediaInfo, listener)); + } + }; + + runCommand(connectionListener); + } + + @Override + public void playMedia(final MediaInfo mediaInfo, final boolean shouldLoop, + final LaunchListener listener) { + + ConnectionListener connectionListener = new ConnectionListener() { + + @Override + public void onConnected() { + MediaMetadata mMediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); + mMediaMetadata.putString(MediaMetadata.KEY_TITLE, mediaInfo.getTitle()); + mMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, mediaInfo.getDescription()); + - boolean relaunchIfRunning = false; - - if (Cast.CastApi.getApplicationStatus(mApiClient) != null && mediaAppId.equals(currentAppId)) { - relaunchIfRunning = false; - } - else { - relaunchIfRunning = true; + ImageInfo imageInfo = mediaInfo.allImages.get(0); + if (imageInfo.getUrl() != null) { + Uri iconUri = Uri.parse(imageInfo.getUrl()); + WebImage image = new WebImage(iconUri, 100, 100); + mMediaMetadata.addImage(image); } - Cast.CastApi.launchApplication(mApiClient, mediaAppId, relaunchIfRunning) - .setResultCallback(new ApplicationConnectionResultCallback(mediaInfo, listener)); + com.google.android.gms.cast.MediaInfo mediaInfo2 = new com.google.android.gms.cast.MediaInfo.Builder(mediaInfo.getUrl()) + .setContentType(mediaInfo.getMimeType()) + .setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_BUFFERED) + .setMetadata(mMediaMetadata) + .build(); + + Cast.CastApi.launchApplication(mApiClient, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, false) + .setResultCallback(new ApplicationConnectionResultCallback(mediaInfo2, listener)); } }; diff --git a/src/com/connectsdk/service/DLNAService.java b/src/com/connectsdk/service/DLNAService.java index 7d8d389e..3d59153a 100644 --- a/src/com/connectsdk/service/DLNAService.java +++ b/src/com/connectsdk/service/DLNAService.java @@ -195,11 +195,80 @@ public void onError(ServiceCommandError error) { request.send(); } + public void displayMedia(final MediaInfo mediaInfo, final LaunchListener listener) { + final String instanceId = "0"; + String[] mediaElements = mediaInfo.getMimeType().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 responseListener = new ResponseListener() { + + @Override + public void onSuccess(Object response) { + String method = "Play"; + + Map parameters = new HashMap(); + parameters.put("Speed", "1"); + + JSONObject payload = getMethodBody(instanceId, method, parameters); + + ResponseListener playResponseListener = new ResponseListener () { + @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)); + } + + @Override + public void onError(ServiceCommandError error) { + if ( listener != null ) { + listener.onError(error); + } + } + }; + + ServiceCommand> request = new ServiceCommand>(DLNAService.this, method, payload, playResponseListener); + request.send(); + } + + @Override + public void onError(ServiceCommandError error) { + if ( listener != null ) { + listener.onError(error); + } + } + }; + + String method = "SetAVTransportURI"; + JSONObject httpMessage = getSetAVTransportURIBody(method, instanceId, mediaInfo.getUrl(), mMimeType, mediaInfo.getTitle()); + + ServiceCommand> request = new ServiceCommand>(DLNAService.this, method, httpMessage, responseListener); + request.send(); + } + @Override public void displayImage(String url, String mimeType, String title, String description, String iconSrc, LaunchListener listener) { displayMedia(url, mimeType, title, description, iconSrc, listener); } + @Override + public void displayImage(MediaInfo mediaInfo, LaunchListener listener) { + + displayMedia(mediaInfo, listener); + + } + @Override public void playMedia(final String url, final String mimeType, final String title, final String description, final String iconSrc, final boolean shouldLoop, final LaunchListener listener) { displayMedia(url, mimeType, title, description, iconSrc, listener); @@ -271,6 +340,12 @@ public void playMedia(final String url, final String mimeType, final String titl // }); } + @Override + public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, + LaunchListener listener) { + + } + @Override public void closeMedia(LaunchSession launchSession, ResponseListener listener) { if (launchSession.getService() instanceof DLNAService) diff --git a/src/com/connectsdk/service/MultiScreenService.java b/src/com/connectsdk/service/MultiScreenService.java index 07eb7035..a6c10bcc 100644 --- a/src/com/connectsdk/service/MultiScreenService.java +++ b/src/com/connectsdk/service/MultiScreenService.java @@ -159,6 +159,48 @@ public void onSuccess(Object object) { }); } + @Override + public void displayImage(final MediaInfo mediaInfo, final LaunchListener listener) { + final String webAppId = "ConnectSDKSampler"; + + getWebAppLauncher().joinWebApp(webAppId, new WebAppSession.LaunchListener() { + + @Override + public void onSuccess(WebAppSession webAppSession) { + webAppSession.getMediaPlayer().displayImage(mediaInfo, listener); + } + + @Override + public void onError(ServiceCommandError error) { + getWebAppLauncher().launchWebApp(webAppId, new WebAppSession.LaunchListener() { + + @Override + public void onError(ServiceCommandError error) { + if (listener != null) { + Util.postError(listener, error); + } + } + + @Override + public void onSuccess(final WebAppSession webAppSession) { + webAppSession.connect(new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + Util.postError(listener, error); + } + + @Override + public void onSuccess(Object object) { + webAppSession.getMediaPlayer().displayImage(mediaInfo, listener); + } + }); + } + }); + } + }); + } + @Override public void playMedia(final String url, final String mimeType, final String title, final String description, final String iconSrc, final boolean shouldLoop, @@ -202,6 +244,49 @@ public void onSuccess(Object object) { } }); } + + @Override + public void playMedia(final MediaInfo mediaInfo, final boolean shouldLoop, + final LaunchListener listener) { + final String webAppId = "ConnectSDKSampler"; + + getWebAppLauncher().joinWebApp(webAppId, new WebAppSession.LaunchListener() { + + @Override + public void onSuccess(WebAppSession webAppSession) { + webAppSession.playMedia(mediaInfo, shouldLoop, listener); + } + + @Override + public void onError(ServiceCommandError error) { + getWebAppLauncher().launchWebApp(webAppId, new WebAppSession.LaunchListener() { + + @Override + public void onError(ServiceCommandError error) { + if (listener != null) { + Util.postError(listener, error); + } + } + + @Override + public void onSuccess(final WebAppSession webAppSession) { + webAppSession.connect(new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + Util.postError(listener, error); + } + + @Override + public void onSuccess(Object object) { + webAppSession.playMedia(mediaInfo, shouldLoop, listener); + } + }); + } + }); + } + }); + } @Override public void closeMedia(LaunchSession launchSession, diff --git a/src/com/connectsdk/service/NetcastTVService.java b/src/com/connectsdk/service/NetcastTVService.java index 40e04dd5..dcf575f4 100644 --- a/src/com/connectsdk/service/NetcastTVService.java +++ b/src/com/connectsdk/service/NetcastTVService.java @@ -1475,6 +1475,38 @@ public void onSuccess(MediaLaunchObject object) { } } + @Override + public void displayImage(final MediaInfo mediaInfo, final LaunchListener listener) { + + if ( dlnaService != null ) { + final MediaPlayer.LaunchListener launchListener = new LaunchListener() { + + @Override + public void onError(ServiceCommandError error) { + if (listener != null) + Util.postError(listener, error); + } + + @Override + public void onSuccess(MediaLaunchObject object) { + object.launchSession.setAppId("SmartShareª"); + object.launchSession.setAppName("SmartShareª"); + + object.mediaControl = NetcastTVService.this.getMediaControl(); + + if (listener != null) + Util.postSuccess(listener, object); + } + }; + + getDLNAService().displayImage(mediaInfo, launchListener); + } + else { + System.err.println("DLNA Service is not ready yet"); + } + + } + @Override public void playMedia(final String url, final String mimeType, final String title, final String description, final String iconSrc, final boolean shouldLoop, final MediaPlayer.LaunchListener listener) { if ( getDLNAService() != null ) { @@ -1505,6 +1537,36 @@ public void onSuccess(MediaLaunchObject object) { } } + @Override + public void playMedia(final MediaInfo mediaInfo, final boolean shouldLoop, final MediaPlayer.LaunchListener listener) { + if ( getDLNAService() != null ) { + final MediaPlayer.LaunchListener launchListener = new LaunchListener() { + + @Override + public void onError(ServiceCommandError error) { + if (listener != null) + Util.postError(listener, error); + } + + @Override + public void onSuccess(MediaLaunchObject object) { + object.launchSession.setAppId("SmartShareª"); + object.launchSession.setAppName("SmartShareª"); + + object.mediaControl = NetcastTVService.this.getMediaControl(); + + if (listener != null) + Util.postSuccess(listener, object); + } + }; + + getDLNAService().playMedia(mediaInfo, shouldLoop, launchListener); + } + else { + System.err.println("DLNA Service is not ready yet"); + } + } + @Override public void closeMedia(LaunchSession launchSession, ResponseListener listener) { if (getDLNAService() == null) { diff --git a/src/com/connectsdk/service/RokuService.java b/src/com/connectsdk/service/RokuService.java index 3f7d591a..074aba01 100644 --- a/src/com/connectsdk/service/RokuService.java +++ b/src/com/connectsdk/service/RokuService.java @@ -74,12 +74,13 @@ import com.connectsdk.service.config.ServiceDescription; import com.connectsdk.service.sessions.LaunchSession; -public class RokuService extends DeviceService implements Launcher, MediaPlayer, MediaControl, KeyControl, TextInputControl { - +public class RokuService extends DeviceService implements Launcher, + MediaPlayer, MediaControl, KeyControl, TextInputControl { + public static final String ID = "Roku"; private static List registeredApps = new ArrayList(); - + DIALService dialService; static { @@ -87,7 +88,7 @@ public class RokuService extends DeviceService implements Launcher, MediaPlayer, registeredApps.add("Netflix"); registeredApps.add("Amazon"); } - + public static void registerApp(String appId) { if (!registeredApps.contains(appId)) registeredApps.add(appId); @@ -95,31 +96,33 @@ public static void registerApp(String appId) { HttpClient httpClient; - public RokuService(ServiceDescription serviceDescription, ServiceConfig serviceConfig) { + public RokuService(ServiceDescription serviceDescription, + ServiceConfig serviceConfig) { super(serviceDescription, serviceConfig); - + httpClient = new DefaultHttpClient(); ClientConnectionManager mgr = httpClient.getConnectionManager(); HttpParams params = httpClient.getParams(); - httpClient = new DefaultHttpClient(new ThreadSafeClientConnManager(params, mgr.getSchemeRegistry()), params); + httpClient = new DefaultHttpClient(new ThreadSafeClientConnManager( + params, mgr.getSchemeRegistry()), params); } - + @Override public void setServiceDescription(ServiceDescription serviceDescription) { super.setServiceDescription(serviceDescription); - - if (this.serviceDescription != null) + + if (this.serviceDescription != null) this.serviceDescription.setPort(8060); - + probeForAppSupport(); } public static JSONObject discoveryParameters() { JSONObject params = new JSONObject(); - + try { params.put("serviceId", ID); - params.put("filter", "roku:ecp"); + params.put("filter", "roku:ecp"); } catch (JSONException e) { e.printStackTrace(); } @@ -127,7 +130,6 @@ public static JSONObject discoveryParameters() { return params; } - @Override public Launcher getLauncher() { return this; @@ -137,30 +139,31 @@ public Launcher getLauncher() { public CapabilityPriorityLevel getLauncherCapabilityLevel() { return CapabilityPriorityLevel.HIGH; } - + class RokuLaunchSession extends LaunchSession { String appName; RokuService service; - - RokuLaunchSession (RokuService service) { + + RokuLaunchSession(RokuService service) { this.service = service; } - - RokuLaunchSession (RokuService service, String appId, String appName) { + + RokuLaunchSession(RokuService service, String appId, String appName) { this.service = service; this.appId = appId; this.appName = appName; } - - RokuLaunchSession (RokuService service, JSONObject obj) throws JSONException { + + RokuLaunchSession(RokuService service, JSONObject obj) + throws JSONException { this.service = service; fromJSONObject(obj); } - + public void close(ResponseListener responseListener) { home(responseListener); } - + @Override public JSONObject toJSONObject() throws JSONException { JSONObject obj = super.toJSONObject(); @@ -168,119 +171,130 @@ public JSONObject toJSONObject() throws JSONException { obj.put("appName", appName); return obj; } - + @Override public void fromJSONObject(JSONObject obj) throws JSONException { super.fromJSONObject(obj); appName = obj.optString("appName"); } } - + @Override public void launchApp(String appId, AppLaunchListener listener) { if (appId == null) { - Util.postError(listener, new ServiceCommandError(0, "Must supply a valid app id", null)); + Util.postError(listener, new ServiceCommandError(0, + "Must supply a valid app id", null)); return; } - + AppInfo appInfo = new AppInfo(); appInfo.setId(appId); - + launchAppWithInfo(appInfo, listener); } @Override - public void launchAppWithInfo(AppInfo appInfo, Launcher.AppLaunchListener listener) { + public void launchAppWithInfo(AppInfo appInfo, + Launcher.AppLaunchListener listener) { launchAppWithInfo(appInfo, null, listener); } @Override - public void launchAppWithInfo(final AppInfo appInfo, Object params, final Launcher.AppLaunchListener listener) { - if (appInfo == null || appInfo.getId() == null) - { + public void launchAppWithInfo(final AppInfo appInfo, Object params, + final Launcher.AppLaunchListener listener) { + if (appInfo == null || appInfo.getId() == null) { if (listener != null) - Util.postError(listener, new ServiceCommandError(-1, "Cannot launch app without valid AppInfo object", appInfo)); - + Util.postError(listener, new ServiceCommandError(-1, + "Cannot launch app without valid AppInfo object", + appInfo)); + return; } - + String baseTargetURL = requestURL("launch", appInfo.getId()); String queryParams = ""; - - if (params != null && params instanceof JSONObject) - { + + if (params != null && params instanceof JSONObject) { JSONObject jsonParams = (JSONObject) params; - + int count = 0; Iterator jsonIterator = jsonParams.keys(); - + while (jsonIterator.hasNext()) { String key = (String) jsonIterator.next(); String value = null; - - try { value = jsonParams.getString(key); } catch (JSONException ex) { } - - if (value == null) continue; - + + try { + value = jsonParams.getString(key); + } catch (JSONException ex) { + } + + if (value == null) + continue; + String urlSafeKey = null; String urlSafeValue = null; String prefix = (count == 0) ? "?" : "&"; - + try { urlSafeKey = URLEncoder.encode(key, "UTF-8"); urlSafeValue = URLEncoder.encode(value, "UTF-8"); } catch (UnsupportedEncodingException ex) { - + } - + if (urlSafeKey == null || urlSafeValue == null) continue; - + String appendString = prefix + urlSafeKey + "=" + urlSafeValue; queryParams = queryParams + appendString; - + count++; } } - + String targetURL = null; - + if (queryParams.length() > 0) targetURL = baseTargetURL + queryParams; else targetURL = baseTargetURL; - + ResponseListener responseListener = new ResponseListener() { - + @Override public void onSuccess(Object response) { - Util.postSuccess(listener, new RokuLaunchSession(RokuService.this, appInfo.getId(), appInfo.getName())); + Util.postSuccess(listener, new RokuLaunchSession( + RokuService.this, appInfo.getId(), appInfo.getName())); } - + @Override public void onError(ServiceCommandError error) { Util.postError(listener, error); } }; - - ServiceCommand> request = new ServiceCommand>(this, targetURL, null, responseListener); - request.send(); + + ServiceCommand> request = new ServiceCommand>( + this, targetURL, null, responseListener); + request.send(); } @Override - public void closeApp(LaunchSession launchSession, ResponseListener listener) { + public void closeApp(LaunchSession launchSession, + ResponseListener listener) { home(listener); } @Override public void getAppList(final AppListListener listener) { ResponseListener responseListener = new ResponseListener() { - + @Override public void onSuccess(Object response) { - String msg = (String)response; - - SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); + String msg = (String) response; + + SAXParserFactory saxParserFactory = SAXParserFactory + .newInstance(); InputStream stream; try { stream = new ByteArrayInputStream(msg.getBytes("UTF-8")); @@ -288,9 +302,9 @@ public void onSuccess(Object response) { RokuApplicationListParser parser = new RokuApplicationListParser(); saxParser.parse(stream, parser); - + List appList = parser.getApplicationList(); - + Util.postSuccess(listener, appList); } catch (UnsupportedEncodingException e) { e.printStackTrace(); @@ -302,19 +316,20 @@ public void onSuccess(Object response) { e.printStackTrace(); } } - + @Override public void onError(ServiceCommandError error) { Util.postError(listener, error); } }; - + String action = "query"; String param = "apps"; - + String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, responseListener); + + ServiceCommand> request = new ServiceCommand>( + this, uri, null, responseListener); request.setHttpMethod(ServiceCommand.TYPE_GET); request.send(); } @@ -325,19 +340,22 @@ public void getRunningApp(AppInfoListener listener) { } @Override - public ServiceSubscription subscribeRunningApp(AppInfoListener listener) { + public ServiceSubscription subscribeRunningApp( + AppInfoListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); return new NotSupportedServiceSubscription(); } - + @Override - public void getAppState(LaunchSession launchSession, AppStateListener listener) { + public void getAppState(LaunchSession launchSession, + AppStateListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); } - + @Override - public ServiceSubscription subscribeAppState(LaunchSession launchSession, AppStateListener listener) { + public ServiceSubscription subscribeAppState( + LaunchSession launchSession, AppStateListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); return null; @@ -349,34 +367,40 @@ public void launchBrowser(String url, Launcher.AppLaunchListener listener) { } @Override - public void launchYouTube(String contentId, Launcher.AppLaunchListener listener) { - launchYouTube(contentId, (float)0.0, listener); + public void launchYouTube(String contentId, + Launcher.AppLaunchListener listener) { + launchYouTube(contentId, (float) 0.0, listener); } - + @Override - public void launchYouTube(String contentId, float startTime, AppLaunchListener listener) { + public void launchYouTube(String contentId, float startTime, + AppLaunchListener listener) { if (getDIALService() != null) { - getDIALService().getLauncher().launchYouTube(contentId, startTime, listener); - } - else { + getDIALService().getLauncher().launchYouTube(contentId, startTime, + listener); + } else { if (listener != null) { - listener.onError(new ServiceCommandError(0, "Cannot reach DIAL service for launching with provided start time", null)); + listener.onError(new ServiceCommandError( + 0, + "Cannot reach DIAL service for launching with provided start time", + null)); } } } @Override - public void launchNetflix(final String contentId, final Launcher.AppLaunchListener listener) { + public void launchNetflix(final String contentId, + final Launcher.AppLaunchListener listener) { getAppList(new AppListListener() { - + @Override public void onSuccess(List appList) { - for (AppInfo appInfo: appList) { - if ( appInfo.getName().equalsIgnoreCase("Netflix") ) { + for (AppInfo appInfo : appList) { + if (appInfo.getName().equalsIgnoreCase("Netflix")) { JSONObject payload = new JSONObject(); try { payload.put("mediaType", "movie"); - + if (contentId != null && contentId.length() > 0) payload.put("contentId", contentId); } catch (JSONException e) { @@ -387,7 +411,7 @@ public void onSuccess(List appList) { } } } - + @Override public void onError(ServiceCommandError error) { Util.postError(listener, error); @@ -396,13 +420,14 @@ public void onError(ServiceCommandError error) { } @Override - public void launchHulu(final String contentId, final Launcher.AppLaunchListener listener) { + public void launchHulu(final String contentId, + final Launcher.AppLaunchListener listener) { getAppList(new AppListListener() { - + @Override public void onSuccess(List appList) { - for (AppInfo appInfo: appList) { - if ( appInfo.getName().contains("Hulu") ) { + for (AppInfo appInfo : appList) { + if (appInfo.getName().contains("Hulu")) { JSONObject payload = new JSONObject(); try { payload.put("contentId", contentId); @@ -414,32 +439,34 @@ public void onSuccess(List appList) { } } } - + @Override public void onError(ServiceCommandError error) { Util.postError(listener, error); } - }); + }); } - + @Override public void launchAppStore(final String appId, AppLaunchListener listener) { AppInfo appInfo = new AppInfo("11"); appInfo.setName("Channel Store"); - + JSONObject params = null; try { - params = new JSONObject() {{ - put("contentId", appId); - }}; + params = new JSONObject() { + { + put("contentId", appId); + } + }; } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } - + launchAppWithInfo(appInfo, params, listener); } - + @Override public KeyControl getKeyControl() { return this; @@ -454,10 +481,11 @@ public CapabilityPriorityLevel getKeyControlCapabilityLevel() { public void up(ResponseListener listener) { String action = "keypress"; String param = "Up"; - + String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); + + ServiceCommand> request = new ServiceCommand>( + this, uri, null, listener); request.send(); } @@ -465,10 +493,11 @@ public void up(ResponseListener listener) { public void down(final ResponseListener listener) { String action = "keypress"; String param = "Down"; - + String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); + + ServiceCommand> request = new ServiceCommand>( + this, uri, null, listener); request.send(); } @@ -476,10 +505,11 @@ public void down(final ResponseListener listener) { public void left(ResponseListener listener) { String action = "keypress"; String param = "Left"; - + String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); + + ServiceCommand> request = new ServiceCommand>( + this, uri, null, listener); request.send(); } @@ -487,10 +517,11 @@ public void left(ResponseListener listener) { public void right(ResponseListener listener) { String action = "keypress"; String param = "Right"; - + String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); + + ServiceCommand> request = new ServiceCommand>( + this, uri, null, listener); request.send(); } @@ -498,10 +529,11 @@ public void right(ResponseListener listener) { public void ok(final ResponseListener listener) { String action = "keypress"; String param = "Select"; - + String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); + + ServiceCommand> request = new ServiceCommand>( + this, uri, null, listener); request.send(); } @@ -509,10 +541,11 @@ public void ok(final ResponseListener listener) { public void back(ResponseListener listener) { String action = "keypress"; String param = "Back"; - + String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); + + ServiceCommand> request = new ServiceCommand>( + this, uri, null, listener); request.send(); } @@ -520,14 +553,14 @@ public void back(ResponseListener listener) { public void home(ResponseListener listener) { String action = "keypress"; String param = "Home"; - + String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); + + ServiceCommand> request = new ServiceCommand>( + this, uri, null, listener); request.send(); } - @Override public MediaControl getMediaControl() { return this; @@ -542,21 +575,23 @@ public CapabilityPriorityLevel getMediaControlCapabilityLevel() { public void play(ResponseListener listener) { String action = "keypress"; String param = "Play"; - + String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); - request.send(); + + ServiceCommand> request = new ServiceCommand>( + this, uri, null, listener); + request.send(); } @Override public void pause(ResponseListener listener) { String action = "keypress"; String param = "Play"; - + String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); + + ServiceCommand> request = new ServiceCommand>( + this, uri, null, listener); request.send(); } @@ -564,10 +599,11 @@ public void pause(ResponseListener listener) { public void stop(ResponseListener listener) { String action = null; String param = "input?a=sto"; - + String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); + + ServiceCommand> request = new ServiceCommand>( + this, uri, null, listener); request.send(); } @@ -575,39 +611,41 @@ public void stop(ResponseListener listener) { public void rewind(ResponseListener listener) { String action = "keypress"; String param = "Rev"; - + String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); - request.send(); + + ServiceCommand> request = new ServiceCommand>( + this, uri, null, listener); + request.send(); } @Override public void fastForward(ResponseListener listener) { String action = "keypress"; String param = "Fwd"; - + String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); - request.send(); + + ServiceCommand> request = new ServiceCommand>( + this, uri, null, listener); + request.send(); } - + @Override public void getDuration(DurationListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); } - + @Override public void getPosition(PositionListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); } - + @Override public void seek(long position, ResponseListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); } - + @Override public MediaPlayer getMediaPlayer() { return this; @@ -618,73 +656,145 @@ public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { return CapabilityPriorityLevel.NORMAL; } - private void displayMedia(String url, String mimeType, String title, String description, String iconSrc, final MediaPlayer.LaunchListener listener) { + private void displayMedia(String url, String mimeType, String title, + String description, String iconSrc, + final MediaPlayer.LaunchListener listener) { ResponseListener responseListener = new ResponseListener() { - + @Override public void onSuccess(Object response) { - Util.postSuccess(listener, new MediaLaunchObject(new RokuLaunchSession(RokuService.this), RokuService.this)); + Util.postSuccess(listener, new MediaLaunchObject( + new RokuLaunchSession(RokuService.this), + RokuService.this)); } - + @Override public void onError(ServiceCommandError error) { Util.postError(listener, error); } }; - - String host = String.format("%s:%s", serviceDescription.getIpAddress(), serviceDescription.getPort()); + + String host = String.format("%s:%s", serviceDescription.getIpAddress(), + serviceDescription.getPort()); String action = "input"; String mediaFormat = mimeType; if (mimeType.contains("/")) { - int index = mimeType.indexOf("/") + 1; + int index = mimeType.indexOf("/") + 1; mediaFormat = mimeType.substring(index); } - + String param; if (mimeType.contains("image")) { - param = String.format("15985?t=p&u=%s&h=%s&tr=crossfade", - HttpMessage.encode(url), - HttpMessage.encode(host)); + param = String.format("15985?t=p&u=%s&h=%s&tr=crossfade", + HttpMessage.encode(url), HttpMessage.encode(host)); + } else if (mimeType.contains("video")) { + param = String.format( + "15985?t=v&u=%s&k=(null)&h=%s&videoName=%s&videoFormat=%s", + HttpMessage.encode(url), HttpMessage.encode(host), + HttpMessage.encode(title), HttpMessage.encode(mediaFormat)); + } else { // if (mimeType.contains("audio")) { + param = String + .format("15985?t=a&u=%s&k=(null)&h=%s&songname=%s&artistname=%s&songformat=%s&albumarturl=%s", + HttpMessage.encode(url), HttpMessage.encode(host), + HttpMessage.encode(title), + HttpMessage.encode(description), + HttpMessage.encode(mediaFormat), + HttpMessage.encode(iconSrc)); } - else if (mimeType.contains("video")) { - param = String.format("15985?t=v&u=%s&k=(null)&h=%s&videoName=%s&videoFormat=%s", - HttpMessage.encode(url), - HttpMessage.encode(host), - HttpMessage.encode(title), - HttpMessage.encode(mediaFormat)); + + String uri = requestURL(action, param); + + ServiceCommand> request = new ServiceCommand>( + this, uri, null, responseListener); + request.send(); + } + + private void displayMedia(MediaInfo mediaInfo, + final MediaPlayer.LaunchListener listener) { + ResponseListener responseListener = new ResponseListener() { + + @Override + public void onSuccess(Object response) { + Util.postSuccess(listener, new MediaLaunchObject( + new RokuLaunchSession(RokuService.this), + RokuService.this)); + } + + @Override + public void onError(ServiceCommandError error) { + Util.postError(listener, error); + } + }; + + String host = String.format("%s:%s", serviceDescription.getIpAddress(), + serviceDescription.getPort()); + + String action = "input"; + String mediaFormat = mediaInfo.getMimeType(); + if (mediaInfo.getMimeType().contains("/")) { + int index = mediaInfo.getMimeType().indexOf("/") + 1; + mediaFormat = mediaInfo.getMimeType().substring(index); } - else { // if (mimeType.contains("audio")) { - param = String.format("15985?t=a&u=%s&k=(null)&h=%s&songname=%s&artistname=%s&songformat=%s&albumarturl=%s", - HttpMessage.encode(url), - HttpMessage.encode(host), - HttpMessage.encode(title), - HttpMessage.encode(description), - HttpMessage.encode(mediaFormat), - HttpMessage.encode(iconSrc)); + + String param; + if (mediaInfo.getMimeType().contains("image")) { + param = String.format("15985?t=p&u=%s&h=%s&tr=crossfade", + HttpMessage.encode(mediaInfo.getUrl()), HttpMessage.encode(host)); + } else if (mediaInfo.getMimeType().contains("video")) { + param = String.format( + "15985?t=v&u=%s&k=(null)&h=%s&videoName=%s&videoFormat=%s", + HttpMessage.encode(mediaInfo.getUrl()), HttpMessage.encode(host), + HttpMessage.encode(mediaInfo.getTitle()), HttpMessage.encode(mediaFormat)); + } else { // if (mimeType.contains("audio")) { + param = String + .format("15985?t=a&u=%s&k=(null)&h=%s&songname=%s&artistname=%s&songformat=%s&albumarturl=%s", + HttpMessage.encode(mediaInfo.getUrl()), HttpMessage.encode(host), + HttpMessage.encode(mediaInfo.getTitle()), + HttpMessage.encode(mediaInfo.getDescription()), + HttpMessage.encode(mediaFormat), + HttpMessage.encode(mediaInfo.getImages().get(0).getUrl())); } String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, responseListener); - request.send(); + + ServiceCommand> request = new ServiceCommand>( + this, uri, null, responseListener); + request.send(); } - + @Override - public void displayImage(String url, String mimeType, String title, String description, String iconSrc, MediaPlayer.LaunchListener listener) { + public void displayImage(String url, String mimeType, String title, + String description, String iconSrc, + MediaPlayer.LaunchListener listener) { displayMedia(url, mimeType, title, description, iconSrc, listener); } @Override - public void playMedia(String url, String mimeType, String title, String description, String iconSrc, boolean shouldLoop, MediaPlayer.LaunchListener listener) { + public void displayImage(MediaInfo mediaInfo, + MediaPlayer.LaunchListener listener) { + displayMedia(mediaInfo, listener); + } + + @Override + public void playMedia(String url, String mimeType, String title, + String description, String iconSrc, boolean shouldLoop, + MediaPlayer.LaunchListener listener) { displayMedia(url, mimeType, title, description, iconSrc, listener); } + + @Override + public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, + MediaPlayer.LaunchListener listener) { + displayMedia(mediaInfo, listener); + } @Override - public void closeMedia(LaunchSession launchSession, ResponseListener listener) { + public void closeMedia(LaunchSession launchSession, + ResponseListener listener) { home(listener); } - + @Override public TextInputControl getTextInputControl() { return this; @@ -696,7 +806,8 @@ public CapabilityPriorityLevel getTextInputControlCapabilityLevel() { } @Override - public ServiceSubscription subscribeTextInputStatus(TextInputStatusListener listener) { + public ServiceSubscription subscribeTextInputStatus( + TextInputStatusListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); return new NotSupportedServiceSubscription(); @@ -704,25 +815,25 @@ public ServiceSubscription subscribeTextInputStatus(Tex @Override public void sendText(String input) { - if ( input == null || input.length() == 0 ) { + if (input == null || input.length() == 0) { return; } - + ResponseListener listener = new ResponseListener() { - + @Override public void onSuccess(Object response) { // TODO Auto-generated method stub - + } - + @Override public void onError(ServiceCommandError error) { // TODO Auto-generated method stub - + } }; - + String action = "keypress"; String param = null; try { @@ -731,15 +842,16 @@ public void onError(ServiceCommandError error) { // This can be safetly ignored since it isn't a dynamic encoding. e.printStackTrace(); } - + String uri = requestURL(action, param); - + Log.d("Connect SDK", "RokuService::send() | uri = " + uri); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); - request.send(); + + ServiceCommand> request = new ServiceCommand>( + this, uri, null, listener); + request.send(); } - + @Override public void sendKeyCode(int keyCode, ResponseListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); @@ -748,73 +860,77 @@ public void sendKeyCode(int keyCode, ResponseListener listener) { @Override public void sendEnter() { ResponseListener listener = new ResponseListener() { - + @Override public void onSuccess(Object response) { // TODO Auto-generated method stub - + } - + @Override public void onError(ServiceCommandError error) { // TODO Auto-generated method stub - + } }; - + String action = "keypress"; String param = "Enter"; - + String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); - request.send(); + + ServiceCommand> request = new ServiceCommand>( + this, uri, null, listener); + request.send(); } @Override public void sendDelete() { ResponseListener listener = new ResponseListener() { - + @Override public void onSuccess(Object response) { // TODO Auto-generated method stub - + } - + @Override public void onError(ServiceCommandError error) { // TODO Auto-generated method stub - + } }; - + String action = "keypress"; String param = "Backspace"; - + String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); - request.send(); + + ServiceCommand> request = new ServiceCommand>( + this, uri, null, listener); + request.send(); } - + @Override - public void unsubscribe(URLServiceSubscription subscription) { } - + public void unsubscribe(URLServiceSubscription subscription) { + } + @Override public void sendCommand(final ServiceCommand mCommand) { Thread thread = new Thread(new Runnable() { - + @SuppressWarnings("unchecked") @Override public void run() { ServiceCommand> command = (ServiceCommand>) mCommand; Object payload = command.getPayload(); - + HttpRequestBase request = command.getRequest(); HttpResponse response = null; int code = -1; - if (command.getHttpMethod().equalsIgnoreCase(ServiceCommand.TYPE_POST)) { + if (command.getHttpMethod().equalsIgnoreCase( + ServiceCommand.TYPE_POST)) { HttpPost post = (HttpPost) request; AbstractHttpEntity entity = null; @@ -825,33 +941,40 @@ public void run() { } } catch (UnsupportedEncodingException e) { e.printStackTrace(); - // Error is handled below if entity is null; + // Error is handled below if entity is null; } - + if (entity == null) { - Util.postError(command.getResponseListener(), new ServiceCommandError(0, "Unknown Error while preparing to send message", null)); - + Util.postError( + command.getResponseListener(), + new ServiceCommandError( + 0, + "Unknown Error while preparing to send message", + null)); + return; } - + post.setEntity(entity); } } - + try { if (httpClient != null) { response = httpClient.execute(request); - + code = response.getStatusLine().getStatusCode(); - - if ( code == 200 || code == 201) { - HttpEntity entity = response.getEntity(); - String message = EntityUtils.toString(entity, "UTF-8"); - Util.postSuccess(command.getResponseListener(), message); - } - else { - Util.postError(command.getResponseListener(), ServiceCommandError.getError(code)); + if (code == 200 || code == 201) { + HttpEntity entity = response.getEntity(); + String message = EntityUtils.toString(entity, + "UTF-8"); + + Util.postSuccess(command.getResponseListener(), + message); + } else { + Util.postError(command.getResponseListener(), + ServiceCommandError.getError(code)); } } } catch (ClientProtocolException e) { @@ -863,33 +986,34 @@ public void run() { }); thread.start(); } - + private String requestURL(String action, String parameter) { StringBuilder sb = new StringBuilder(); - + sb.append("http://"); sb.append(serviceDescription.getIpAddress()).append(":"); sb.append(serviceDescription.getPort()).append("/"); - - if ( action != null ) + + if (action != null) sb.append(action); - - if ( parameter != null ) + + if (parameter != null) sb.append("/").append(parameter); - + return sb.toString(); } - + private void probeForAppSupport() { getAppList(new AppListListener() { - + @Override - public void onError(ServiceCommandError error) { } - + public void onError(ServiceCommandError error) { + } + @Override public void onSuccess(List object) { List appsToAdd = new ArrayList(); - + for (String probe : registeredApps) { for (AppInfo app : object) { if (app.getName().contains(probe)) { @@ -898,20 +1022,22 @@ public void onSuccess(List object) { } } } - + addCapabilities(appsToAdd); } }); } - + @Override protected void updateCapabilities() { List capabilities = new ArrayList(); - - for (String capability : KeyControl.Capabilities) { capabilities.add(capability); } - + + for (String capability : KeyControl.Capabilities) { + capabilities.add(capability); + } + capabilities.add(Application); - + capabilities.add(Application_Params); capabilities.add(Application_List); capabilities.add(AppStore); @@ -932,7 +1058,7 @@ protected void updateCapabilities() { capabilities.add(Send); capabilities.add(Send_Delete); capabilities.add(Send_Enter); - + setCapabilities(capabilities); } @@ -942,42 +1068,46 @@ public void getPlayState(PlayStateListener listener) { } @Override - public ServiceSubscription subscribePlayState(PlayStateListener listener) { + public ServiceSubscription subscribePlayState( + PlayStateListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); return null; } - + @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(); - + // 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); } - + @Override public void disconnect() { connected = false; - + if (mServiceReachability != null) mServiceReachability.stop(); - + Util.runOnUI(new Runnable() { - + @Override public void run() { if (listener != null) @@ -985,7 +1115,7 @@ public void run() { } }); } - + @Override public void onLoseReachability(DeviceServiceReachability reachability) { if (connected) { @@ -995,26 +1125,27 @@ public void onLoseReachability(DeviceServiceReachability reachability) { mServiceReachability.stop(); } } - + public DIALService getDIALService() { if (dialService == null) { DiscoveryManager discoveryManager = DiscoveryManager.getInstance(); - ConnectableDevice device = discoveryManager.getAllDevices().get(serviceDescription.getIpAddress()); + ConnectableDevice device = discoveryManager.getAllDevices().get( + serviceDescription.getIpAddress()); if (device != null) { DIALService foundService = null; - - for (DeviceService service: device.getServices()) { + + for (DeviceService service : device.getServices()) { if (DIALService.class.isAssignableFrom(service.getClass())) { - foundService = (DIALService)service; + foundService = (DIALService) service; break; } } dialService = foundService; - } + } } - + return dialService; } } diff --git a/src/com/connectsdk/service/WebOSTVService.java b/src/com/connectsdk/service/WebOSTVService.java index 32de6482..3cdb7e49 100644 --- a/src/com/connectsdk/service/WebOSTVService.java +++ b/src/com/connectsdk/service/WebOSTVService.java @@ -1203,6 +1203,76 @@ public void onSuccess(WebAppSession webAppSession) { }); } } + + @Override + public void displayImage(final MediaInfo mediaInfo, + final MediaPlayer.LaunchListener listener) { + if ("4.0.0".equalsIgnoreCase(this.serviceDescription.getVersion())) { + DeviceService dlnaService = this.getDLNAService(); + + if (dlnaService != null) { + MediaPlayer mediaPlayer = dlnaService.getAPI(MediaPlayer.class); + + if (mediaPlayer != null) { + mediaPlayer.displayImage(mediaInfo, listener); + return; + } + } + + JSONObject params = null; + + try { + params = new JSONObject() { + { + put("target", mediaInfo.getUrl()); + put("title", mediaInfo.getTitle() == null ? NULL : mediaInfo.getTitle()); + put("description", mediaInfo.getDescription() == null ? NULL + : mediaInfo.getDescription()); + put("mimeType", mediaInfo.getMimeType() == null ? NULL : mediaInfo.getMimeType()); + put("iconSrc", mediaInfo.getImages().get(0).getUrl() == null ? NULL : mediaInfo.getImages().get(0).getUrl()); + } + }; + } catch (JSONException ex) { + ex.printStackTrace(); + Util.postError(listener, + new ServiceCommandError(-1, ex.getLocalizedMessage(), + ex)); + } + + if (params != null) + this.displayMedia(params, listener); + } else { + final String webAppId = "MediaPlayer"; + + final WebAppSession.LaunchListener webAppLaunchListener = new WebAppSession.LaunchListener() { + + @Override + public void onError(ServiceCommandError error) { + listener.onError(error); + } + + @Override + public void onSuccess(WebAppSession webAppSession) { + webAppSession.displayImage(mediaInfo, listener); + } + }; + + this.getWebAppLauncher().joinWebApp(webAppId, + new WebAppSession.LaunchListener() { + + @Override + public void onError(ServiceCommandError error) { + getWebAppLauncher().launchWebApp(webAppId, + webAppLaunchListener); + } + + @Override + public void onSuccess(WebAppSession webAppSession) { + webAppSession.displayImage(mediaInfo, listener); + } + }); + } + } @Override public void playMedia(final String url, final String mimeType, final String title, final String description, final String iconSrc, final boolean shouldLoop, final MediaPlayer.LaunchListener listener) { @@ -1267,6 +1337,77 @@ public void onSuccess(WebAppSession webAppSession) { } } + @Override + public void playMedia(final MediaInfo mediaInfo, + final boolean shouldLoop, final MediaPlayer.LaunchListener listener) { + if ("4.0.0".equalsIgnoreCase(this.serviceDescription.getVersion())) { + DeviceService dlnaService = this.getDLNAService(); + + if (dlnaService != null) { + MediaPlayer mediaPlayer = dlnaService.getAPI(MediaPlayer.class); + + if (mediaPlayer != null) { + mediaPlayer.playMedia(mediaInfo, shouldLoop, listener); + return; + } + } + + JSONObject params = null; + + try { + params = new JSONObject() { + { + put("target", mediaInfo.getUrl()); + put("title", mediaInfo.getTitle() == null ? NULL : mediaInfo.getTitle()); + put("description", mediaInfo.getDescription() == null ? NULL + : mediaInfo.getDescription()); + put("mimeType", mediaInfo.getMimeType() == null ? NULL : mediaInfo.getMimeType()); + put("iconSrc", mediaInfo.getImages().get(0).getUrl() == null ? NULL : mediaInfo.getImages().get(0).getUrl()); + put("loop", shouldLoop); + } + }; + } catch (JSONException ex) { + ex.printStackTrace(); + Util.postError(listener, + new ServiceCommandError(-1, ex.getLocalizedMessage(), + ex)); + } + + if (params != null) + this.displayMedia(params, listener); + } else { + final String webAppId = "MediaPlayer"; + + final WebAppSession.LaunchListener webAppLaunchListener = new WebAppSession.LaunchListener() { + + @Override + public void onError(ServiceCommandError error) { + listener.onError(error); + } + + @Override + public void onSuccess(WebAppSession webAppSession) { + webAppSession.playMedia(mediaInfo, shouldLoop, listener); + } + }; + + this.getWebAppLauncher().joinWebApp(webAppId, + new WebAppSession.LaunchListener() { + + @Override + public void onError(ServiceCommandError error) { + getWebAppLauncher().launchWebApp(webAppId, + webAppLaunchListener); + } + + @Override + public void onSuccess(WebAppSession webAppSession) { + webAppSession.playMedia(mediaInfo, shouldLoop, listener); + } + }); + } + } + @Override public void closeMedia(LaunchSession launchSession, ResponseListener listener) { JSONObject payload = new JSONObject(); diff --git a/src/com/connectsdk/service/capability/MediaPlayer.java b/src/com/connectsdk/service/capability/MediaPlayer.java index d506a5b1..a75092c6 100644 --- a/src/com/connectsdk/service/capability/MediaPlayer.java +++ b/src/com/connectsdk/service/capability/MediaPlayer.java @@ -20,6 +20,9 @@ package com.connectsdk.service.capability; +import java.util.ArrayList; +import java.util.List; + import com.connectsdk.service.capability.listeners.ResponseListener; import com.connectsdk.service.sessions.LaunchSession; @@ -75,4 +78,154 @@ public MediaLaunchObject(LaunchSession launchSession, MediaControl mediaControl) this.mediaControl = mediaControl; } } + public static class MediaInfo { + + public MediaInfo(String url, String mimeType, String title, String description, + List allImages) { + super(); + this.url = url; + this.mimeType = mimeType; + this.title = title; + this.description = description; + this.allImages = allImages; + } + + public MediaInfo(String url, String mimeType, String title, String description) { + super(); + this.url = url; + this.mimeType = mimeType; + this.title = title; + this.description = description; + } + + public String url, mimeType, description, title; + + public List allImages; + + public long duration; + + public String getMimeType() { + return mimeType; + } + + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getImages() { + return allImages; + } + + public void setImages(List images) { + this.allImages = images; + } + + public long getDuration() { + return duration; + } + + public void setDuration(long duration) { + this.duration = duration; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public void addImages(ImageInfo... images) { + + List list = new ArrayList(); + for (int i = 0; i < images.length; i++) { + list.add(images[i]); + } + + this.setImages(list); + + } + + public static class ImageInfo { + + public ImageInfo(String url, ImageType type, int width, int height) { + super(); + this.url = url; + this.type = type; + this.width = width; + this.height = height; + } + + public ImageInfo(String url) { + super(); + this.url = url; + } + + public enum ImageType { + Thumb, Video_Poster, Album_Art, Unknown; + } + + public String url; + public ImageType type; + public int width; + public int height; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public ImageType getType() { + return type; + } + + public void setType(ImageType type) { + this.type = type; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + } + + } + + void displayImage(MediaInfo mediaInfo, LaunchListener listener); + + void playMedia(MediaInfo mediaInfo, boolean shouldLoop, + LaunchListener listener); + } diff --git a/src/com/connectsdk/service/sessions/WebAppSession.java b/src/com/connectsdk/service/sessions/WebAppSession.java index 486f12c4..f10078ff 100644 --- a/src/com/connectsdk/service/sessions/WebAppSession.java +++ b/src/com/connectsdk/service/sessions/WebAppSession.java @@ -31,77 +31,89 @@ import com.connectsdk.service.command.ServiceSubscription; /** - * ###Overview - * When a web app is launched on a first screen device, there are certain tasks that can be performed with that web app. WebAppSession serves as a second screen reference of the web app that was launched. It behaves similarly to LaunchSession, but is not nearly as static. - * - * ###In Depth - * On top of maintaining session information (contained in the launchSession property), WebAppSession provides access to a number of capabilities. - * - MediaPlayer - * - MediaControl - * - Bi-directional communication with web app - * - * MediaPlayer and MediaControl are provided to allow for the most common first screen use cases -- a media player (audio, video, & images). - * - * The Connect SDK JavaScript Bridge has been produced to provide normalized support for these capabilities across protocols (Chromecast, webOS, etc). + * ###Overview When a web app is launched on a first screen device, there are + * certain tasks that can be performed with that web app. WebAppSession serves + * as a second screen reference of the web app that was launched. It behaves + * similarly to LaunchSession, but is not nearly as static. + * + * ###In Depth On top of maintaining session information (contained in the + * launchSession property), WebAppSession provides access to a number of + * capabilities. - MediaPlayer - MediaControl - Bi-directional communication + * with web app + * + * MediaPlayer and MediaControl are provided to allow for the most common first + * screen use cases -- a media player (audio, video, & images). + * + * The Connect SDK JavaScript Bridge has been produced to provide normalized + * support for these capabilities across protocols (Chromecast, webOS, etc). */ public class WebAppSession implements MediaControl, MediaPlayer { /** Status of the web app */ public enum WebAppStatus { /** Web app status is unknown */ - Unknown, + Unknown, /** Web app is running and in the foreground */ - Open, + Open, /** Web app is running and in the background */ - Background, + Background, /** Web app is in the foreground but has not started running yet */ - Foreground, + Foreground, /** Web app is not running and is not in the foreground or background */ Closed } - + /** - * LaunchSession object containing key session information. Much of this information is required for web app messaging & closing the web app. + * LaunchSession object containing key session information. Much of this + * information is required for web app messaging & closing the web app. */ public LaunchSession launchSession; - + // @cond INTERNAL - protected DeviceService service; + protected DeviceService service; private WebAppSessionListener webAppListener; + // @endcond /** - * Instantiates a WebAppSession object with all the information necessary to interact with a web app. - * - * @param launchSession LaunchSession containing info about the web app session - * @param service DeviceService that was responsible for launching this web app + * Instantiates a WebAppSession object with all the information necessary to + * interact with a web app. + * + * @param launchSession + * LaunchSession containing info about the web app session + * @param service + * DeviceService that was responsible for launching this web app */ public WebAppSession(LaunchSession launchSession, DeviceService service) { this.launchSession = launchSession; this.service = service; } - + /** * DeviceService that was responsible for launching this web app. */ - protected void setService(DeviceService service) { } - + protected void setService(DeviceService service) { + } + /** * Subscribes to changes in the web app's status. - * - * @param listener (optional) MessageListener to be called on app status change + * + * @param listener + * (optional) MessageListener to be called on app status change */ - public ServiceSubscription subscribeWebAppStatus(MessageListener listener) { + public ServiceSubscription subscribeWebAppStatus( + MessageListener listener) { if (listener != null) listener.onError(ServiceCommandError.notSupported()); - + return null; } - + /** * Establishes a communication channel with the web app. - * - * @param connectionListener (optional) ResponseListener to be called on success + * + * @param connectionListener + * (optional) ResponseListener to be called on success */ public void connect(ResponseListener connectionListener) { Util.postError(connectionListener, ServiceCommandError.notSupported()); @@ -119,237 +131,279 @@ public void join(ResponseListener connectionListener) { /** * Closes any open communication channel with the web app. */ - public void disconnectFromWebApp() { } - + public void disconnectFromWebApp() { + } + /** * Closes the web app on the first screen device. - * - * @param listener (optional) ResponseListener to be called on success + * + * @param listener + * (optional) ResponseListener to be called on success */ public void close(ResponseListener listener) { if (listener != null) listener.onError(ServiceCommandError.notSupported()); } - + /** - * Sends a simple string to the web app. The Connect SDK JavaScript Bridge will receive this message and hand it off as a string object. - * - * @param listener (optional) ResponseListener to be called on success + * Sends a simple string to the web app. The Connect SDK JavaScript Bridge + * will receive this message and hand it off as a string object. + * + * @param listener + * (optional) ResponseListener to be called on success */ public void sendMessage(String message, ResponseListener listener) { if (listener != null) listener.onError(ServiceCommandError.notSupported()); } - + /** - * Sends a JSON object to the web app. The Connect SDK JavaScript Bridge will receive this message and hand it off as a JavaScript object. - * - * @param success (optional) ResponseListener to be called on success + * Sends a JSON object to the web app. The Connect SDK JavaScript Bridge + * will receive this message and hand it off as a JavaScript object. + * + * @param success + * (optional) ResponseListener to be called on success */ - public void sendMessage(JSONObject message, ResponseListener listener) { + public void sendMessage(JSONObject message, + ResponseListener listener) { if (listener != null) { listener.onError(ServiceCommandError.notSupported()); } } - + // @cond INTERNAL - @Override public MediaControl getMediaControl() { return null; } + @Override + public MediaControl getMediaControl() { + return null; + } @Override public CapabilityPriorityLevel getMediaControlCapabilityLevel() { return CapabilityPriorityLevel.VERY_LOW; } - + @Override public void play(ResponseListener listener) { MediaControl mediaControl = null; - + if (service != null) mediaControl = service.getAPI(MediaControl.class); - + if (mediaControl != null) mediaControl.play(listener); else if (listener != null) listener.onError(ServiceCommandError.notSupported()); } - + @Override public void pause(ResponseListener listener) { MediaControl mediaControl = null; - + if (service != null) mediaControl = service.getAPI(MediaControl.class); - + if (mediaControl != null) mediaControl.pause(listener); else if (listener != null) listener.onError(ServiceCommandError.notSupported()); } - + @Override public void stop(ResponseListener listener) { MediaControl mediaControl = null; - + if (service != null) mediaControl = service.getAPI(MediaControl.class); - + if (mediaControl != null) mediaControl.stop(listener); else if (listener != null) listener.onError(ServiceCommandError.notSupported()); } - + @Override public void rewind(ResponseListener listener) { MediaControl mediaControl = null; - + if (service != null) mediaControl = service.getAPI(MediaControl.class); - + if (mediaControl != null) mediaControl.rewind(listener); else if (listener != null) listener.onError(ServiceCommandError.notSupported()); } - + @Override public void fastForward(ResponseListener listener) { MediaControl mediaControl = null; - + if (service != null) mediaControl = service.getAPI(MediaControl.class); - + if (mediaControl != null) mediaControl.fastForward(listener); else if (listener != null) listener.onError(ServiceCommandError.notSupported()); } - + @Override public void seek(long position, ResponseListener listener) { MediaControl mediaControl = null; - + if (service != null) mediaControl = service.getAPI(MediaControl.class); - + if (mediaControl != null) mediaControl.seek(position, listener); else if (listener != null) listener.onError(ServiceCommandError.notSupported()); } - + @Override public void getDuration(DurationListener listener) { MediaControl mediaControl = null; - + if (service != null) mediaControl = service.getAPI(MediaControl.class); - + if (mediaControl != null) mediaControl.getDuration(listener); else if (listener != null) listener.onError(ServiceCommandError.notSupported()); } - + @Override public void getPosition(PositionListener listener) { MediaControl mediaControl = null; - + if (service != null) mediaControl = service.getAPI(MediaControl.class); - + if (mediaControl != null) mediaControl.getPosition(listener); else if (listener != null) listener.onError(ServiceCommandError.notSupported()); } - + @Override public void getPlayState(PlayStateListener listener) { MediaControl mediaControl = null; - + if (service != null) mediaControl = service.getAPI(MediaControl.class); - + if (mediaControl != null) mediaControl.getPlayState(listener); else if (listener != null) listener.onError(ServiceCommandError.notSupported()); } - + @Override - public ServiceSubscription subscribePlayState(PlayStateListener listener) { + public ServiceSubscription subscribePlayState( + PlayStateListener listener) { MediaControl mediaControl = null; - + if (service != null) mediaControl = service.getAPI(MediaControl.class); - + if (mediaControl != null) return mediaControl.subscribePlayState(listener); else if (listener != null) listener.onError(ServiceCommandError.notSupported()); - + return null; } - + @Override - public void closeMedia(LaunchSession launchSession, ResponseListener listener) { + public void closeMedia(LaunchSession launchSession, + ResponseListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); } - + @Override - public void displayImage(String url, String mimeType, String title, String description, String iconSrc, MediaPlayer.LaunchListener listener) { + public void displayImage(String url, String mimeType, String title, + String description, String iconSrc, + MediaPlayer.LaunchListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); } - + + @Override + public void displayImage(MediaPlayer.MediaInfo mediaInfo, MediaPlayer.LaunchListener listener) { + Util.postError(listener, ServiceCommandError.notSupported()); + } + @Override public void playMedia(String url, String mimeType, String title, String description, String iconSrc, boolean shouldLoop, MediaPlayer.LaunchListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); } + @Override + public void playMedia(MediaPlayer.MediaInfo mediaInfo, boolean shouldLoop, + MediaPlayer.LaunchListener listener) { + Util.postError(listener, ServiceCommandError.notSupported()); + + } + @Override public MediaPlayer getMediaPlayer() { return null; } - + @Override public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { return CapabilityPriorityLevel.VERY_LOW; } + // @endcond - + /** - * When messages are received from a web app, they are parsed into the appropriate object type (string vs JSON/NSDictionary) and routed to the WebAppSessionListener. + * When messages are received from a web app, they are parsed into the + * appropriate object type (string vs JSON/NSDictionary) and routed to the + * WebAppSessionListener. */ public WebAppSessionListener getWebAppSessionListener() { return webAppListener; } - + /** - * When messages are received from a web app, they are parsed into the appropriate object type (string vs JSON/NSDictionary) and routed to the WebAppSessionListener. + * When messages are received from a web app, they are parsed into the + * appropriate object type (string vs JSON/NSDictionary) and routed to the + * WebAppSessionListener. * - * @param listener WebAppSessionListener to be called when messages are received from the web app + * @param listener + * WebAppSessionListener to be called when messages are received + * from the web app */ public void setWebAppSessionListener(WebAppSessionListener listener) { webAppListener = listener; } - + /** * Success block that is called upon successfully launch of a web app. - * - * Passes a WebAppSession Object containing important information about the web app's session. This object is required to perform many functions with the web app, including app-to-app communication, media playback, closing, etc. - */ - public static interface LaunchListener extends ResponseListener { } - + * + * Passes a WebAppSession Object containing important information about the + * web app's session. This object is required to perform many functions with + * the web app, including app-to-app communication, media playback, closing, + * etc. + */ + public static interface LaunchListener extends + ResponseListener { + } + /** - * Success block that is called upon successfully getting a web app's status. - * - * Passes a WebAppStatus of the current running & foreground status of the web app + * Success block that is called upon successfully getting a web app's + * status. + * + * Passes a WebAppStatus of the current running & foreground status of the + * web app */ - public static interface StatusListener extends ResponseListener { } - + public static interface StatusListener extends + ResponseListener { + } + // @cond INTERNAL - public static interface MessageListener extends ResponseListener{ + public static interface MessageListener extends ResponseListener { abstract public void onMessage(Object message); } // @endcond From 9b52d0b3c9e79aa7cbe2150df714200a3a46bf4a Mon Sep 17 00:00:00 2001 From: Simon Gladkoskok Date: Tue, 12 Aug 2014 09:36:43 -0700 Subject: [PATCH 06/76] change ApplicationConnectionResultCallback to use google MediaInfo, import ImageInfo to CastService --- src/com/connectsdk/service/CastService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/com/connectsdk/service/CastService.java b/src/com/connectsdk/service/CastService.java index a4150ead..35e0981e 100644 --- a/src/com/connectsdk/service/CastService.java +++ b/src/com/connectsdk/service/CastService.java @@ -38,6 +38,7 @@ import com.connectsdk.discovery.DiscoveryManager; import com.connectsdk.service.capability.MediaControl; import com.connectsdk.service.capability.MediaPlayer; +import com.connectsdk.service.capability.MediaPlayer.MediaInfo.ImageInfo; import com.connectsdk.service.capability.VolumeControl; import com.connectsdk.service.capability.WebAppLauncher; import com.connectsdk.service.capability.listeners.ResponseListener; @@ -1138,10 +1139,10 @@ public void run() { private final class ApplicationConnectionResultCallback implements ResultCallback { - MediaInfo mediaInfo; + com.google.android.gms.cast.MediaInfo mediaInfo; LaunchListener listener; - public ApplicationConnectionResultCallback(MediaInfo mediaInfo, LaunchListener listener) { + public ApplicationConnectionResultCallback(com.google.android.gms.cast.MediaInfo mediaInfo, LaunchListener listener) { this.mediaInfo = mediaInfo; this.listener = listener; } From 666f957f6c4f723ee7555d130d47dc9327bd21e7 Mon Sep 17 00:00:00 2001 From: Simon Gladkoskok Date: Tue, 12 Aug 2014 11:05:06 -0700 Subject: [PATCH 07/76] pull out MediaInfo and ImageInfo to their own class --- src/com/connectsdk/core/ImageInfo.java | 63 ++++++++ src/com/connectsdk/core/MediaInfo.java | 91 +++++++++++ .../connectsdk/service/AirPlayService.java | 3 +- src/com/connectsdk/service/CastService.java | 12 +- src/com/connectsdk/service/DLNAService.java | 1 + .../service/MultiScreenService.java | 1 + .../connectsdk/service/NetcastTVService.java | 1 + src/com/connectsdk/service/RokuService.java | 1 + .../connectsdk/service/WebOSTVService.java | 1 + .../service/capability/MediaPlayer.java | 151 +----------------- .../service/sessions/WebAppSession.java | 5 +- 11 files changed, 173 insertions(+), 157 deletions(-) create mode 100644 src/com/connectsdk/core/ImageInfo.java create mode 100644 src/com/connectsdk/core/MediaInfo.java diff --git a/src/com/connectsdk/core/ImageInfo.java b/src/com/connectsdk/core/ImageInfo.java new file mode 100644 index 00000000..b52cb3e0 --- /dev/null +++ b/src/com/connectsdk/core/ImageInfo.java @@ -0,0 +1,63 @@ +package com.connectsdk.core; + + +public class ImageInfo { + + + public ImageInfo(String url, ImageType type, int width, int height) { + super(); + this.url = url; + this.type = type; + this.width = width; + this.height = height; + } + + public ImageInfo(String url) { + super(); + this.url = url; + } + + private enum ImageType { + Thumb, Video_Poster, Album_Art, Unknown; + } + + private String url; + private ImageType type; + private int width; + private int height; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public ImageType getType() { + return type; + } + + public void setType(ImageType type) { + this.type = type; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + +} + + diff --git a/src/com/connectsdk/core/MediaInfo.java b/src/com/connectsdk/core/MediaInfo.java new file mode 100644 index 00000000..d10162d0 --- /dev/null +++ b/src/com/connectsdk/core/MediaInfo.java @@ -0,0 +1,91 @@ +package com.connectsdk.core; + +import java.util.ArrayList; +import java.util.List; + +public class MediaInfo { + + public MediaInfo(String url, String mimeType, String title, String description, + List allImages) { + super(); + this.url = url; + this.mimeType = mimeType; + this.title = title; + this.description = description; + this.allImages = allImages; + } + + public MediaInfo(String url, String mimeType, String title, String description) { + super(); + this.url = url; + this.mimeType = mimeType; + this.title = title; + this.description = description; + } + + private String url, mimeType, description, title; + + private List allImages; + + private long duration; + + public String getMimeType() { + return mimeType; + } + + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getImages() { + return allImages; + } + + public void setImages(List images) { + this.allImages = images; + } + + public long getDuration() { + return duration; + } + + public void setDuration(long duration) { + this.duration = duration; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public void addImages(ImageInfo... images) { + + List list = new ArrayList(); + for (int i = 0; i < images.length; i++) { + list.add(images[i]); + } + + this.setImages(list); + + } + +} diff --git a/src/com/connectsdk/service/AirPlayService.java b/src/com/connectsdk/service/AirPlayService.java index 9cadb9fa..e0e40c0d 100644 --- a/src/com/connectsdk/service/AirPlayService.java +++ b/src/com/connectsdk/service/AirPlayService.java @@ -52,6 +52,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; import com.connectsdk.etc.helper.DeviceServiceReachability; import com.connectsdk.etc.helper.HttpMessage; @@ -375,7 +376,7 @@ public void onError(ServiceCommandError error) { HttpEntity entity = null; try { - URL imagePath = new URL(mediaInfo.url); + URL imagePath = new URL(mediaInfo.getUrl()); HttpURLConnection connection = (HttpURLConnection) imagePath.openConnection(); connection.setDoInput(true); connection.connect(); diff --git a/src/com/connectsdk/service/CastService.java b/src/com/connectsdk/service/CastService.java index 35e0981e..4c46d490 100644 --- a/src/com/connectsdk/service/CastService.java +++ b/src/com/connectsdk/service/CastService.java @@ -34,11 +34,12 @@ import android.os.Bundle; import android.util.Log; +import com.connectsdk.core.ImageInfo; +import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; import com.connectsdk.discovery.DiscoveryManager; import com.connectsdk.service.capability.MediaControl; import com.connectsdk.service.capability.MediaPlayer; -import com.connectsdk.service.capability.MediaPlayer.MediaInfo.ImageInfo; import com.connectsdk.service.capability.VolumeControl; import com.connectsdk.service.capability.WebAppLauncher; import com.connectsdk.service.capability.listeners.ResponseListener; @@ -57,7 +58,6 @@ import com.google.android.gms.cast.Cast.ApplicationConnectionResult; import com.google.android.gms.cast.CastDevice; import com.google.android.gms.cast.CastMediaControlIntent; -import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.RemoteMediaPlayer; @@ -507,10 +507,10 @@ public void onConnected() { mMediaMetadata.putString(MediaMetadata.KEY_TITLE, mediaInfo.getTitle()); mMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, mediaInfo.getDescription()); - ImageInfo imageInfo = mediaInfo.allImages.get(0); + ImageInfo imageInfo = mediaInfo.getImages().get(0); - if ( imageInfo.url!= null) { - Uri iconUri = Uri.parse(imageInfo.url); + if ( imageInfo.getUrl()!= null) { + Uri iconUri = Uri.parse(imageInfo.getUrl()); WebImage image = new WebImage(iconUri, 100, 100); mMediaMetadata.addImage(image); } @@ -575,7 +575,7 @@ public void onConnected() { mMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, mediaInfo.getDescription()); - ImageInfo imageInfo = mediaInfo.allImages.get(0); + ImageInfo imageInfo = mediaInfo.getImages().get(0); if (imageInfo.getUrl() != null) { Uri iconUri = Uri.parse(imageInfo.getUrl()); WebImage image = new WebImage(iconUri, 100, 100); diff --git a/src/com/connectsdk/service/DLNAService.java b/src/com/connectsdk/service/DLNAService.java index 3d59153a..8840372f 100644 --- a/src/com/connectsdk/service/DLNAService.java +++ b/src/com/connectsdk/service/DLNAService.java @@ -42,6 +42,7 @@ import org.json.JSONException; import org.json.JSONObject; +import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; import com.connectsdk.core.upnp.service.Service; import com.connectsdk.etc.helper.DeviceServiceReachability; diff --git a/src/com/connectsdk/service/MultiScreenService.java b/src/com/connectsdk/service/MultiScreenService.java index a6c10bcc..26c248a3 100644 --- a/src/com/connectsdk/service/MultiScreenService.java +++ b/src/com/connectsdk/service/MultiScreenService.java @@ -9,6 +9,7 @@ import org.json.JSONException; import org.json.JSONObject; +import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; import com.connectsdk.service.capability.MediaControl; import com.connectsdk.service.capability.MediaPlayer; diff --git a/src/com/connectsdk/service/NetcastTVService.java b/src/com/connectsdk/service/NetcastTVService.java index dcf575f4..de2a8bc7 100644 --- a/src/com/connectsdk/service/NetcastTVService.java +++ b/src/com/connectsdk/service/NetcastTVService.java @@ -57,6 +57,7 @@ import com.connectsdk.core.AppInfo; import com.connectsdk.core.ChannelInfo; import com.connectsdk.core.ExternalInputInfo; +import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; import com.connectsdk.device.ConnectableDevice; import com.connectsdk.device.netcast.NetcastAppNumberParser; diff --git a/src/com/connectsdk/service/RokuService.java b/src/com/connectsdk/service/RokuService.java index 074aba01..0099c56b 100644 --- a/src/com/connectsdk/service/RokuService.java +++ b/src/com/connectsdk/service/RokuService.java @@ -53,6 +53,7 @@ import android.util.Log; import com.connectsdk.core.AppInfo; +import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; import com.connectsdk.device.ConnectableDevice; import com.connectsdk.device.roku.RokuApplicationListParser; diff --git a/src/com/connectsdk/service/WebOSTVService.java b/src/com/connectsdk/service/WebOSTVService.java index 3cdb7e49..ad20c8af 100644 --- a/src/com/connectsdk/service/WebOSTVService.java +++ b/src/com/connectsdk/service/WebOSTVService.java @@ -46,6 +46,7 @@ import com.connectsdk.core.AppInfo; import com.connectsdk.core.ChannelInfo; import com.connectsdk.core.ExternalInputInfo; +import com.connectsdk.core.MediaInfo; import com.connectsdk.core.ProgramList; import com.connectsdk.core.Util; import com.connectsdk.core.upnp.Device; diff --git a/src/com/connectsdk/service/capability/MediaPlayer.java b/src/com/connectsdk/service/capability/MediaPlayer.java index a75092c6..bd91e496 100644 --- a/src/com/connectsdk/service/capability/MediaPlayer.java +++ b/src/com/connectsdk/service/capability/MediaPlayer.java @@ -20,9 +20,7 @@ package com.connectsdk.service.capability; -import java.util.ArrayList; -import java.util.List; - +import com.connectsdk.core.MediaInfo; import com.connectsdk.service.capability.listeners.ResponseListener; import com.connectsdk.service.sessions.LaunchSession; @@ -78,154 +76,11 @@ public MediaLaunchObject(LaunchSession launchSession, MediaControl mediaControl) this.mediaControl = mediaControl; } } - public static class MediaInfo { - - public MediaInfo(String url, String mimeType, String title, String description, - List allImages) { - super(); - this.url = url; - this.mimeType = mimeType; - this.title = title; - this.description = description; - this.allImages = allImages; - } - - public MediaInfo(String url, String mimeType, String title, String description) { - super(); - this.url = url; - this.mimeType = mimeType; - this.title = title; - this.description = description; - } - - public String url, mimeType, description, title; - - public List allImages; - - public long duration; - - public String getMimeType() { - return mimeType; - } - - public void setMimeType(String mimeType) { - this.mimeType = mimeType; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public List getImages() { - return allImages; - } - - public void setImages(List images) { - this.allImages = images; - } - - public long getDuration() { - return duration; - } - public void setDuration(long duration) { - this.duration = duration; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public void addImages(ImageInfo... images) { - - List list = new ArrayList(); - for (int i = 0; i < images.length; i++) { - list.add(images[i]); - } - - this.setImages(list); - - } - - public static class ImageInfo { - - public ImageInfo(String url, ImageType type, int width, int height) { - super(); - this.url = url; - this.type = type; - this.width = width; - this.height = height; - } - - public ImageInfo(String url) { - super(); - this.url = url; - } - - public enum ImageType { - Thumb, Video_Poster, Album_Art, Unknown; - } - - public String url; - public ImageType type; - public int width; - public int height; - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public ImageType getType() { - return type; - } - - public void setType(ImageType type) { - this.type = type; - } - - public int getWidth() { - return width; - } - - public void setWidth(int width) { - this.width = width; - } - - public int getHeight() { - return height; - } - - public void setHeight(int height) { - this.height = height; - } - - } - - } - void displayImage(MediaInfo mediaInfo, LaunchListener listener); + public void displayImage(MediaInfo mediaInfo, LaunchListener listener); - void playMedia(MediaInfo mediaInfo, boolean shouldLoop, + public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, LaunchListener listener); } diff --git a/src/com/connectsdk/service/sessions/WebAppSession.java b/src/com/connectsdk/service/sessions/WebAppSession.java index f10078ff..ef8565aa 100644 --- a/src/com/connectsdk/service/sessions/WebAppSession.java +++ b/src/com/connectsdk/service/sessions/WebAppSession.java @@ -22,6 +22,7 @@ import org.json.JSONObject; +import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; import com.connectsdk.service.DeviceService; import com.connectsdk.service.capability.MediaControl; @@ -329,7 +330,7 @@ public void displayImage(String url, String mimeType, String title, } @Override - public void displayImage(MediaPlayer.MediaInfo mediaInfo, MediaPlayer.LaunchListener listener) { + public void displayImage(MediaInfo mediaInfo, MediaPlayer.LaunchListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); } @@ -339,7 +340,7 @@ public void playMedia(String url, String mimeType, String title, String descript } @Override - public void playMedia(MediaPlayer.MediaInfo mediaInfo, boolean shouldLoop, + public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, MediaPlayer.LaunchListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); From 72fc63c0747083b8d56a15437b5e5aa9aa6c3419 Mon Sep 17 00:00:00 2001 From: Simon Gladkoskok Date: Tue, 12 Aug 2014 16:23:20 -0700 Subject: [PATCH 08/76] override displayImage and playMedia in WebOSWebAppSession , add optional capability to show poster image to playMedia on webOS --- src/com/connectsdk/core/ImageInfo.java | 17 +- src/com/connectsdk/core/MediaInfo.java | 125 ++- .../connectsdk/service/WebOSTVService.java | 53 +- .../service/sessions/WebOSWebAppSession.java | 861 +++++++++++------- 4 files changed, 638 insertions(+), 418 deletions(-) diff --git a/src/com/connectsdk/core/ImageInfo.java b/src/com/connectsdk/core/ImageInfo.java index b52cb3e0..e45d4c32 100644 --- a/src/com/connectsdk/core/ImageInfo.java +++ b/src/com/connectsdk/core/ImageInfo.java @@ -1,23 +1,20 @@ package com.connectsdk.core; - public class ImageInfo { - - public ImageInfo(String url, ImageType type, int width, int height) { + public ImageInfo(String url) { super(); this.url = url; + } + + public ImageInfo(String url, ImageType type, int width, int height) { + this(url); this.type = type; this.width = width; this.height = height; } - - public ImageInfo(String url) { - super(); - this.url = url; - } - private enum ImageType { + public enum ImageType { Thumb, Video_Poster, Album_Art, Unknown; } @@ -59,5 +56,3 @@ public void setHeight(int height) { } } - - diff --git a/src/com/connectsdk/core/MediaInfo.java b/src/com/connectsdk/core/MediaInfo.java index d10162d0..b827df71 100644 --- a/src/com/connectsdk/core/MediaInfo.java +++ b/src/com/connectsdk/core/MediaInfo.java @@ -5,87 +5,84 @@ public class MediaInfo { - public MediaInfo(String url, String mimeType, String title, String description, - List allImages) { - super(); - this.url = url; - this.mimeType = mimeType; - this.title = title; - this.description = description; - this.allImages = allImages; - } + public MediaInfo(String url, String mimeType, String title, + String description) { + super(); + this.url = url; + this.mimeType = mimeType; + this.title = title; + this.description = description; + } - public MediaInfo(String url, String mimeType, String title, String description) { - super(); - this.url = url; - this.mimeType = mimeType; - this.title = title; - this.description = description; - } - - private String url, mimeType, description, title; + public MediaInfo(String url, String mimeType, String title, + String description, List allImages) { + this(url, mimeType, title, description); + this.allImages = allImages; + } - private List allImages; + private String url, mimeType, description, title; - private long duration; + private List allImages; - public String getMimeType() { - return mimeType; - } + private long duration; - public void setMimeType(String mimeType) { - this.mimeType = mimeType; - } + public String getMimeType() { + return mimeType; + } - public String getTitle() { - return title; - } + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } - public void setTitle(String title) { - this.title = title; - } - - public String getDescription() { - return description; - } + public String getTitle() { + return title; + } - public void setDescription(String description) { - this.description = description; - } + public void setTitle(String title) { + this.title = title; + } - public List getImages() { - return allImages; - } + public String getDescription() { + return description; + } - public void setImages(List images) { - this.allImages = images; - } + public void setDescription(String description) { + this.description = description; + } - public long getDuration() { - return duration; - } + public List getImages() { + return allImages; + } - public void setDuration(long duration) { - this.duration = duration; - } + public void setImages(List images) { + this.allImages = images; + } - public String getUrl() { - return url; - } + public long getDuration() { + return duration; + } - public void setUrl(String url) { - this.url = url; - } + public void setDuration(long duration) { + this.duration = duration; + } - public void addImages(ImageInfo... images) { + public String getUrl() { + return url; + } - List list = new ArrayList(); - for (int i = 0; i < images.length; i++) { - list.add(images[i]); - } + public void setUrl(String url) { + this.url = url; + } - this.setImages(list); + public void addImages(ImageInfo... images) { + List list = new ArrayList(); + for (int i = 0; i < images.length; i++) { + list.add(images[i]); } - + + this.setImages(list); + + } + } diff --git a/src/com/connectsdk/service/WebOSTVService.java b/src/com/connectsdk/service/WebOSTVService.java index ad20c8af..13895b0b 100644 --- a/src/com/connectsdk/service/WebOSTVService.java +++ b/src/com/connectsdk/service/WebOSTVService.java @@ -1221,57 +1221,57 @@ public void displayImage(final MediaInfo mediaInfo, } JSONObject params = null; + try { params = new JSONObject() { { put("target", mediaInfo.getUrl()); put("title", mediaInfo.getTitle() == null ? NULL : mediaInfo.getTitle()); - put("description", mediaInfo.getDescription() == null ? NULL - : mediaInfo.getDescription()); + put("description", mediaInfo.getDescription() == null ? NULL : mediaInfo.getDescription()); put("mimeType", mediaInfo.getMimeType() == null ? NULL : mediaInfo.getMimeType()); put("iconSrc", mediaInfo.getImages().get(0).getUrl() == null ? NULL : mediaInfo.getImages().get(0).getUrl()); - } - }; + + }}; } catch (JSONException ex) { ex.printStackTrace(); - Util.postError(listener, - new ServiceCommandError(-1, ex.getLocalizedMessage(), - ex)); + Util.postError(listener, new ServiceCommandError(-1, ex.getLocalizedMessage(), ex)); } - + if (params != null) this.displayMedia(params, listener); } else { final String webAppId = "MediaPlayer"; - + final WebAppSession.LaunchListener webAppLaunchListener = new WebAppSession.LaunchListener() { - + + + @Override public void onError(ServiceCommandError error) { listener.onError(error); } - + @Override public void onSuccess(WebAppSession webAppSession) { webAppSession.displayImage(mediaInfo, listener); } }; - - this.getWebAppLauncher().joinWebApp(webAppId, - new WebAppSession.LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - getWebAppLauncher().launchWebApp(webAppId, - webAppLaunchListener); - } - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.displayImage(mediaInfo, listener); - } - }); + + this.getWebAppLauncher().joinWebApp(webAppId, new WebAppSession.LaunchListener() { + + + + @Override + public void onError(ServiceCommandError error) { + getWebAppLauncher().launchWebApp(webAppId, webAppLaunchListener); + } + + @Override + public void onSuccess(WebAppSession webAppSession) { + webAppSession.displayImage(mediaInfo, listener); + } + }); } } @@ -1364,6 +1364,7 @@ public void playMedia(final MediaInfo mediaInfo, : mediaInfo.getDescription()); put("mimeType", mediaInfo.getMimeType() == null ? NULL : mediaInfo.getMimeType()); put("iconSrc", mediaInfo.getImages().get(0).getUrl() == null ? NULL : mediaInfo.getImages().get(0).getUrl()); + put("posterSrc", mediaInfo.getImages().get(1).getUrl() == null ? NULL : mediaInfo.getImages().get(1).getUrl()); put("loop", shouldLoop); } }; diff --git a/src/com/connectsdk/service/sessions/WebOSWebAppSession.java b/src/com/connectsdk/service/sessions/WebOSWebAppSession.java index c63cc5dd..3859fe42 100644 --- a/src/com/connectsdk/service/sessions/WebOSWebAppSession.java +++ b/src/com/connectsdk/service/sessions/WebOSWebAppSession.java @@ -27,6 +27,7 @@ import org.json.JSONException; import org.json.JSONObject; +import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; import com.connectsdk.service.DeviceService; import com.connectsdk.service.WebOSTVService; @@ -44,111 +45,114 @@ public class WebOSWebAppSession extends WebAppSession { private static final String namespaceKey = "connectsdk."; protected WebOSTVService service; - + ResponseListener>> mConnectionListener; - + public WebOSTVServiceSocketClient socket; public URLServiceSubscription> appToAppSubscription; - + private ServiceSubscription mPlayStateSubscription; private ServiceSubscription mMessageSubscription; private ConcurrentHashMap> mActiveCommands; - + String mFullAppId; - + private int UID; private boolean connected; - + public WebOSWebAppSession(LaunchSession launchSession, DeviceService service) { super(launchSession, service); - + UID = 0; - mActiveCommands = new ConcurrentHashMap>(0, 0.75f, 10); + mActiveCommands = new ConcurrentHashMap>(0, + 0.75f, 10); connected = false; - + this.service = (WebOSTVService) service; } private int getNextId() { return ++UID; } - + public Boolean isConnected() { return connected; } - - public void setConnected(Boolean connected) { + + public void setConnected(Boolean connected) { this.connected = connected; } - + public void handleMediaEvent(JSONObject payload) { String type = ""; - + type = payload.optString("type"); if (type.length() == 0) return; - + if (type.equals("playState")) { if (mPlayStateSubscription == null) return; - + String playStateString = payload.optString(type); if (playStateString.length() == 0) return; final MediaControl.PlayStateStatus playState = parsePlayState(playStateString); - - for (PlayStateListener listener : mPlayStateSubscription.getListeners()) { + + for (PlayStateListener listener : mPlayStateSubscription + .getListeners()) { Util.postSuccess(listener, playState); } } } - + public String getFullAppId() { if (mFullAppId == null) { if (launchSession.getSessionType() != LaunchSessionType.WebApp) mFullAppId = launchSession.getAppId(); else { - Enumeration enumeration = service.getWebAppIdMappings().keys(); - + Enumeration enumeration = service.getWebAppIdMappings() + .keys(); + while (enumeration.hasMoreElements()) { String mappedFullAppId = enumeration.nextElement(); - String mappedAppId = service.getWebAppIdMappings().get(mappedFullAppId); - - if (mappedAppId.equalsIgnoreCase(launchSession.getAppId())) - { + String mappedAppId = service.getWebAppIdMappings().get( + mappedFullAppId); + + if (mappedAppId.equalsIgnoreCase(launchSession.getAppId())) { mFullAppId = mappedAppId; break; } } } } - + if (mFullAppId == null) return launchSession.getAppId(); else return mFullAppId; } - + public void setFullAppId(String fullAppId) { mFullAppId = fullAppId; } - + private WebOSTVServiceSocketClientListener mSocketListener = new WebOSTVServiceSocketClientListener() { - + @Override - public void onRegistrationFailed(ServiceCommandError error) { } - + public void onRegistrationFailed(ServiceCommandError error) { + } + @Override public Boolean onReceiveMessage(JSONObject payload) { String type = payload.optString("type"); - if ("p2p".equals(type)) - { + if ("p2p".equals(type)) { String fromAppId = null; - + fromAppId = payload.optString("from"); - + if (!fromAppId.equalsIgnoreCase(getFullAppId())) return false; @@ -156,25 +160,27 @@ public Boolean onReceiveMessage(JSONObject payload) { if (message instanceof JSONObject) { JSONObject messageJSON = (JSONObject) message; - + String contentType = messageJSON.optString("contentType"); - Integer contentTypeIndex = contentType.indexOf("connectsdk."); - - if (contentType != null && contentTypeIndex >= 0) - { + Integer contentTypeIndex = contentType + .indexOf("connectsdk."); + + if (contentType != null && contentTypeIndex >= 0) { String payloadKey = contentType.split("connectsdk.")[1]; - + if (payloadKey == null || payloadKey.length() == 0) return false; - - JSONObject messagePayload = messageJSON.optJSONObject(payloadKey); - + + JSONObject messagePayload = messageJSON + .optJSONObject(payloadKey); + if (messagePayload == null) return false; - + if (payloadKey.equalsIgnoreCase("mediaEvent")) handleMediaEvent(messagePayload); - else if (payloadKey.equalsIgnoreCase("mediaCommandResponse")) + else if (payloadKey + .equalsIgnoreCase("mediaCommandResponse")) handleMediaCommandResponse(messagePayload); } else { handleMessage(messageJSON); @@ -182,91 +188,97 @@ else if (payloadKey.equalsIgnoreCase("mediaCommandResponse")) } else if (message instanceof String) { handleMessage(message); } - + return false; } - + return true; } - + @Override public void onFailWithError(ServiceCommandError error) { connected = false; appToAppSubscription = null; - + if (mConnectionListener != null) { if (error == null) - error = new ServiceCommandError(0, "Unknown error connecting to web socket", null); - + error = new ServiceCommandError(0, + "Unknown error connecting to web socket", null); + mConnectionListener.onError(error); } - + mConnectionListener = null; } - + @Override public void onConnect() { if (mConnectionListener != null) mConnectionListener.onSuccess(null); - + mConnectionListener = null; } - + @Override public void onCloseWithError(ServiceCommandError error) { connected = false; appToAppSubscription = null; - + if (mConnectionListener != null) { if (error != null) mConnectionListener.onError(error); else { if (getWebAppSessionListener() != null) - getWebAppSessionListener().onWebAppSessionDisconnect(WebOSWebAppSession.this); + getWebAppSessionListener().onWebAppSessionDisconnect( + WebOSWebAppSession.this); } } - + mConnectionListener = null; } - + @Override - public void onBeforeRegister() { } + public void onBeforeRegister() { + } }; - + @SuppressWarnings("unchecked") public void handleMediaCommandResponse(final JSONObject payload) { String requetID = payload.optString("requestId"); if (requetID.length() == 0) return; - - final ServiceCommand> command = (ServiceCommand>) mActiveCommands.get(requetID); - + + final ServiceCommand> command = (ServiceCommand>) mActiveCommands + .get(requetID); + if (command == null) return; - + String mError = payload.optString("error"); if (mError.length() != 0) { - Util.postError(command.getResponseListener(), new ServiceCommandError(0, mError, null)); + Util.postError(command.getResponseListener(), + new ServiceCommandError(0, mError, null)); } else { Util.postSuccess(command.getResponseListener(), payload); } - + mActiveCommands.remove(requetID); } - + public void handleMessage(final Object message) { Util.runOnUI(new Runnable() { - + @Override public void run() { if (getWebAppSessionListener() != null) - getWebAppSessionListener().onReceiveMessage(WebOSWebAppSession.this, message); + getWebAppSessionListener().onReceiveMessage( + WebOSWebAppSession.this, message); } }); - + } - + public PlayStateStatus parsePlayState(String playStateString) { if (playStateString.equals("playing")) return PlayStateStatus.Playing; @@ -278,258 +290,293 @@ else if (playStateString.equals("buffering")) return PlayStateStatus.Buffering; else if (playStateString.equals("finished")) return PlayStateStatus.Finished; - + return PlayStateStatus.Unknown; } - + public void connect(ResponseListener connectionListener) { connect(false, connectionListener); } - + @Override public void join(ResponseListener connectionListener) { connect(true, connectionListener); } - - private void connect(final Boolean joinOnly, final ResponseListener connectionListener) { - if (socket != null && socket.getState() == WebOSTVServiceSocketClient.State.CONNECTING) { - if (connectionListener != null); - connectionListener.onError(new ServiceCommandError(0, "You have a connection request pending, please wait until it has finished", null)); - + + private void connect(final Boolean joinOnly, + final ResponseListener connectionListener) { + if (socket != null + && socket.getState() == WebOSTVServiceSocketClient.State.CONNECTING) { + if (connectionListener != null) + ; + connectionListener + .onError(new ServiceCommandError( + 0, + "You have a connection request pending, please wait until it has finished", + null)); + return; } - + if (isConnected()) { if (connectionListener != null) connectionListener.onSuccess(null); - + return; } - + mConnectionListener = new ResponseListener>>() { - + @Override public void onError(ServiceCommandError error) { if (socket != null) disconnectFromWebApp(); - + if (connectionListener != null) { if (error == null) - error = new ServiceCommandError(0, "Unknown error connecting to web app", null); - + error = new ServiceCommandError(0, + "Unknown error connecting to web app", null); + connectionListener.onError(error); } } - + @Override - public void onSuccess(ServiceCommand> object) { + public void onSuccess( + ServiceCommand> object) { ResponseListener finalConnectionListener = new ResponseListener() { - + @Override public void onError(ServiceCommandError error) { disconnectFromWebApp(); - + if (connectionListener != null) connectionListener.onError(error); } - + @Override public void onSuccess(Object object) { connected = true; - + if (connectionListener != null) connectionListener.onSuccess(object); } }; - - service.connectToWebApp(WebOSWebAppSession.this, joinOnly, finalConnectionListener); + + service.connectToWebApp(WebOSWebAppSession.this, joinOnly, + finalConnectionListener); } }; - + if (socket != null) { if (socket.isConnected()) mConnectionListener.onSuccess(null); else socket.connect(); } else { - socket = new WebOSTVServiceSocketClient(service, WebOSTVServiceSocketClient.getURI(service)); + socket = new WebOSTVServiceSocketClient(service, + WebOSTVServiceSocketClient.getURI(service)); socket.setListener(mSocketListener); socket.connect(); } } - + public void disconnectFromWebApp() { connected = false; mConnectionListener = null; - + if (appToAppSubscription != null) { appToAppSubscription.removeListeners(); appToAppSubscription = null; } - + if (socket != null) { socket.setListener(null); socket.disconnect(); socket = null; } } - + @Override - public void sendMessage(final String message, final ResponseListener listener) { + public void sendMessage(final String message, + final ResponseListener listener) { if (message == null || message.length() == 0) { if (listener != null) - listener.onError(new ServiceCommandError(0, "Cannot send an Empty Message", null)); - + listener.onError(new ServiceCommandError(0, + "Cannot send an Empty Message", null)); + return; } sendP2PMessage(message, listener); } - + @Override - public void sendMessage(final JSONObject message, final ResponseListener listener) { + public void sendMessage(final JSONObject message, + final ResponseListener listener) { if (message == null || message.length() == 0) { - Util.postError(listener, new ServiceCommandError(0, "Cannot send an Empty Message", null)); - + Util.postError(listener, new ServiceCommandError(0, + "Cannot send an Empty Message", null)); + return; } sendP2PMessage(message, listener); } - - private void sendP2PMessage(final Object message, final ResponseListener listener) { + + private void sendP2PMessage(final Object message, + final ResponseListener listener) { JSONObject _payload = new JSONObject(); - + try { _payload.put("type", "p2p"); _payload.put("to", getFullAppId()); _payload.put("payload", message); - } catch (JSONException ex) - { - // do nothing + } catch (JSONException ex) { + // do nothing } - + final JSONObject payload = _payload; - + if (isConnected()) { socket.sendMessage(payload, null); - + if (listener != null) listener.onSuccess(null); } else { ResponseListener connectListener = new ResponseListener() { - + @Override public void onError(ServiceCommandError error) { if (listener != null) listener.onError(error); } - + @Override public void onSuccess(Object object) { sendP2PMessage(message, listener); } }; - + connect(connectListener); } } - + @Override public void close(ResponseListener listener) { mActiveCommands.clear(); - + if (mPlayStateSubscription != null) { mPlayStateSubscription.unsubscribe(); mPlayStateSubscription = null; } - + if (mMessageSubscription != null) { mMessageSubscription.unsubscribe(); mMessageSubscription = null; } - + service.getWebAppLauncher().closeWebApp(launchSession, listener); } - + @Override public void seek(final long position, ResponseListener listener) { if (position < 0) { if (listener != null) - listener.onError(new ServiceCommandError(0, "Must pass a valid positive value", null)); - + listener.onError(new ServiceCommandError(0, + "Must pass a valid positive value", null)); + return; } int requestIdNumber = getNextId(); - final String requestId = String.format(Locale.US, "req%d", requestIdNumber); - + final String requestId = String.format(Locale.US, "req%d", + requestIdNumber); + JSONObject message = null; try { - message = new JSONObject() {{ - put("contentType", namespaceKey + "mediaCommand"); - put("mediaCommand", new JSONObject() {{ - put("type", "seek"); - put("position", position / 1000); - put("requestId", requestId); - }}); - }}; + message = new JSONObject() { + { + put("contentType", namespaceKey + "mediaCommand"); + put("mediaCommand", new JSONObject() { + { + put("type", "seek"); + put("position", position / 1000); + put("requestId", requestId); + } + }); + } + }; } catch (JSONException e) { if (listener != null) - listener.onError(new ServiceCommandError(0, "JSON Parse error", null)); + listener.onError(new ServiceCommandError(0, "JSON Parse error", + null)); } - - ServiceCommand> command = new ServiceCommand>(null, null, null, listener); - + + ServiceCommand> command = new ServiceCommand>( + null, null, null, listener); + mActiveCommands.put(requestId, command); - + sendMessage(message, listener); } - + @Override public void getPosition(final PositionListener listener) { int requestIdNumber = getNextId(); - final String requestId = String.format(Locale.US, "req%d", requestIdNumber); - + final String requestId = String.format(Locale.US, "req%d", + requestIdNumber); + JSONObject message = null; try { - message = new JSONObject() {{ - put("contentType", namespaceKey + "mediaCommand"); - put("mediaCommand", new JSONObject() {{ - put("type", "getPosition"); - put("requestId", requestId); - }}); - }}; + message = new JSONObject() { + { + put("contentType", namespaceKey + "mediaCommand"); + put("mediaCommand", new JSONObject() { + { + put("type", "getPosition"); + put("requestId", requestId); + } + }); + } + }; } catch (JSONException e) { - Util.postError(listener, new ServiceCommandError(0, "JSON Parse error", null)); + Util.postError(listener, new ServiceCommandError(0, + "JSON Parse error", null)); } - - ServiceCommand> command = new ServiceCommand>(null, null, null, new ResponseListener() { - + + ServiceCommand> command = new ServiceCommand>( + null, null, null, new ResponseListener() { + + @Override + public void onSuccess(Object response) { + try { + long position = ((JSONObject) response) + .getLong("position"); + + if (listener != null) + listener.onSuccess(position * 1000); + } catch (JSONException e) { + this.onError(new ServiceCommandError(0, + "JSON Parse error", null)); + } + } + + @Override + public void onError(ServiceCommandError error) { + if (listener != null) + listener.onError(error); + } + }); + + mActiveCommands.put(requestId, command); + + sendMessage(message, new ResponseListener() { + @Override public void onSuccess(Object response) { - try { - long position = ((JSONObject) response).getLong("position"); - - if (listener != null) - listener.onSuccess(position * 1000); - } catch (JSONException e) { - this.onError(new ServiceCommandError(0, "JSON Parse error", null)); - } - } - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) - listener.onError(error); } - }); - - mActiveCommands.put(requestId, command); - - sendMessage(message, new ResponseListener() { - - @Override public void onSuccess(Object response) { } + @Override public void onError(ServiceCommandError error) { if (listener != null) @@ -537,52 +584,64 @@ public void onError(ServiceCommandError error) { } }); } - + @Override public void getDuration(final DurationListener listener) { int requestIdNumber = getNextId(); - final String requestId = String.format(Locale.US, "req%d", requestIdNumber); - + final String requestId = String.format(Locale.US, "req%d", + requestIdNumber); + JSONObject message = null; try { - message = new JSONObject() {{ - put("contentType", namespaceKey + "mediaCommand"); - put("mediaCommand", new JSONObject() {{ - put("type", "getDuration"); - put("requestId", requestId); - }}); - }}; + message = new JSONObject() { + { + put("contentType", namespaceKey + "mediaCommand"); + put("mediaCommand", new JSONObject() { + { + put("type", "getDuration"); + put("requestId", requestId); + } + }); + } + }; } catch (JSONException e) { if (listener != null) - listener.onError(new ServiceCommandError(0, "JSON Parse error", null)); + listener.onError(new ServiceCommandError(0, "JSON Parse error", + null)); } - - ServiceCommand> command = new ServiceCommand>(null, null, null, new ResponseListener() { - + + ServiceCommand> command = new ServiceCommand>( + null, null, null, new ResponseListener() { + + @Override + public void onSuccess(Object response) { + try { + long position = ((JSONObject) response) + .getLong("duration"); + + if (listener != null) + listener.onSuccess(position * 1000); + } catch (JSONException e) { + this.onError(new ServiceCommandError(0, + "JSON Parse error", null)); + } + } + + @Override + public void onError(ServiceCommandError error) { + if (listener != null) + listener.onError(error); + } + }); + + mActiveCommands.put(requestId, command); + + sendMessage(message, new ResponseListener() { + @Override public void onSuccess(Object response) { - try { - long position = ((JSONObject) response).getLong("duration"); - - if (listener != null) - listener.onSuccess(position * 1000); - } catch (JSONException e) { - this.onError(new ServiceCommandError(0, "JSON Parse error", null)); - } } - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) - listener.onError(error); - } - }); - - mActiveCommands.put(requestId, command); - - sendMessage(message, new ResponseListener() { - - @Override public void onSuccess(Object response) { } + @Override public void onError(ServiceCommandError error) { if (listener != null) @@ -590,53 +649,65 @@ public void onError(ServiceCommandError error) { } }); } - + @Override public void getPlayState(final PlayStateListener listener) { int requestIdNumber = getNextId(); - final String requestId = String.format(Locale.US, "req%d", requestIdNumber); - + final String requestId = String.format(Locale.US, "req%d", + requestIdNumber); + JSONObject message = null; try { - message = new JSONObject() {{ - put("contentType", namespaceKey + "mediaCommand"); - put("mediaCommand", new JSONObject() {{ - put("type", "getPlayState"); - put("requestId", requestId); - }}); - }}; + message = new JSONObject() { + { + put("contentType", namespaceKey + "mediaCommand"); + put("mediaCommand", new JSONObject() { + { + put("type", "getPlayState"); + put("requestId", requestId); + } + }); + } + }; } catch (JSONException e) { if (listener != null) - listener.onError(new ServiceCommandError(0, "JSON Parse error", null)); + listener.onError(new ServiceCommandError(0, "JSON Parse error", + null)); } - - ServiceCommand> command = new ServiceCommand>(null, null, null, new ResponseListener() { - + + ServiceCommand> command = new ServiceCommand>( + null, null, null, new ResponseListener() { + + @Override + public void onSuccess(Object response) { + try { + String playStateString = ((JSONObject) response) + .getString("playState"); + PlayStateStatus playState = parsePlayState(playStateString); + + if (listener != null) + listener.onSuccess(playState); + } catch (JSONException e) { + this.onError(new ServiceCommandError(0, + "JSON Parse error", null)); + } + } + + @Override + public void onError(ServiceCommandError error) { + if (listener != null) + listener.onError(error); + } + }); + + mActiveCommands.put(requestId, command); + + sendMessage(message, new ResponseListener() { + @Override public void onSuccess(Object response) { - try { - String playStateString = ((JSONObject) response).getString("playState"); - PlayStateStatus playState = parsePlayState(playStateString); - - if (listener != null) - listener.onSuccess(playState); - } catch (JSONException e) { - this.onError(new ServiceCommandError(0, "JSON Parse error", null)); - } - } - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) - listener.onError(error); } - }); - - mActiveCommands.put(requestId, command); - - sendMessage(message, new ResponseListener() { - - @Override public void onSuccess(Object response) { } + @Override public void onError(ServiceCommandError error) { if (listener != null) @@ -644,29 +715,34 @@ public void onError(ServiceCommandError error) { } }); } - + @Override - public ServiceSubscription subscribePlayState(final PlayStateListener listener) { + public ServiceSubscription subscribePlayState( + final PlayStateListener listener) { if (mPlayStateSubscription == null) - mPlayStateSubscription = new URLServiceSubscription(null, null, null, null); - + mPlayStateSubscription = new URLServiceSubscription( + null, null, null, null); + if (!connected) { connect(new ResponseListener() { - - @Override public void onError(ServiceCommandError error) { + + @Override + public void onError(ServiceCommandError error) { Util.postError(listener, error); } - @Override public void onSuccess(Object response) { + + @Override + public void onSuccess(Object response) { } }); } - + if (!mPlayStateSubscription.getListeners().contains(listener)) mPlayStateSubscription.addListener(listener); return mPlayStateSubscription; } - + /***************** * Media Control * *****************/ @@ -674,12 +750,12 @@ public ServiceSubscription subscribePlayState(final PlayState public MediaControl getMediaControl() { return this; } - + @Override public CapabilityPriorityLevel getMediaControlCapabilityLevel() { return CapabilityPriorityLevel.HIGH; } - + /**************** * Media Player * ****************/ @@ -687,114 +763,265 @@ public CapabilityPriorityLevel getMediaControlCapabilityLevel() { public MediaPlayer getMediaPlayer() { return this; } - + @Override public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { return CapabilityPriorityLevel.HIGH; } - + + @Override + public void displayImage(final String url, final String mimeType, + final String title, final String description, final String iconSrc, + final MediaPlayer.LaunchListener listener) { + int requestIdNumber = getNextId(); + final String requestId = String.format(Locale.US, "req%d", + requestIdNumber); + + JSONObject message = null; + try { + message = new JSONObject() { + { + putOpt("contentType", namespaceKey + "mediaCommand"); + putOpt("mediaCommand", new JSONObject() { + { + putOpt("type", "displayImage"); + putOpt("mediaURL", url); + putOpt("iconURL", iconSrc); + putOpt("title", title); + putOpt("description", description); + putOpt("mimeType", mimeType); + putOpt("requestId", requestId); + } + }); + } + }; + } catch (JSONException e) { + e.printStackTrace(); + // Should never hit this + } + + ResponseListener response = new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + Util.postError(listener, error); + } + + @Override + public void onSuccess(Object object) { + Util.postSuccess(listener, new MediaLaunchObject(launchSession, + getMediaControl())); + } + }; + + ServiceCommand> command = new ServiceCommand>( + socket, null, null, response); + + mActiveCommands.put(requestId, command); + + sendP2PMessage(message, new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + Util.postError(listener, error); + } + + @Override + public void onSuccess(Object object) { + } + }); + } + @Override - public void displayImage(final String url, final String mimeType, final String title, final String description, final String iconSrc, final MediaPlayer.LaunchListener listener) { + public void displayImage(final MediaInfo mediaInfo, + final MediaPlayer.LaunchListener listener) { int requestIdNumber = getNextId(); - final String requestId = String.format(Locale.US, "req%d", requestIdNumber); - + final String requestId = String.format(Locale.US, "req%d", + requestIdNumber); + JSONObject message = null; try { - message = new JSONObject() {{ - putOpt("contentType", namespaceKey + "mediaCommand"); - putOpt("mediaCommand", new JSONObject() {{ - putOpt("type", "displayImage"); - putOpt("mediaURL", url); - putOpt("iconURL", iconSrc); - putOpt("title", title); - putOpt("description", description); - putOpt("mimeType", mimeType); - putOpt("requestId", requestId); - }}); - }}; + message = new JSONObject() { + { + putOpt("contentType", namespaceKey + "mediaCommand"); + putOpt("mediaCommand", new JSONObject() { + { + putOpt("type", "displayImage"); + putOpt("mediaURL", mediaInfo.getUrl()); + putOpt("iconURL", mediaInfo.getImages().get(0) + .getUrl()); + putOpt("title", mediaInfo.getTitle()); + putOpt("description", mediaInfo.getDescription()); + putOpt("mimeType", mediaInfo.getMimeType()); + putOpt("requestId", requestId); + } + }); + } + }; } catch (JSONException e) { e.printStackTrace(); // Should never hit this } - + ResponseListener response = new ResponseListener() { - + @Override public void onError(ServiceCommandError error) { Util.postError(listener, error); } - + @Override public void onSuccess(Object object) { - Util.postSuccess(listener, new MediaLaunchObject(launchSession, getMediaControl())); + Util.postSuccess(listener, new MediaLaunchObject(launchSession, + getMediaControl())); } }; - - ServiceCommand> command = new ServiceCommand>(socket, null, null, response); - + + ServiceCommand> command = new ServiceCommand>( + socket, null, null, response); + mActiveCommands.put(requestId, command); - + sendP2PMessage(message, new ResponseListener() { - + @Override public void onError(ServiceCommandError error) { Util.postError(listener, error); } - - @Override public void onSuccess(Object object) { } + + @Override + public void onSuccess(Object object) { + } }); } - + @Override - public void playMedia(final String url, final String mimeType, final String title, final String description, final String iconSrc, final boolean shouldLoop, final MediaPlayer.LaunchListener listener) { + public void playMedia(final String url, final String mimeType, + final String title, final String description, final String iconSrc, + final boolean shouldLoop, final MediaPlayer.LaunchListener listener) { int requestIdNumber = getNextId(); - final String requestId = String.format(Locale.US, "req%d", requestIdNumber); - + final String requestId = String.format(Locale.US, "req%d", + requestIdNumber); + JSONObject message = null; try { - message = new JSONObject() {{ - putOpt("contentType", namespaceKey + "mediaCommand"); - putOpt("mediaCommand", new JSONObject() {{ - putOpt("type", "playMedia"); - putOpt("mediaURL", url); - putOpt("iconURL", iconSrc); - putOpt("title", title); - putOpt("description", description); - putOpt("mimeType", mimeType); - putOpt("shouldLoop", shouldLoop); - putOpt("requestId", requestId); - }}); - }}; + message = new JSONObject() { + { + putOpt("contentType", namespaceKey + "mediaCommand"); + putOpt("mediaCommand", new JSONObject() { + { + putOpt("type", "playMedia"); + putOpt("mediaURL", url); + putOpt("iconURL", iconSrc); + putOpt("title", title); + putOpt("description", description); + putOpt("mimeType", mimeType); + putOpt("shouldLoop", shouldLoop); + putOpt("requestId", requestId); + } + }); + } + }; } catch (JSONException e) { e.printStackTrace(); // Should never hit this } - + ResponseListener response = new ResponseListener() { - + @Override public void onError(ServiceCommandError error) { Util.postError(listener, error); } - + @Override public void onSuccess(Object object) { - Util.postSuccess(listener, new MediaLaunchObject(launchSession, getMediaControl())); + Util.postSuccess(listener, new MediaLaunchObject(launchSession, + getMediaControl())); } }; - - ServiceCommand> command = new ServiceCommand>(null, null, null, response); - + + ServiceCommand> command = new ServiceCommand>( + null, null, null, response); + mActiveCommands.put(requestId, command); - + sendMessage(message, new ResponseListener() { - + @Override public void onError(ServiceCommandError error) { Util.postError(listener, error); } - - @Override public void onSuccess(Object object) { } + + @Override + public void onSuccess(Object object) { + } + }); + } + + @Override + public void playMedia(final MediaInfo mediaInfo, + final boolean shouldLoop, final MediaPlayer.LaunchListener listener) { + int requestIdNumber = getNextId(); + final String requestId = String.format(Locale.US, "req%d", + requestIdNumber); + + JSONObject message = null; + try { + message = new JSONObject() { + { + putOpt("contentType", namespaceKey + "mediaCommand"); + putOpt("mediaCommand", new JSONObject() { + { + putOpt("type", "playMedia"); + putOpt("mediaURL", mediaInfo.getUrl()); + putOpt("iconURL", mediaInfo.getImages().get(0) + .getUrl() == null ? NULL : mediaInfo.getImages().get(0) + .getUrl()) ; + putOpt("poster", mediaInfo.getImages().get(1).getUrl() == null ? NULL : mediaInfo.getImages().get(1).getUrl()); + putOpt("title", mediaInfo.getTitle()); + putOpt("description", mediaInfo.getDescription()); + putOpt("mimeType", mediaInfo.getMimeType()); + putOpt("shouldLoop", shouldLoop); + putOpt("requestId", requestId); + } + }); + } + }; + } catch (JSONException e) { + e.printStackTrace(); + // Should never hit this + } + + ResponseListener response = new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + Util.postError(listener, error); + } + + @Override + public void onSuccess(Object object) { + Util.postSuccess(listener, new MediaLaunchObject(launchSession, + getMediaControl())); + } + }; + + ServiceCommand> command = new ServiceCommand>( + null, null, null, response); + + mActiveCommands.put(requestId, command); + + sendMessage(message, new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + Util.postError(listener, error); + } + + @Override + public void onSuccess(Object object) { + } }); } } From 646f9ac9d3bd010fde535b7af63fcbebd70f3954 Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Wed, 13 Aug 2014 16:48:03 +0900 Subject: [PATCH 09/76] Refactored CastService and CastWebAppSession to reduce the inconsistency with iOS --- src/com/connectsdk/service/CastService.java | 1070 ++++++----------- .../service/sessions/CastWebAppSession.java | 145 +-- 2 files changed, 411 insertions(+), 804 deletions(-) diff --git a/src/com/connectsdk/service/CastService.java b/src/com/connectsdk/service/CastService.java index 4c46d490..f2c86107 100644 --- a/src/com/connectsdk/service/CastService.java +++ b/src/com/connectsdk/service/CastService.java @@ -23,7 +23,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -69,23 +68,24 @@ import com.google.android.gms.common.images.WebImage; public class CastService extends DeviceService implements MediaPlayer, MediaControl, VolumeControl, WebAppLauncher { + public interface LaunchWebAppListener{ + void onSuccess(WebAppSession webAppSession); + void onFailure(ServiceCommandError error); + }; // @cond INTERNAL - public interface ConnectionListener { - void onConnected(); - }; - public static final String ID = "Chromecast"; public final static String TAG = "Connect SDK"; public final static String PLAY_STATE = "PlayState"; - public final static String VOLUME = "Volume"; - public final static String MUTE = "Mute"; + public final static String CAST_SERVICE_VOLUME_SUBSCRIPTION_NAME = "volume"; + public final static String CAST_SERVICE_MUTE_SUBSCRIPTION_NAME = "mute"; // @endcond String currentAppId; + String launchingAppId; GoogleApiClient mApiClient; CastListener mCastClientListener; @@ -97,13 +97,9 @@ public interface ConnectionListener { Map sessions; List> subscriptions; - - boolean isConnected = false; - - // Queue of commands that should be sent once register is complete - LinkedHashSet commandQueue = new LinkedHashSet(); - protected static final double VOLUME_INCREMENT = 0.05; + float currentVolumeLevel; + boolean currentMuteStatus; public CastService(ServiceDescription serviceDescription, ServiceConfig serviceConfig) { super(serviceDescription, serviceConfig); @@ -137,19 +133,48 @@ public static JSONObject discoveryParameters() { @Override public void connect() { - if (mApiClient != null) { - if ((mApiClient.isConnected()) || (mApiClient.isConnecting())) - return; - - mApiClient.connect(); + if (connected) + return; + + if (castDevice == null) { + if (serviceDescription instanceof CastServiceDescription) + this.castDevice = ((CastServiceDescription)serviceDescription).getCastDevice(); + } + + if (mApiClient == null) { + Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions + .builder(castDevice, mCastClientListener); + + mApiClient = new GoogleApiClient.Builder(DiscoveryManager.getInstance().getContext()) + .addApi(Cast.API, apiOptionsBuilder.build()) + .addConnectionCallbacks(mConnectionCallbacks) + .addOnConnectionFailedListener(mConnectionFailedListener) + .build(); + + mApiClient.connect(); } } @Override public void disconnect() { - if (mApiClient.isConnected()) - mApiClient.disconnect(); - isConnected = false; + if (!connected) + return; + + connected = false; + + Cast.CastApi.leaveApplication(mApiClient); + mApiClient.disconnect(); + mApiClient = null; + + Util.runOnUI(new Runnable() { + + @Override + public void run() { + if (getListener() != null) { + getListener().onDisconnect(CastService.this, null); + } + } + }); } @Override @@ -159,171 +184,95 @@ public MediaControl getMediaControl() { @Override public CapabilityPriorityLevel getMediaControlCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; + return CapabilityPriorityLevel.HIGH; } @Override public void play(final ResponseListener listener) { - if (mMediaPlayer == null) { + try { + mMediaPlayer.play(mApiClient); + + Util.postSuccess(listener, null); + } catch (Exception e) { Util.postError(listener, new ServiceCommandError(0, "Unable to play", null)); - return; } - - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - try { - mMediaPlayer.play(mApiClient); - - if (listener != null) - listener.onSuccess(null); - } catch (Exception e) { - // NOTE: older versions of Play Services required a check for IOException - Log.w("Connect SDK", "Unable to play", e); - } - } - }; - - runCommand(connectionListener); } @Override public void pause(final ResponseListener listener) { - if (mMediaPlayer == null) { - Util.postError(listener, new ServiceCommandError(0, "Unable to pause", null)); - return; - } - - ConnectionListener connectionListener = new ConnectionListener() { + try { + mMediaPlayer.pause(mApiClient); - @Override - public void onConnected() { - try { - mMediaPlayer.pause(mApiClient); - - if (listener != null) - listener.onSuccess(null); - } catch (Exception e) { - // NOTE: older versions of Play Services required a check for IOException - Log.w("Connect SDK", "Unable to pause", e); - } - } - }; - - runCommand(connectionListener); + Util.postError(listener, null); + } catch (Exception e) { + Util.postError(listener, new ServiceCommandError(0, "Unable to pause", null)); + } } @Override public void stop(final ResponseListener listener) { - if (mMediaPlayer == null) { + try { + mMediaPlayer.stop(mApiClient); + + Util.postError(listener, null); + } catch (Exception e) { Util.postError(listener, new ServiceCommandError(0, "Unable to stop", null)); - return; } - - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - try { - mMediaPlayer.stop(mApiClient); - - if (listener != null) - listener.onSuccess(null); - } catch (Exception e) { - // NOTE: older versions of Play Services required a check for IOException - Log.w("Connect SDK", "Unable to stop"); - } - } - }; - - runCommand(connectionListener); } @Override public void rewind(ResponseListener listener) { - if (listener != null) - Util.postError(listener, ServiceCommandError.notSupported()); + Util.postError(listener, ServiceCommandError.notSupported()); } @Override public void fastForward(ResponseListener listener) { - if (listener != null) - Util.postError(listener, ServiceCommandError.notSupported()); + Util.postError(listener, ServiceCommandError.notSupported()); } @Override - public void seek(final long position, final ResponseListener listener) { - if (mMediaPlayer == null) { - Util.postError(listener, new ServiceCommandError(0, "Unable to seek", null)); + public void seek(long position, final ResponseListener listener) { + if (mMediaPlayer.getMediaStatus() == null) { + Util.postError(listener, new ServiceCommandError(0, "There is no media currently available", null)); return; } - - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - int resumeState = RemoteMediaPlayer.RESUME_STATE_UNCHANGED; - - mMediaPlayer.seek(mApiClient, position, resumeState).setResultCallback( - new ResultCallback() { - - @Override - public void onResult(MediaChannelResult result) { - Status status = result.getStatus(); - if (status.isSuccess()) { - Log.d("Connect SDK", "Seek Successfull"); - Util.postSuccess(listener, result); - } else { - Log.w("Connect SDK", "Unable to seek: " + status.getStatusCode()); - Util.postError(listener, new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), status)); - } - } - - }); - } - }; - - runCommand(connectionListener); + + mMediaPlayer.seek(mApiClient, position, RemoteMediaPlayer.RESUME_STATE_UNCHANGED).setResultCallback( + new ResultCallback() { + + @Override + public void onResult(MediaChannelResult result) { + Status status = result.getStatus(); + + if (status.isSuccess()) { + Util.postSuccess(listener, null); + } else { + Util.postError(listener, new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), status)); + } + } + }); } @Override public void getDuration(final DurationListener listener) { - if (mMediaPlayer == null) { - Util.postError(listener, new ServiceCommandError(0, "Unable to get duration", null)); - return; + if (mMediaPlayer.getMediaStatus() != null) { + Util.postSuccess(listener, mMediaPlayer.getStreamDuration()); + } + else { + Util.postError(listener, new ServiceCommandError(0, "There is no media currently available", null)); } - - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - Util.postSuccess(listener, mMediaPlayer.getStreamDuration()); - } - }; - - runCommand(connectionListener); } @Override public void getPosition(final PositionListener listener) { - if (mMediaPlayer == null) { - Util.postError(listener, new ServiceCommandError(0, "Unable to get position", null)); - return; + if (mMediaPlayer.getMediaStatus() != null) { + Util.postSuccess(listener, mMediaPlayer.getApproximateStreamPosition()); + } + else { + Util.postError(listener, new ServiceCommandError(0, "There is no media currently available", null)); } - - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - Util.postSuccess(listener, mMediaPlayer.getApproximateStreamPosition()); - } - }; - - runCommand(connectionListener); } - @Override public MediaPlayer getMediaPlayer() { return this; @@ -331,7 +280,7 @@ public MediaPlayer getMediaPlayer() { @Override public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; + return CapabilityPriorityLevel.HIGH; } private void attachMediaPlayer() { @@ -366,264 +315,138 @@ public void onMetadataUpdated() { } }); - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - try { - Cast.CastApi.setMessageReceivedCallbacks(mApiClient, mMediaPlayer.getNamespace(), - mMediaPlayer); - } catch (IOException e) { - Log.w("Connect SDK", "Exception while creating media channel", e); - } - } - }; - - runCommand(connectionListener); - } - - @SuppressWarnings("unused") - private void reattachMediaPlayer() { - if (mMediaPlayer != null) { - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - try { - Cast.CastApi.setMessageReceivedCallbacks(mApiClient, mMediaPlayer.getNamespace(), - mMediaPlayer); - } catch (IOException e) { - Log.w("Connect SDK", "Exception while launching application", e); - } - } - }; - - runCommand(connectionListener); + try { + Cast.CastApi.setMessageReceivedCallbacks(mApiClient, mMediaPlayer.getNamespace(), + mMediaPlayer); + } catch (IOException e) { + Log.w("Connect SDK", "Exception while creating media channel", e); } } - + private void detachMediaPlayer() { if (mMediaPlayer != null) { - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - try { - Cast.CastApi.removeMessageReceivedCallbacks(mApiClient, - mMediaPlayer.getNamespace()); - } catch (IOException e) { - Log.w("Connect SDK", "Exception while launching application", e); - } - mMediaPlayer = null; - } - }; - - runCommand(connectionListener); + try { + Cast.CastApi.removeMessageReceivedCallbacks(mApiClient, + mMediaPlayer.getNamespace()); + } catch (IOException e) { + Log.w("Connect SDK", "Exception while launching application", e); + } + mMediaPlayer = null; } } - private void playMedia(final com.google.android.gms.cast.MediaInfo media, final LaunchListener listener) { - if (media == null) { - Util.postError(listener, new ServiceCommandError(500, "MediaInfo is null", null)); - return; - } - - if (mMediaPlayer == null) { - Util.postError(listener, new ServiceCommandError(500, "Trying to play a video with no active media session", null)); - return; - } - - if (mApiClient == null) { - Util.postError(listener, new ServiceCommandError(500, "GoogleApiClient is null", null)); - return; - } - - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - mMediaPlayer.load(mApiClient, media, true).setResultCallback( - new ResultCallback() { - - @Override - public void onResult(MediaChannelResult result) { - if (result.getStatus().isSuccess()) { - LaunchSession launchSession = LaunchSession.launchSessionForAppId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID); - launchSession.setService(CastService.this); - launchSession.setSessionType(LaunchSessionType.Media); - - Util.postSuccess(listener, new MediaLaunchObject(launchSession, CastService.this)); - } else { - Util.postError(listener, new ServiceCommandError(result.getStatus().getStatusCode(), result.getStatus().toString(), result)); - } - } - }); - } - }; + @Override + public void displayImage(String url, String mimeType, String title, + String description, String iconSrc, LaunchListener listener) { + MediaMetadata mMediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_PHOTO); + mMediaMetadata.putString(MediaMetadata.KEY_TITLE, title); + mMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, description); + + if (iconSrc != null) { + Uri iconUri = Uri.parse(iconSrc); + WebImage image = new WebImage(iconUri, 100, 100); + mMediaMetadata.addImage(image); + } - runCommand(connectionListener); - } + com.google.android.gms.cast.MediaInfo mediaInformation = new com.google.android.gms.cast.MediaInfo.Builder(url) + .setContentType(mimeType) + .setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_NONE) + .setMetadata(mMediaMetadata) + .setStreamDuration(0) + .setCustomData(null) + .build(); + playMedia(mediaInformation, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, listener); + } + @Override - public void displayImage(final String url, final String mimeType, final String title, - final String description, final String iconSrc, final LaunchListener listener) { - - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - MediaMetadata mMediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_PHOTO); - mMediaMetadata.putString(MediaMetadata.KEY_TITLE, title); - mMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, description); - - if (iconSrc != null) { - Uri iconUri = Uri.parse(iconSrc); - WebImage image = new WebImage(iconUri, 100, 100); - mMediaMetadata.addImage(image); - } - - com.google.android.gms.cast.MediaInfo mediaInfo = new com.google.android.gms.cast.MediaInfo.Builder(url) - .setContentType(mimeType) - .setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_NONE) - .setMetadata(mMediaMetadata) - .build(); - - Cast.CastApi.launchApplication(mApiClient, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, false) - .setResultCallback(new ApplicationConnectionResultCallback(mediaInfo, listener)); - } - }; - - runCommand(connectionListener); + public void displayImage(MediaInfo mediaInfo, LaunchListener listener) { + ImageInfo imageInfo = mediaInfo.getImages().get(0); + String iconSrc = imageInfo.getUrl(); + + displayImage(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, listener); } - @Override - public void displayImage(final MediaInfo mediaInfo, final LaunchListener listener) { - - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - MediaMetadata mMediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_PHOTO); - mMediaMetadata.putString(MediaMetadata.KEY_TITLE, mediaInfo.getTitle()); - mMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, mediaInfo.getDescription()); + @Override + public void playMedia(String url, String mimeType, String title, + String description, String iconSrc, boolean shouldLoop, + LaunchListener listener) { + MediaMetadata mMediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); + mMediaMetadata.putString(MediaMetadata.KEY_TITLE, title); + mMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, description); + + if (iconSrc != null) { + Uri iconUri = Uri.parse(iconSrc); + WebImage image = new WebImage(iconUri, 100, 100); + mMediaMetadata.addImage(image); + } - ImageInfo imageInfo = mediaInfo.getImages().get(0); - - if ( imageInfo.getUrl()!= null) { - Uri iconUri = Uri.parse(imageInfo.getUrl()); - WebImage image = new WebImage(iconUri, 100, 100); - mMediaMetadata.addImage(image); - } - - com.google.android.gms.cast.MediaInfo mediaInfo2 = new com.google.android.gms.cast.MediaInfo.Builder(mediaInfo.getUrl()) - .setContentType(mediaInfo.getMimeType()) - .setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_NONE) - .setMetadata(mMediaMetadata) - .build(); - - Cast.CastApi.launchApplication(mApiClient, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, false) - .setResultCallback(new ApplicationConnectionResultCallback(mediaInfo2, listener)); - } - }; + com.google.android.gms.cast.MediaInfo mediaInformation = new com.google.android.gms.cast.MediaInfo.Builder(url) + .setContentType(mimeType) + .setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_BUFFERED) + .setMetadata(mMediaMetadata) + .setStreamDuration(1000) + .setCustomData(null) + .build(); - runCommand(connectionListener); + playMedia(mediaInformation, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, listener); } - @Override - public void playMedia(final String url, final String mimeType, final String title, - final String description, final String iconSrc, final boolean shouldLoop, - final LaunchListener listener) { - - ConnectionListener connectionListener = new ConnectionListener() { + @Override + public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, LaunchListener listener) { + ImageInfo imageInfo = mediaInfo.getImages().get(0); + String iconSrc = imageInfo.getUrl(); + + playMedia(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, shouldLoop, listener); + } + + private void playMedia(final com.google.android.gms.cast.MediaInfo mediaInformation, String mediaAppId, final LaunchListener listener) { + ApplicationConnectionResultCallback webAppLaunchCallback = new ApplicationConnectionResultCallback(new LaunchWebAppListener() { @Override - public void onConnected() { - MediaMetadata mMediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); - mMediaMetadata.putString(MediaMetadata.KEY_TITLE, title); - mMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, description); - - if (iconSrc != null) { - Uri iconUri = Uri.parse(iconSrc); - WebImage image = new WebImage(iconUri, 100, 100); - mMediaMetadata.addImage(image); - } - - com.google.android.gms.cast.MediaInfo mediaInfo = new com.google.android.gms.cast.MediaInfo.Builder(url) - .setContentType(mimeType) - .setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_BUFFERED) - .setMetadata(mMediaMetadata) - .build(); - - Cast.CastApi.launchApplication(mApiClient, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, false) - .setResultCallback(new ApplicationConnectionResultCallback(mediaInfo, listener)); + public void onSuccess(final WebAppSession webAppSession) { + mMediaPlayer.load(mApiClient, mediaInformation, true).setResultCallback(new ResultCallback() { + + @Override + public void onResult(MediaChannelResult result) { + Status status = result.getStatus(); + + if (status.isSuccess()) { + webAppSession.launchSession.setSessionType(LaunchSessionType.Media); + + Util.postSuccess(listener, new MediaLaunchObject(webAppSession.launchSession, CastService.this)); + } + else { + Util.postError(listener, new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), status)); + } + } + }); } - }; - - runCommand(connectionListener); - } - - @Override - public void playMedia(final MediaInfo mediaInfo, final boolean shouldLoop, - final LaunchListener listener) { - - ConnectionListener connectionListener = new ConnectionListener() { @Override - public void onConnected() { - MediaMetadata mMediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); - mMediaMetadata.putString(MediaMetadata.KEY_TITLE, mediaInfo.getTitle()); - mMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, mediaInfo.getDescription()); - - - ImageInfo imageInfo = mediaInfo.getImages().get(0); - if (imageInfo.getUrl() != null) { - Uri iconUri = Uri.parse(imageInfo.getUrl()); - WebImage image = new WebImage(iconUri, 100, 100); - mMediaMetadata.addImage(image); - } - - com.google.android.gms.cast.MediaInfo mediaInfo2 = new com.google.android.gms.cast.MediaInfo.Builder(mediaInfo.getUrl()) - .setContentType(mediaInfo.getMimeType()) - .setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_BUFFERED) - .setMetadata(mMediaMetadata) - .build(); - - Cast.CastApi.launchApplication(mApiClient, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, false) - .setResultCallback(new ApplicationConnectionResultCallback(mediaInfo2, listener)); + public void onFailure(ServiceCommandError error) { + Util.postError(listener, error); } - }; + }); - runCommand(connectionListener); + launchingAppId = mediaAppId; + + Cast.CastApi.launchApplication(mApiClient, mediaAppId,false).setResultCallback(webAppLaunchCallback); } @Override public void closeMedia(final LaunchSession launchSession, final ResponseListener listener) { - if (!mApiClient.isConnected()) { - Util.postError(listener, new ServiceCommandError(-1, "The Google Cast API Client is not connected", null)); - return; - } - - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - Cast.CastApi.stopApplication(mApiClient).setResultCallback(new ResultCallback() { + Cast.CastApi.stopApplication(mApiClient, launchSession.getSessionId()).setResultCallback(new ResultCallback() { - @Override - public void onResult(Status result) { - if (result.isSuccess()) { - ((CastService) launchSession.getService()).detachMediaPlayer(); - - Util.postSuccess(listener, result); - } else { - Util.postError(listener, new ServiceCommandError(result.getStatusCode(), result.getStatusMessage(), result)); - } - } - }); + @Override + public void onResult(Status result) { + if (result.isSuccess()) { + Util.postSuccess(listener, result); + } else { + Util.postError(listener, new ServiceCommandError(result.getStatusCode(), result.getStatusMessage(), result)); + } } - }; - - runCommand(connectionListener); + }); } @Override @@ -633,61 +456,73 @@ public WebAppLauncher getWebAppLauncher() { @Override public CapabilityPriorityLevel getWebAppLauncherCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; + return CapabilityPriorityLevel.HIGH; } @Override - public void launchWebApp(final String webAppId, final WebAppSession.LaunchListener listener) { + public void launchWebApp(String webAppId, WebAppSession.LaunchListener listener) { launchWebApp(webAppId, true, listener); } + @Override + public void launchWebApp(final String webAppId, final boolean relaunchIfRunning, final WebAppSession.LaunchListener listener) { + launchingAppId = webAppId; + + Cast.CastApi.launchApplication(mApiClient, webAppId, relaunchIfRunning).setResultCallback( + new ApplicationConnectionResultCallback(new LaunchWebAppListener() { + + @Override + public void onSuccess(WebAppSession webAppSession) { + Util.postSuccess(listener, webAppSession); + } + + @Override + public void onFailure(ServiceCommandError error) { + Util.postError(listener, error); + } + }) + ); + } + + @Override + public void launchWebApp(String webAppId, JSONObject params, WebAppSession.LaunchListener listener) { + Util.postError(listener, ServiceCommandError.notSupported()); + } + + @Override + public void launchWebApp(String webAppId, JSONObject params, boolean relaunchIfRunning, WebAppSession.LaunchListener listener) { + Util.postError(listener, ServiceCommandError.notSupported()); + } + @Override public void joinWebApp(final LaunchSession webAppLaunchSession, final WebAppSession.LaunchListener listener) { - final ResultCallback resultCallback = new ResultCallback() { - + ApplicationConnectionResultCallback webAppLaunchCallback = new ApplicationConnectionResultCallback(new LaunchWebAppListener() { + @Override - public void onResult(Cast.ApplicationConnectionResult result) { - Status status = result.getStatus(); - - if (status.isSuccess()) { - final CastWebAppSession webAppSession; + public void onSuccess(final WebAppSession webAppSession) { + webAppSession.connect(new ResponseListener() { - if (sessions.containsKey(webAppLaunchSession.getAppId())) { - webAppSession = sessions.get(webAppLaunchSession.getAppId()); - } - else { - webAppSession = new CastWebAppSession(webAppLaunchSession, CastService.this); - sessions.put(webAppLaunchSession.getAppId(), webAppSession); + @Override + public void onSuccess(Object object) { + Util.postSuccess(listener, webAppSession); } - webAppSession.join(new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - Util.postSuccess(listener, webAppSession); - } - }); - } - else { - Util.postError(listener, new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), result)); - } + @Override + public void onError(ServiceCommandError error) { + Util.postError(listener, error); + } + }); } - }; - - ConnectionListener connectionListener = new ConnectionListener() { @Override - public void onConnected() { - Cast.CastApi.joinApplication(mApiClient, webAppLaunchSession.getAppId(), webAppLaunchSession.getSessionId()).setResultCallback(resultCallback); + public void onFailure(ServiceCommandError error) { + Util.postError(listener, error); } - }; - - runCommand(connectionListener); + }); + + launchingAppId = webAppLaunchSession.getAppId(); + + Cast.CastApi.joinApplication(mApiClient, webAppLaunchSession.getAppId()).setResultCallback(webAppLaunchCallback); } @Override @@ -699,83 +534,22 @@ public void joinWebApp(String webAppId, WebAppSession.LaunchListener listener) { joinWebApp(launchSession, listener); } - @Override - public void launchWebApp(String webAppId, JSONObject params, WebAppSession.LaunchListener listener) { - launchWebApp(webAppId, true, listener); - } - - @Override - public void launchWebApp(final String webAppId, final boolean relaunchIfRunning, final WebAppSession.LaunchListener listener) { - Log.d(TAG, "CastService::launchWebApp() | webAppId = " + webAppId); - - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - Cast.CastApi.launchApplication(mApiClient, webAppId, relaunchIfRunning) - .setResultCallback( - new ResultCallback() { - - @Override - public void onResult(Cast.ApplicationConnectionResult result) { - Status status = result.getStatus(); - - if (status.isSuccess()) { - currentAppId = webAppId; - - LaunchSession launchSession = LaunchSession.launchSessionForAppId(webAppId); - launchSession.setService(CastService.this); - launchSession.setSessionType(LaunchSessionType.WebApp); - - CastWebAppSession webAppSession = sessions.get(webAppId); - - if (webAppSession == null) { - webAppSession = new CastWebAppSession(launchSession, CastService.this); - sessions.put(webAppId, webAppSession); - } - - Util.postSuccess(listener, webAppSession); - } - else { - Util.postError(listener, new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), result)); - } - } - }); - } - }; - - runCommand(connectionListener); - } - - @Override - public void launchWebApp(String webAppId, JSONObject params, boolean relaunchIfRunning, WebAppSession.LaunchListener listener) { - launchWebApp(webAppId, relaunchIfRunning, listener); - } - @Override public void closeWebApp(LaunchSession launchSession, final ResponseListener listener) { - final ResultCallback resultCallback = new ResultCallback() { + Cast.CastApi.stopApplication(mApiClient).setResultCallback(new ResultCallback() { + @Override - public void onResult(final Status result) { - if (result.isSuccess()) + public void onResult(Status status) { + if (status.isSuccess()) { Util.postSuccess(listener, null); - else - Util.postError(listener, new ServiceCommandError(0, "TV Error", null)); - } - }; - - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - Cast.CastApi.stopApplication(mApiClient).setResultCallback(resultCallback); + } + else { + Util.postError(listener, new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), status)); + } } - }; - - runCommand(connectionListener); + }); } - @Override public VolumeControl getVolumeControl() { return this; @@ -783,7 +557,7 @@ public VolumeControl getVolumeControl() { @Override public CapabilityPriorityLevel getVolumeControlCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; + return CapabilityPriorityLevel.HIGH; } @Override @@ -792,31 +566,19 @@ public void volumeUp(final ResponseListener listener) { @Override public void onSuccess(final Float volume) { - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - try { - float newVolume; - if (volume + VOLUME_INCREMENT >= 1.0) { - newVolume = 1; - } - else { - newVolume = (float) (volume + VOLUME_INCREMENT); - } - - Cast.CastApi.setVolume(mApiClient, newVolume); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (IllegalStateException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } - }; - - runCommand(connectionListener); + 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 @@ -832,111 +594,63 @@ public void volumeDown(final ResponseListener listener) { @Override public void onSuccess(final Float volume) { - ConnectionListener connectionListener = new ConnectionListener() { + if (volume <= 0.0) { + Util.postSuccess(listener, null); + } + else { + float newVolume = (float)(volume - 0.01); - @Override - public void onConnected() { - float newVolume; - if (volume - VOLUME_INCREMENT <= 0) { - newVolume = 0; - } - else { - newVolume = (float) (volume - VOLUME_INCREMENT); - } - - try { - Cast.CastApi.setVolume(mApiClient, newVolume); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (IllegalStateException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } - }; - - runCommand(connectionListener); + 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); + Util.postError(listener, error); } }); } @Override - public void setVolume(final float volume, ResponseListener listener) { - ConnectionListener connectionListener = new ConnectionListener() { + public void setVolume(float volume, ResponseListener listener) { + try { + Cast.CastApi.setVolume(mApiClient, volume); - @Override - public void onConnected() { - try { - Cast.CastApi.setVolume(mApiClient, volume); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (IllegalStateException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } - }; - - runCommand(connectionListener); + Util.postSuccess(listener, null); + } catch (IOException e) { + Util.postError(listener, new ServiceCommandError(0, "setting volume level failed", null)); + } } @Override - public void getVolume(final VolumeListener listener) { - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - float volume = (float) Cast.CastApi.getVolume(mApiClient); - Util.postSuccess(listener, volume); - } - }; - - runCommand(connectionListener); + public void getVolume(VolumeListener listener) { + Util.postSuccess(listener, currentVolumeLevel); } @Override - public void setMute(final boolean isMute, ResponseListener listener) { - ConnectionListener connectionListener = new ConnectionListener() { + public void setMute(boolean isMute, ResponseListener listener) { + try { + Cast.CastApi.setMute(mApiClient, isMute); - @Override - public void onConnected() { - try { - Cast.CastApi.setMute(mApiClient, isMute); - } catch (IllegalStateException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } - }; - - runCommand(connectionListener); + Util.postSuccess(listener, null); + } catch (IOException e) { + Util.postError(listener, new ServiceCommandError(0, "setting mute status failed", null)); + } } @Override public void getMute(final MuteListener listener) { - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - boolean isMute = Cast.CastApi.isMute(mApiClient); - Util.postSuccess(listener, isMute); - } - }; - - runCommand(connectionListener); + Util.postSuccess(listener, currentMuteStatus); } @Override public ServiceSubscription subscribeVolume(VolumeListener listener) { - URLServiceSubscription request = new URLServiceSubscription(this, VOLUME, null, null); + URLServiceSubscription request = new URLServiceSubscription(this, CAST_SERVICE_VOLUME_SUBSCRIPTION_NAME, null, null); request.addListener(listener); addSubscription(request); @@ -945,7 +659,7 @@ public ServiceSubscription subscribeVolume(VolumeListener listen @Override public ServiceSubscription subscribeMute(MuteListener listener) { - URLServiceSubscription request = new URLServiceSubscription(this, MUTE, null, null); + URLServiceSubscription request = new URLServiceSubscription(this, CAST_SERVICE_MUTE_SUBSCRIPTION_NAME, null, null); request.addListener(listener); addSubscription(request); @@ -999,64 +713,37 @@ public void onApplicationDisconnected(int statusCode) { @Override public void onApplicationStatusChanged() { - ConnectionListener connectionListener = new ConnectionListener() { + ApplicationMetadata applicationMetadata = Cast.CastApi.getApplicationMetadata(mApiClient); - @Override - public void onConnected() { - ApplicationMetadata applicationMetadata = Cast.CastApi.getApplicationMetadata(mApiClient); - - if (applicationMetadata != null) - currentAppId = applicationMetadata.getApplicationId(); - } - }; - - runCommand(connectionListener); + if (applicationMetadata != null) + currentAppId = applicationMetadata.getApplicationId(); } @Override public void onVolumeChanged() { + try { + currentVolumeLevel = (float) Cast.CastApi.getVolume(mApiClient); + currentMuteStatus = Cast.CastApi.isMute(mApiClient); + } catch (IllegalStateException e) { + e.printStackTrace(); + } + if (subscriptions.size() > 0) { for (URLServiceSubscription subscription: subscriptions) { - if (subscription.getTarget().equalsIgnoreCase(VOLUME)) { + if (subscription.getTarget().equals(CAST_SERVICE_VOLUME_SUBSCRIPTION_NAME)) { for (int i = 0; i < subscription.getListeners().size(); i++) { @SuppressWarnings("unchecked") - final ResponseListener listener = (ResponseListener) subscription.getListeners().get(i); + ResponseListener listener = (ResponseListener) subscription.getListeners().get(i); - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - try { - float volume = (float) Cast.CastApi.getVolume(mApiClient); - Util.postSuccess(listener, volume); - } catch (IllegalStateException e) { - e.printStackTrace(); - } - } - }; - - runCommand(connectionListener); + Util.postSuccess(listener, currentVolumeLevel); } } - else if (subscription.getTarget().equalsIgnoreCase(MUTE)) { + else if (subscription.getTarget().equals(CAST_SERVICE_MUTE_SUBSCRIPTION_NAME)) { for (int i = 0; i < subscription.getListeners().size(); i++) { @SuppressWarnings("unchecked") - final ResponseListener listener = (ResponseListener) subscription.getListeners().get(i); - - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - try { - boolean isMute = Cast.CastApi.isMute(mApiClient); - Util.postSuccess(listener, isMute); - } catch (IllegalStateException e) { - e.printStackTrace(); - } - } - }; + ResponseListener listener = (ResponseListener) subscription.getListeners().get(i); - runCommand(connectionListener); + Util.postSuccess(listener, currentMuteStatus); } } } @@ -1100,16 +787,11 @@ public void run() { @Override public void onConnected(Bundle connectionHint) { Log.d("Connect SDK", "ConnectionCallbacks.onConnected"); - isConnected = true; - if (!commandQueue.isEmpty()) { - LinkedHashSet tempHashSet = new LinkedHashSet(commandQueue); - for (ConnectionListener listener : tempHashSet) { - listener.onConnected(); - commandQueue.remove(listener); - } - } - + attachMediaPlayer(); + + connected = true; + reportConnected(true); } } @@ -1120,7 +802,7 @@ public void onConnectionFailed(final ConnectionResult result) { Log.d("Connect SDK", "ConnectionFailedListener.onConnectionFailed"); detachMediaPlayer(); - isConnected = false; + connected = false; Util.runOnUI(new Runnable() { @@ -1136,56 +818,49 @@ public void run() { } } - private final class ApplicationConnectionResultCallback implements + private class ApplicationConnectionResultCallback implements ResultCallback { + LaunchWebAppListener listener; - com.google.android.gms.cast.MediaInfo mediaInfo; - LaunchListener listener; - - public ApplicationConnectionResultCallback(com.google.android.gms.cast.MediaInfo mediaInfo, LaunchListener listener) { - this.mediaInfo = mediaInfo; + public ApplicationConnectionResultCallback(LaunchWebAppListener listener) { this.listener = listener; } @Override public void onResult(ApplicationConnectionResult result) { Status status = result.getStatus(); - Log.d("Connect SDK", "ApplicationConnectionResultCallback.onResult: statusCode: " + status.getStatusCode()); - + if (status.isSuccess()) { - ApplicationMetadata applicationMetadata = result.getApplicationMetadata(); - currentAppId = applicationMetadata.getApplicationId(); - - String sessionId = result.getSessionId(); - String applicationStatus = result.getApplicationStatus(); - boolean wasLaunched = result.getWasLaunched(); - Log.d("Connect SDK", "application name: " + applicationMetadata.getName() - + ", status: " + applicationStatus + ", sessionId: " + sessionId - + ", wasLaunched: " + wasLaunched); - - attachMediaPlayer(); - playMedia(mediaInfo, listener); - - LaunchSession launchSession = LaunchSession.launchSessionForAppId(applicationMetadata.getApplicationId()); - launchSession.setService(CastService.this); - launchSession.setSessionType(LaunchSessionType.Media); - - CastWebAppSession webAppSession = sessions.get(applicationMetadata.getApplicationId()); - - if (webAppSession == null) { - webAppSession = new CastWebAppSession(launchSession, CastService.this); - sessions.put(applicationMetadata.getApplicationId(), webAppSession); - } - } else { - Util.postError(listener, new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), status)); + ApplicationMetadata applicationMetadata = result.getApplicationMetadata(); + currentAppId = applicationMetadata.getApplicationId(); + + LaunchSession launchSession = LaunchSession.launchSessionForAppId(applicationMetadata.getApplicationId()); + launchSession.setAppName(applicationMetadata.getName()); + launchSession.setSessionId(result.getSessionId()); + launchSession.setSessionType(LaunchSessionType.WebApp); + launchSession.setService(CastService.this); + + CastWebAppSession webAppSession = new CastWebAppSession(launchSession, CastService.this); + webAppSession.setMetadata(applicationMetadata); + + sessions.put(applicationMetadata.getApplicationId(), webAppSession); + + if (listener != null) { + listener.onSuccess(webAppSession); +// Util.postSuccess(listener, webAppSession); + } + + launchingAppId = null; + } + else { + if (listener != null) { + listener.onFailure(new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), status)); +// Util.postError(listener, new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), status)); + } } } } - public CastDevice getDevice() { - return castDevice; - } - @Override public void getPlayState(PlayStateListener listener) { if (mMediaPlayer == null) { @@ -1226,25 +901,6 @@ public GoogleApiClient getApiClient() { return mApiClient; } - @Override - public void setServiceDescription(ServiceDescription serviceDescription) { - super.setServiceDescription(serviceDescription); - - if (serviceDescription instanceof CastServiceDescription) - this.castDevice = ((CastServiceDescription)serviceDescription).getCastDevice(); - - if (this.castDevice != null) { - Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions - .builder(castDevice, mCastClientListener); - - mApiClient = new GoogleApiClient.Builder(DiscoveryManager.getInstance().getContext()) - .addApi(Cast.API, apiOptionsBuilder.build()) - .addConnectionCallbacks(mConnectionCallbacks) - .addOnConnectionFailedListener(mConnectionFailedListener) - .build(); - } - } - ////////////////////////////////////////////////// // Device Service Methods ////////////////////////////////////////////////// @@ -1253,10 +909,10 @@ public boolean isConnectable() { return true; } - @Override - public boolean isConnected() { - return isConnected; - } + @Override + public boolean isConnected() { + return connected; + } @Override public ServiceSubscription subscribePlayState(PlayStateListener listener) { @@ -1277,16 +933,6 @@ public void unsubscribe(URLServiceSubscription subscription) { subscriptions.remove(subscription); } - public void runCommand(ConnectionListener connectionListener) { - if (mApiClient.isConnected()) { - connectionListener.onConnected(); - } - else { - connect(); - commandQueue.add(connectionListener); - } - } - public List> getSubscriptions() { return subscriptions; } diff --git a/src/com/connectsdk/service/sessions/CastWebAppSession.java b/src/com/connectsdk/service/sessions/CastWebAppSession.java index 4e157ee8..48a3773f 100644 --- a/src/com/connectsdk/service/sessions/CastWebAppSession.java +++ b/src/com/connectsdk/service/sessions/CastWebAppSession.java @@ -28,23 +28,21 @@ import com.connectsdk.core.Util; import com.connectsdk.service.CastService; -import com.connectsdk.service.CastService.ConnectionListener; import com.connectsdk.service.CastServiceChannel; import com.connectsdk.service.DeviceService; -import com.connectsdk.service.capability.MediaControl; import com.connectsdk.service.capability.MediaPlayer; import com.connectsdk.service.capability.listeners.ResponseListener; import com.connectsdk.service.command.ServiceCommandError; import com.connectsdk.service.command.URLServiceSubscription; +import com.google.android.gms.cast.ApplicationMetadata; import com.google.android.gms.cast.Cast; -import com.google.android.gms.cast.Cast.MessageReceivedCallback; -import com.google.android.gms.cast.CastDevice; import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.Status; public class CastWebAppSession extends WebAppSession { - protected CastService service; - protected CastServiceChannel mCastServiceChannel; + private CastService service; + private CastServiceChannel castServiceChannel; + private ApplicationMetadata metadata; public CastWebAppSession(LaunchSession launchSession, DeviceService service) { super(launchSession, service); @@ -54,43 +52,43 @@ public CastWebAppSession(LaunchSession launchSession, DeviceService service) { @Override public void connect(final ResponseListener listener) { - if (mCastServiceChannel != null) { + if (castServiceChannel != null) { disconnectFromWebApp(launchSession); } - mCastServiceChannel = new CastServiceChannel(launchSession.getAppId(), this); + castServiceChannel = new CastServiceChannel(launchSession.getAppId(), this); - ConnectionListener connectionListener = new ConnectionListener() { + try { + Cast.CastApi.removeMessageReceivedCallbacks(service.getApiClient(), castServiceChannel.getNamespace()); - @Override - public void onConnected() { - try { - Cast.CastApi.setMessageReceivedCallbacks(service.getApiClient(), - mCastServiceChannel.getNamespace(), - mCastServiceChannel); - - Util.postSuccess(listener, null); - } catch (IOException e) { - Util.postError(listener, new ServiceCommandError(0, "Failed to create channel", null)); - } - } - }; - - service.runCommand(connectionListener); + Cast.CastApi.setMessageReceivedCallbacks(service.getApiClient(), + castServiceChannel.getNamespace(), + castServiceChannel); + + Util.postSuccess(listener, null); + } catch (IOException e) { + castServiceChannel = null; + + Util.postError(listener, new ServiceCommandError(0, "Failed to create channel", null)); + } } @Override - public void join(final ResponseListener connectionListener) { + public void join(ResponseListener connectionListener) { connect(connectionListener); } - public MessageReceivedCallback messageReceivedCallback = new MessageReceivedCallback() { - - @Override - public void onMessageReceived(CastDevice castDevice, String namespace, String message) { - + public void disconnectFromWebApp(LaunchSession launchSession) { + if (castServiceChannel == null) + return; + + try { + Cast.CastApi.removeMessageReceivedCallbacks(service.getApiClient(), castServiceChannel.getNamespace()); + castServiceChannel = null; + } catch (IOException e) { + Log.e("Connect SDK", "Exception while removing application", e); } - }; + } public void handleAppClose() { for (URLServiceSubscription subscription: service.getSubscriptions()) { @@ -108,75 +106,30 @@ public void handleAppClose() { } } - public void disconnectFromWebApp(LaunchSession launchSession) { - if (service.getApiClient() == null) - return; - - if (mCastServiceChannel == null) - return; - - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - try { - Cast.CastApi.removeMessageReceivedCallbacks(service.getApiClient(), mCastServiceChannel.getNamespace()); - mCastServiceChannel = null; - } catch (IOException e) { - Log.e("Connect SDK", "Exception while removing application", e); - } - } - }; - - service.runCommand(connectionListener); - } - - @Override - public MediaControl getMediaControl() { - return this; - } - - @Override - public CapabilityPriorityLevel getMediaControlCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - @Override - public void sendMessage(final String message, final ResponseListener listener) { + public void sendMessage(String message, final ResponseListener listener) { if (message == null) { Util.postError(listener, new ServiceCommandError(0, "Cannot send null message", null)); return; } - if (mCastServiceChannel == null) { - Util.postError(listener, new ServiceCommandError(0, "Must connect web app first", null)); + if (castServiceChannel == null) { + Util.postError(listener, new ServiceCommandError(0, "Cannot send a message to the web app without first connecting", null)); return; } - Log.d(Util.T, "CastService::sendMessage() | mCastServiceChannel.getNamespace() = " + mCastServiceChannel.getNamespace()); - ConnectionListener connectionListener = new ConnectionListener() { - - @Override - public void onConnected() { - Cast.CastApi.sendMessage(service.getApiClient(), mCastServiceChannel.getNamespace(), message) - .setResultCallback(new ResultCallback() { - - @Override - public void onResult(Status result) { - if (result.isSuccess()) { - Log.d("Connect SDK", "Sending message succeeded"); - Util.postSuccess(listener, result); - } - else { - Log.e("Connect SDK", "Sending message failed"); - Util.postError(listener, new ServiceCommandError(result.getStatusCode(), result.toString(), result)); - } - } - }); - } - }; - - service.runCommand(connectionListener); + Cast.CastApi.sendMessage(service.getApiClient(), castServiceChannel.getNamespace(), message).setResultCallback(new ResultCallback() { + + @Override + public void onResult(Status result) { + if (result.isSuccess()) { + Util.postSuccess(listener, null); + } + else { + Util.postError(listener, new ServiceCommandError(result.getStatusCode(), result.toString(), result)); + } + } + }); } @Override @@ -203,7 +156,7 @@ public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { } @Override - public void playMedia( String url, String mimeType, String title, String description, String iconSrc, boolean shouldLoop, MediaPlayer.LaunchListener listener) { + public void playMedia(String url, String mimeType, String title, String description, String iconSrc, boolean shouldLoop, MediaPlayer.LaunchListener listener) { service.playMedia(url, mimeType, title, description, iconSrc, shouldLoop, listener); } @@ -211,4 +164,12 @@ public void playMedia( String url, String mimeType, String title, String descrip public void closeMedia(LaunchSession launchSession, ResponseListener listener) { close(listener); } + + public ApplicationMetadata getMetadata() { + return metadata; + } + + public void setMetadata(ApplicationMetadata metadata) { + this.metadata = metadata; + } } From 85000f01d1c5ec41cc0864da993325933d3929d8 Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Wed, 13 Aug 2014 17:16:25 +0900 Subject: [PATCH 10/76] Fixed missing merge #138 --- src/com/connectsdk/service/CastService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/com/connectsdk/service/CastService.java b/src/com/connectsdk/service/CastService.java index f2c86107..c9a138d5 100644 --- a/src/com/connectsdk/service/CastService.java +++ b/src/com/connectsdk/service/CastService.java @@ -431,7 +431,12 @@ public void onFailure(ServiceCommandError error) { launchingAppId = mediaAppId; - Cast.CastApi.launchApplication(mApiClient, mediaAppId,false).setResultCallback(webAppLaunchCallback); + boolean relaunchIfRunning = false; + + if (Cast.CastApi.getApplicationStatus(mApiClient) == null || (!mediaAppId.equals(currentAppId))) + relaunchIfRunning = true; + + Cast.CastApi.launchApplication(mApiClient, mediaAppId, relaunchIfRunning).setResultCallback(webAppLaunchCallback); } @Override From d2327675c1f431e82a9b43ad9b88053b7b7ab3f7 Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Wed, 13 Aug 2014 19:50:26 +0900 Subject: [PATCH 11/76] Fixed Chromecast: Callback behavior for onConnectionSuspended needs to change #145 --- src/com/connectsdk/service/CastService.java | 62 ++++++++++----------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/src/com/connectsdk/service/CastService.java b/src/com/connectsdk/service/CastService.java index c9a138d5..7ca409f9 100644 --- a/src/com/connectsdk/service/CastService.java +++ b/src/com/connectsdk/service/CastService.java @@ -100,6 +100,7 @@ public interface LaunchWebAppListener{ float currentVolumeLevel; boolean currentMuteStatus; + boolean mWaitingForReconnect; public CastService(ServiceDescription serviceDescription, ServiceConfig serviceConfig) { super(serviceDescription, serviceConfig); @@ -110,6 +111,8 @@ public CastService(ServiceDescription serviceDescription, ServiceConfig serviceC sessions = new HashMap(); subscriptions = new ArrayList>(); + + mWaitingForReconnect = false; } @Override @@ -161,6 +164,7 @@ public void disconnect() { return; connected = false; + mWaitingForReconnect = false; Cast.CastApi.leaveApplication(mApiClient); mApiClient.disconnect(); @@ -714,6 +718,8 @@ public void onApplicationDisconnected(int statusCode) { return; webAppSession.handleAppClose(); + + currentAppId = null; } @Override @@ -761,43 +767,32 @@ private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks public void onConnectionSuspended(final int cause) { Log.d("Connect SDK", "ConnectionCallbacks.onConnectionSuspended"); - disconnect(); - detachMediaPlayer(); - - Util.runOnUI(new Runnable() { - @Override - public void run() { - if (listener != null) { - ServiceCommandError error; - - switch (cause) { - case GoogleApiClient.ConnectionCallbacks.CAUSE_NETWORK_LOST: - error = new ServiceCommandError(cause, "Peer device connection was lost", null); - break; - - case GoogleApiClient.ConnectionCallbacks.CAUSE_SERVICE_DISCONNECTED: - error = new ServiceCommandError(cause, "The service has been killed", null); - break; - - default: - error = new ServiceCommandError(cause, "Unknown connection error", null); - } - - listener.onDisconnect(CastService.this, error); - } - } - }); + mWaitingForReconnect = true; } @Override public void onConnected(Bundle connectionHint) { - Log.d("Connect SDK", "ConnectionCallbacks.onConnected"); - - attachMediaPlayer(); + Log.d("Connect SDK", "ConnectionCallbacks.onConnected, wasWaitingForReconnect: " + mWaitingForReconnect); - connected = true; + if (mWaitingForReconnect) { + mWaitingForReconnect = false; + reconnectChannels(); + } + else { + attachMediaPlayer(); + + connected = true; + + reportConnected(true); + } + } + + private void reconnectChannels() { + if (Cast.CastApi.getApplicationStatus(mApiClient) != null && currentAppId != null) { + CastWebAppSession webAppSession = sessions.get(currentAppId); - reportConnected(true); + webAppSession.connect(null); + } } } @@ -808,7 +803,8 @@ public void onConnectionFailed(final ConnectionResult result) { detachMediaPlayer(); connected = false; - + mWaitingForReconnect = false; + Util.runOnUI(new Runnable() { @Override @@ -852,7 +848,6 @@ public void onResult(ApplicationConnectionResult result) { if (listener != null) { listener.onSuccess(webAppSession); -// Util.postSuccess(listener, webAppSession); } launchingAppId = null; @@ -860,7 +855,6 @@ public void onResult(ApplicationConnectionResult result) { else { if (listener != null) { listener.onFailure(new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), status)); -// Util.postError(listener, new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), status)); } } } From 1c6a0bfc854ce7ef742b46b3b115b7fd95f9d6e0 Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Wed, 13 Aug 2014 20:10:25 +0900 Subject: [PATCH 12/76] Cleanup media player when the service is disconnected --- src/com/connectsdk/service/CastService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/com/connectsdk/service/CastService.java b/src/com/connectsdk/service/CastService.java index 7ca409f9..59fd22b0 100644 --- a/src/com/connectsdk/service/CastService.java +++ b/src/com/connectsdk/service/CastService.java @@ -166,6 +166,8 @@ public void disconnect() { connected = false; mWaitingForReconnect = false; + detachMediaPlayer(); + Cast.CastApi.leaveApplication(mApiClient); mApiClient.disconnect(); mApiClient = null; From dbc29b10715c2ba5224860c596ec8e5d54458362 Mon Sep 17 00:00:00 2001 From: Simon Gladkoskok Date: Wed, 13 Aug 2014 16:09:42 -0700 Subject: [PATCH 13/76] Add comments ImageInfo and MediaInfo for documentation --- src/com/connectsdk/core/AppInfo.java | 56 ++++++++++------ src/com/connectsdk/core/ImageInfo.java | 62 +++++++++++++++++- src/com/connectsdk/core/MediaInfo.java | 89 ++++++++++++++++++++++++++ 3 files changed, 185 insertions(+), 22 deletions(-) diff --git a/src/com/connectsdk/core/AppInfo.java b/src/com/connectsdk/core/AppInfo.java index 8930df4a..054ebbd5 100644 --- a/src/com/connectsdk/core/AppInfo.java +++ b/src/com/connectsdk/core/AppInfo.java @@ -24,8 +24,9 @@ import org.json.JSONObject; /** - * Normalized reference object for information about a DeviceService's app. This object will, in most cases, be used to launch apps. - * + * Normalized reference object for information about a DeviceService's app. This + * object will, in most cases, be used to launch apps. + * * In some cases, all that is needed to launch an app is the app id. */ public class AppInfo implements JSONSerializable { @@ -33,51 +34,64 @@ public class AppInfo implements JSONSerializable { String id; String name; JSONObject raw; + // @endcond - + /** * Default constructor method. */ - public AppInfo() { } - + public AppInfo() { + } + /** * Default constructor method. * - * @param id App id to launch + * @param id + * App id to launch */ public AppInfo(String id) { this.id = id; } - + /** - * Gets the ID of the app on the first screen device. Format is different depending on the platform. (ex. youtube.leanback.v4, 0000001134, netflix, etc). + * Gets the ID of the app on the first screen device. Format is different + * depending on the platform. (ex. youtube.leanback.v4, 0000001134, netflix, + * etc). */ public String getId() { return id; } /** - * Sets the ID of the app on the first screen device. Format is different depending on the platform. (ex. youtube.leanback.v4, 0000001134, netflix, etc). + * Sets the ID of the app on the first screen device. Format is different + * depending on the platform. (ex. youtube.leanback.v4, 0000001134, netflix, + * etc). */ public void setId(String id) { this.id = id; } - /** Gets the user-friendly name of the app (ex. YouTube, Browser, Netflix, etc). */ + /** + * Gets the user-friendly name of the app (ex. YouTube, Browser, Netflix, + * etc). + */ public String getName() { return name; } - - /** Sets the user-friendly name of the app (ex. YouTube, Browser, Netflix, etc). */ + + /** + * Sets the user-friendly name of the app (ex. YouTube, Browser, Netflix, + * etc). + */ public void setName(String name) { this.name = name.trim(); } - + /** Gets the raw data from the first screen device about the app. */ public JSONObject getRawData() { return raw; } - + /** Sets the raw data from the first screen device about the app. */ public void setRawData(JSONObject data) { raw = data; @@ -87,19 +101,21 @@ public void setRawData(JSONObject data) { @Override public JSONObject toJSONObject() throws JSONException { JSONObject obj = new JSONObject(); - + obj.put("name", name); obj.put("id", id); - + return obj; } + // @endcond - + /** * Compares two AppInfo objects. - * - * @param o Other AppInfo object to compare. - * + * + * @param o + * Other AppInfo object to compare. + * * @return true if both AppInfo id values are equal */ @Override diff --git a/src/com/connectsdk/core/ImageInfo.java b/src/com/connectsdk/core/ImageInfo.java index e45d4c32..913dbe39 100644 --- a/src/com/connectsdk/core/ImageInfo.java +++ b/src/com/connectsdk/core/ImageInfo.java @@ -1,19 +1,36 @@ package com.connectsdk.core; -public class ImageInfo { +/** + * Normalized reference object for information about an image file. This object can be used to represent a media file (ex. icon, poster) + * + */ +public class ImageInfo { + + /** + * Default constructor method. + * @param url + */ + public ImageInfo(String url) { super(); this.url = url; } + /** + * Default constructor method. + * @param url + * add type of file, width and height of image. + */ + public ImageInfo(String url, ImageType type, int width, int height) { this(url); this.type = type; this.width = width; this.height = height; } - + + public enum ImageType { Thumb, Video_Poster, Album_Art, Unknown; } @@ -23,33 +40,74 @@ public enum ImageType { private int width; private int height; + /** + * Gets URL address of an image file. + * + */ + public String getUrl() { return url; } + + + /** + * Sets URL address of an image file. + * + */ public void setUrl(String url) { this.url = url; } + + /** + * Gets a type of an image file. + * + */ public ImageType getType() { return type; } + + /** + * Sets a type of an image file. + * + */ public void setType(ImageType type) { this.type = type; } + + /** + * Gets a width of an image. + * + */ public int getWidth() { return width; } + + /** + * Sets a width of an image. + * + */ public void setWidth(int width) { this.width = width; } + + /** + * Gets a height of an image. + * + */ public int getHeight() { return height; } + + /** + * Sets a height of an image. + * + */ public void setHeight(int height) { this.height = height; diff --git a/src/com/connectsdk/core/MediaInfo.java b/src/com/connectsdk/core/MediaInfo.java index b827df71..6c2301dd 100644 --- a/src/com/connectsdk/core/MediaInfo.java +++ b/src/com/connectsdk/core/MediaInfo.java @@ -1,10 +1,20 @@ package com.connectsdk.core; import java.util.ArrayList; + +/** + * Normalized reference object for information about a media to display. This object can be used to pass as a parameter to displayImage or playMedia. + * + */ + import java.util.List; public class MediaInfo { + /** + * Default constructor method. + */ + public MediaInfo(String url, String mimeType, String title, String description) { super(); @@ -14,65 +24,144 @@ public MediaInfo(String url, String mimeType, String title, this.description = description; } + /** + * Default constructor method. + * + * @param allImages + * list of imageInfo objects where [0] is icon, [1] is poster + */ + public MediaInfo(String url, String mimeType, String title, String description, List allImages) { this(url, mimeType, title, description); this.allImages = allImages; } + // @cond INTERNAL private String url, mimeType, description, title; private List allImages; private long duration; + // @endcond + + /** + * Gets type of a media file. + * + * @return + */ + public String getMimeType() { return mimeType; } + /** + * Sets type of a media file. + * + * @param mimeType + */ + public void setMimeType(String mimeType) { this.mimeType = mimeType; } + /** + * Gets title for a media file. + * + * @return + */ + public String getTitle() { return title; } + /** + * Sets title of a media file. + * + * @param title + */ + public void setTitle(String title) { this.title = title; } + /** + * Gets description for a media. + * + */ + public String getDescription() { return description; } + /** + * Sets description for a media. + * @param description + */ + public void setDescription(String description) { this.description = description; } + + /** + * Gets list of ImageInfo objects for images representing a media (ex. icon, poster). Where first ([0]) is icon image, and second ([1]) is poster image. + */ public List getImages() { return allImages; } + /** + * Sets list of ImageInfo objects for images representing a media (ex. icon, poster). Where first ([0]) is icon image, and second ([1]) is poster image. + + * @param images + */ + public void setImages(List images) { this.allImages = images; } + + /** + * Gets duration of a media file. + * @return + */ public long getDuration() { return duration; } + + /** + * Sets duration of a media file. + * @param duration + */ public void setDuration(long duration) { this.duration = duration; } + + /** + * Gets URL address of a media file. + * @return + */ public String getUrl() { return url; } + + /** + * Sets URL address of a media file. + * @param url + */ public void setUrl(String url) { this.url = url; } + + /** + * Stores ImageInfo objects. + * @param images + */ public void addImages(ImageInfo... images) { From 679f989df98d467f4e99b8c7c1766b145bd07ac2 Mon Sep 17 00:00:00 2001 From: Simon Gladkoskok Date: Thu, 14 Aug 2014 10:33:59 -0700 Subject: [PATCH 14/76] minor --- src/com/connectsdk/core/ImageInfo.java | 2 +- src/com/connectsdk/core/MediaInfo.java | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/com/connectsdk/core/ImageInfo.java b/src/com/connectsdk/core/ImageInfo.java index 913dbe39..3e368bed 100644 --- a/src/com/connectsdk/core/ImageInfo.java +++ b/src/com/connectsdk/core/ImageInfo.java @@ -19,7 +19,7 @@ public ImageInfo(String url) { /** * Default constructor method. - * @param url + * @param url, type, width, height * add type of file, width and height of image. */ diff --git a/src/com/connectsdk/core/MediaInfo.java b/src/com/connectsdk/core/MediaInfo.java index 6c2301dd..1b29f030 100644 --- a/src/com/connectsdk/core/MediaInfo.java +++ b/src/com/connectsdk/core/MediaInfo.java @@ -49,7 +49,7 @@ public MediaInfo(String url, String mimeType, String title, /** * Gets type of a media file. * - * @return + * */ public String getMimeType() { @@ -59,7 +59,7 @@ public String getMimeType() { /** * Sets type of a media file. * - * @param mimeType + * */ public void setMimeType(String mimeType) { @@ -69,7 +69,7 @@ public void setMimeType(String mimeType) { /** * Gets title for a media file. * - * @return + * */ public String getTitle() { @@ -79,7 +79,7 @@ public String getTitle() { /** * Sets title of a media file. * - * @param title + * */ public void setTitle(String title) { @@ -97,7 +97,7 @@ public String getDescription() { /** * Sets description for a media. - * @param description + * */ public void setDescription(String description) { @@ -115,7 +115,7 @@ public List getImages() { /** * Sets list of ImageInfo objects for images representing a media (ex. icon, poster). Where first ([0]) is icon image, and second ([1]) is poster image. - * @param images + * */ public void setImages(List images) { @@ -124,7 +124,7 @@ public void setImages(List images) { /** * Gets duration of a media file. - * @return + * */ public long getDuration() { @@ -133,7 +133,7 @@ public long getDuration() { /** * Sets duration of a media file. - * @param duration + * */ public void setDuration(long duration) { @@ -142,7 +142,7 @@ public void setDuration(long duration) { /** * Gets URL address of a media file. - * @return + * */ public String getUrl() { @@ -151,7 +151,7 @@ public String getUrl() { /** * Sets URL address of a media file. - * @param url + * */ public void setUrl(String url) { @@ -160,7 +160,7 @@ public void setUrl(String url) { /** * Stores ImageInfo objects. - * @param images + * */ public void addImages(ImageInfo... images) { From 3458d90ac1f76092ae14a9bfdab39f7ed79a2ba6 Mon Sep 17 00:00:00 2001 From: Simon Gladkoskok Date: Fri, 15 Aug 2014 14:20:40 -0700 Subject: [PATCH 15/76] add submodules, add DefaultPlatform class --- .gitmodules | 6 + libs/multiscreen-android-1.1.11.jar | Bin 171435 -> 0 bytes modules/google_cast | 1 + modules/samsung_multiscreen | 1 + src/com/connectsdk/DefaultPlatform.java | 42 + .../discovery/DiscoveryManager.java | 46 +- .../provider/CastDiscoveryProvider.java | 315 ------ src/com/connectsdk/service/CastService.java | 944 ------------------ .../service/CastServiceChannel.java | 69 -- .../service/MultiScreenService.java | 567 ----------- .../config/CastServiceDescription.java | 41 - .../service/sessions/CastWebAppSession.java | 175 ---- .../sessions/MultiScreenWebAppSession.java | 843 ---------------- 13 files changed, 78 insertions(+), 2972 deletions(-) create mode 100644 .gitmodules delete mode 100644 libs/multiscreen-android-1.1.11.jar create mode 160000 modules/google_cast create mode 160000 modules/samsung_multiscreen create mode 100644 src/com/connectsdk/DefaultPlatform.java delete mode 100644 src/com/connectsdk/discovery/provider/CastDiscoveryProvider.java delete mode 100644 src/com/connectsdk/service/CastService.java delete mode 100644 src/com/connectsdk/service/CastServiceChannel.java delete mode 100644 src/com/connectsdk/service/MultiScreenService.java delete mode 100644 src/com/connectsdk/service/config/CastServiceDescription.java delete mode 100644 src/com/connectsdk/service/sessions/CastWebAppSession.java delete mode 100644 src/com/connectsdk/service/sessions/MultiScreenWebAppSession.java diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..78f2a41f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "modules/google_cast"] + path = modules/google_cast + url = https://github.com/ConnectSDK/Connect-SDK-Android-Google-Cast.git +[submodule "modules/samsung_multiscreen"] + path = modules/samsung_multiscreen + url = https://github.com/ConnectSDK/Connect-SDK-Android-Samsung-MultiScreen.git diff --git a/libs/multiscreen-android-1.1.11.jar b/libs/multiscreen-android-1.1.11.jar deleted file mode 100644 index 6a71784a1a0608dd33026b7f8c699af3e8b08d24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 171435 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 z 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 diff --git a/modules/google_cast b/modules/google_cast new file mode 160000 index 00000000..0527302d --- /dev/null +++ b/modules/google_cast @@ -0,0 +1 @@ +Subproject commit 0527302d094bfa61c75282df51b531b496658442 diff --git a/modules/samsung_multiscreen b/modules/samsung_multiscreen new file mode 160000 index 00000000..72bc0b9b --- /dev/null +++ b/modules/samsung_multiscreen @@ -0,0 +1 @@ +Subproject commit 72bc0b9b3dfeab1fa4cb9ec8ff5de8cb5d98fbd6 diff --git a/src/com/connectsdk/DefaultPlatform.java b/src/com/connectsdk/DefaultPlatform.java new file mode 100644 index 00000000..8cbea76b --- /dev/null +++ b/src/com/connectsdk/DefaultPlatform.java @@ -0,0 +1,42 @@ +package com.connectsdk; + +import java.util.HashMap; + + +public class DefaultPlatform { + +// registerDeviceService(WebOSTVService.class, SSDPDiscoveryProvider.class); +// registerDeviceService(NetcastTVService.class, SSDPDiscoveryProvider.class); +// registerDeviceService(DLNAService.class, SSDPDiscoveryProvider.class); +// registerDeviceService(DIALService.class, SSDPDiscoveryProvider.class); +// registerDeviceService(RokuService.class, SSDPDiscoveryProvider.class); +// registerDeviceService(CastService.class, CastDiscoveryProvider.class); +// registerDeviceService(AirPlayService.class, ZeroconfDiscoveryProvider.class); +// registerDeviceService(MultiScreenService.class, SSDPDiscoveryProvider.class); + + private HashMap deviceServiceMap; + + public DefaultPlatform() { + + deviceServiceMap = new HashMap(); + } + + public HashMap getDeviceServiceMap() { + deviceServiceMap.put("WebOSTVService", "SSDPDiscoveryProvider"); + deviceServiceMap.put("NetcastTVService", "SSDPDiscoveryProvider"); + deviceServiceMap.put("DLNAService", "SSDPDiscoveryProvider"); + deviceServiceMap.put("DIALService", "SSDPDiscoveryProvider"); + deviceServiceMap.put("RokuService", "SSDPDiscoveryProvider"); + deviceServiceMap.put("CastService", "CastDiscoveryProvider"); + deviceServiceMap.put("AirPlayService", "ZeroconfDiscoveryProvider"); + deviceServiceMap.put("MultiScreenService", "SSDPDiscoveryProvider"); + return deviceServiceMap; + } + + + + + + + +} diff --git a/src/com/connectsdk/discovery/DiscoveryManager.java b/src/com/connectsdk/discovery/DiscoveryManager.java index 9575a383..2ba0c802 100644 --- a/src/com/connectsdk/discovery/DiscoveryManager.java +++ b/src/com/connectsdk/discovery/DiscoveryManager.java @@ -25,6 +25,7 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -45,24 +46,16 @@ import android.net.wifi.WifiManager.MulticastLock; import android.util.Log; +import com.connectsdk.DefaultPlatform; import com.connectsdk.core.Util; import com.connectsdk.device.ConnectableDevice; import com.connectsdk.device.ConnectableDeviceListener; import com.connectsdk.device.ConnectableDeviceStore; import com.connectsdk.device.DefaultConnectableDeviceStore; -import com.connectsdk.discovery.provider.CastDiscoveryProvider; -import com.connectsdk.discovery.provider.SSDPDiscoveryProvider; -import com.connectsdk.discovery.provider.ZeroconfDiscoveryProvider; -import com.connectsdk.service.AirPlayService; -import com.connectsdk.service.CastService; -import com.connectsdk.service.DIALService; import com.connectsdk.service.DLNAService; import com.connectsdk.service.DeviceService; import com.connectsdk.service.DeviceService.PairingType; -import com.connectsdk.service.MultiScreenService; import com.connectsdk.service.NetcastTVService; -import com.connectsdk.service.RokuService; -import com.connectsdk.service.WebOSTVService; import com.connectsdk.service.command.ServiceCommandError; import com.connectsdk.service.config.ServiceConfig; import com.connectsdk.service.config.ServiceConfig.ServiceConfigListener; @@ -363,17 +356,34 @@ public boolean deviceIsCompatible(ConnectableDevice device) { * - ZeroconfDiscoveryProvider * + AirPlayService */ + @SuppressWarnings("unchecked") public void registerDefaultDeviceTypes() { - registerDeviceService(WebOSTVService.class, SSDPDiscoveryProvider.class); - registerDeviceService(NetcastTVService.class, SSDPDiscoveryProvider.class); - registerDeviceService(DLNAService.class, SSDPDiscoveryProvider.class); - registerDeviceService(DIALService.class, SSDPDiscoveryProvider.class); - registerDeviceService(RokuService.class, SSDPDiscoveryProvider.class); - registerDeviceService(CastService.class, CastDiscoveryProvider.class); - registerDeviceService(AirPlayService.class, ZeroconfDiscoveryProvider.class); - registerDeviceService(MultiScreenService.class, SSDPDiscoveryProvider.class); + + HashMap deviceServiceMap = new HashMap(); + DefaultPlatform dp = new DefaultPlatform(); + deviceServiceMap = dp.getDeviceServiceMap(); + + for (Map.Entry entry : deviceServiceMap.entrySet()) { + try { + registerDeviceService((Class)Class.forName(entry.getKey()), (Class)Class.forName(entry.getValue())); + } catch (ClassNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + +// registerDeviceService(WebOSTVService.class, SSDPDiscoveryProvider.class); +// registerDeviceService(NetcastTVService.class, SSDPDiscoveryProvider.class); +// registerDeviceService(DLNAService.class, SSDPDiscoveryProvider.class); +// registerDeviceService(DIALService.class, SSDPDiscoveryProvider.class); +// registerDeviceService(RokuService.class, SSDPDiscoveryProvider.class); +// registerDeviceService(CastService.class, CastDiscoveryProvider.class); +// registerDeviceService(AirPlayService.class, ZeroconfDiscoveryProvider.class); +// registerDeviceService(MultiScreenService.class, SSDPDiscoveryProvider.class); } + + /** * Registers a DeviceService with DiscoveryManager and tells it which DiscoveryProvider to use to find it. Each DeviceService has a JSONObject of discovery parameters that its DiscoveryProvider will use to find it. * @@ -803,7 +813,7 @@ public void addServiceDescriptionToDevice(ServiceDescription desc, ConnectableDe } else if (deviceServiceClass == NetcastTVService.class) { if (!isNetcast(desc)) return; - } else if (deviceServiceClass == MultiScreenService.class){ + } else if (deviceServiceClass.getSimpleName().equals("MultiScreenService")){ if (!isSamsungMultiScreen(desc)) return; } diff --git a/src/com/connectsdk/discovery/provider/CastDiscoveryProvider.java b/src/com/connectsdk/discovery/provider/CastDiscoveryProvider.java deleted file mode 100644 index 28808e36..00000000 --- a/src/com/connectsdk/discovery/provider/CastDiscoveryProvider.java +++ /dev/null @@ -1,315 +0,0 @@ -/* - * CastDiscoveryProvider - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Hyun Kook Khang on 20 Feb 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.discovery.provider; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; - -import org.json.JSONObject; - -import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.support.v7.media.MediaRouteSelector; -import android.support.v7.media.MediaRouter; -import android.support.v7.media.MediaRouter.RouteInfo; -import android.util.Log; - -import com.connectsdk.core.Util; -import com.connectsdk.discovery.DiscoveryProvider; -import com.connectsdk.discovery.DiscoveryProviderListener; -import com.connectsdk.service.CastService; -import com.connectsdk.service.config.CastServiceDescription; -import com.connectsdk.service.config.ServiceDescription; -import com.google.android.gms.cast.CastDevice; -import com.google.android.gms.cast.CastMediaControlIntent; - -public class CastDiscoveryProvider implements DiscoveryProvider { - private MediaRouter mMediaRouter; - private MediaRouteSelector mMediaRouteSelector; - private MediaRouter.Callback mMediaRouterCallback; - - private ConcurrentHashMap foundServices; - private CopyOnWriteArrayList serviceListeners; - - private final static int RESCAN_INTERVAL = 10000; - private final static int RESCAN_ATTEMPTS = 3; - private final static int SSDP_TIMEOUT = RESCAN_INTERVAL * RESCAN_ATTEMPTS; - - private Timer addCallbackTimer; - private Timer removeCallbackTimer; - - public CastDiscoveryProvider(Context context) { - mMediaRouter = MediaRouter.getInstance(context); - mMediaRouteSelector = new MediaRouteSelector.Builder() - .addControlCategory(CastMediaControlIntent.categoryForCast(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)) - .build(); - - mMediaRouterCallback = new MediaRouterCallback(); - - foundServices = new ConcurrentHashMap(8, 0.75f, 2); - serviceListeners = new CopyOnWriteArrayList(); - } - - @Override - public void start() { - stop(); - - addCallbackTimer = new Timer(); - addCallbackTimer.schedule(new TimerTask() { - - @Override - public void run() { - sendSearch(); - } - }, 100, RESCAN_INTERVAL); - - removeCallbackTimer = new Timer(); - removeCallbackTimer.schedule(new TimerTask() { - - @Override - public void run() { - new Handler(Looper.getMainLooper()).post(new Runnable() { - - @Override - public void run() { - mMediaRouter.removeCallback(mMediaRouterCallback); - } - }); - } - }, 9100, RESCAN_INTERVAL); - } - - private void sendSearch() { - List killKeys = new ArrayList(); - - long killPoint = new Date().getTime() - SSDP_TIMEOUT; - - for (String key : foundServices.keySet()) { - ServiceDescription service = foundServices.get(key); - if (service == null || service.getLastDetection() < killPoint) { - killKeys.add(key); - } - } - - for (String key : killKeys) { - final ServiceDescription service = foundServices.get(key); - - if (service != null) { - Util.runOnUI(new Runnable() { - - @Override - public void run() { - for (DiscoveryProviderListener listener : serviceListeners) { - listener.onServiceRemoved(CastDiscoveryProvider.this, service); - } - } - }); - } - - if (foundServices.containsKey(key)) - foundServices.remove(key); - } - - new Handler(Looper.getMainLooper()).post(new Runnable() { - - @Override - public void run() { - mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); - } - }); - } - - @Override - public void stop() { - if (addCallbackTimer != null) { - addCallbackTimer.cancel(); - } - - if (removeCallbackTimer != null) { - removeCallbackTimer.cancel(); - } - - if (mMediaRouter != null) { - new Handler(Looper.getMainLooper()).post(new Runnable() { - - @Override - public void run() { - mMediaRouter.removeCallback(mMediaRouterCallback); - } - }); - } - } - - @Override - public void reset() { - stop(); - foundServices.clear(); - } - - @Override - public void addListener(DiscoveryProviderListener listener) { - serviceListeners.add(listener); - } - - @Override - public void removeListener(DiscoveryProviderListener listener) { - serviceListeners.remove(listener); - } - - @Override - public void addDeviceFilter(JSONObject parameters) {} - - @Override - public void removeDeviceFilter(JSONObject parameters) {} - - @Override - public boolean isEmpty() { - return false; - } - - private class MediaRouterCallback extends MediaRouter.Callback { - - @Override - public void onRouteAdded(MediaRouter router, RouteInfo route) { - super.onRouteAdded(router, route); - - CastDevice castDevice = CastDevice.getFromBundle(route.getExtras()); - String uuid = castDevice.getDeviceId(); - - ServiceDescription foundService = foundServices.get(uuid); - - boolean isNew = foundService == null; - boolean listUpdateFlag = false; - - if (isNew) { - foundService = new CastServiceDescription(CastService.ID, uuid, castDevice.getIpAddress().getHostAddress(), castDevice); - foundService.setFriendlyName(castDevice.getFriendlyName()); - foundService.setModelName(castDevice.getModelName()); - foundService.setModelNumber(castDevice.getDeviceVersion()); - foundService.setModelDescription(route.getDescription()); - foundService.setPort(castDevice.getServicePort()); - foundService.setServiceID(CastService.ID); - - listUpdateFlag = true; - } - else { - if (!foundService.getFriendlyName().equals(castDevice.getFriendlyName())) { - foundService.setFriendlyName(castDevice.getFriendlyName()); - listUpdateFlag = true; - } - - ((CastServiceDescription)foundService).setCastDevice(castDevice); - } - - if (foundService != null) - foundService.setLastDetection(new Date().getTime()); - - foundServices.put(uuid, foundService); - - if (listUpdateFlag) { - for (DiscoveryProviderListener listenter: serviceListeners) { - listenter.onServiceAdded(CastDiscoveryProvider.this, foundService); - } - } - } - - @Override - public void onRouteChanged(MediaRouter router, RouteInfo route) { - super.onRouteChanged(router, route); - - CastDevice castDevice = CastDevice.getFromBundle(route.getExtras()); - String uuid = castDevice.getDeviceId(); - - ServiceDescription foundService = foundServices.get(uuid); - - boolean isNew = foundService == null; - boolean listUpdateFlag = false; - - if (!isNew) { - foundService.setIpAddress(castDevice.getIpAddress().getHostAddress()); - foundService.setModelName(castDevice.getModelName()); - foundService.setModelNumber(castDevice.getDeviceVersion()); - foundService.setModelDescription(route.getDescription()); - foundService.setPort(castDevice.getServicePort()); - ((CastServiceDescription)foundService).setCastDevice(castDevice); - - if (!foundService.getFriendlyName().equals(castDevice.getFriendlyName())) { - foundService.setFriendlyName(castDevice.getFriendlyName()); - listUpdateFlag = true; - } - - foundService.setLastDetection(new Date().getTime()); - - foundServices.put(uuid, foundService); - - if (listUpdateFlag) { - for (DiscoveryProviderListener listenter: serviceListeners) { - listenter.onServiceAdded(CastDiscoveryProvider.this, foundService); - } - } - } - } - - @Override - public void onRoutePresentationDisplayChanged(MediaRouter router, - RouteInfo route) { - Log.d(Util.T, "onRoutePresentationDisplayChanged: [" + route.getName() + "] [" + route.getDescription() + "]"); - super.onRoutePresentationDisplayChanged(router, route); - } - - @Override - public void onRouteRemoved(MediaRouter router, RouteInfo route) { - super.onRouteRemoved(router, route); - -// CastDevice castDevice = CastDevice.getFromBundle(route.getExtras()); -// String uuid = castDevice.getDeviceId(); -// -// final ServiceDescription service = foundServices.get(uuid); -// -// if (service != null) { -// Util.runOnUI(new Runnable() { -// -// @Override -// public void run() { -// for (DiscoveryProviderListener listener : serviceListeners) { -// listener.onServiceRemoved(CastDiscoveryProvider.this, service); -// } -// } -// }); -// -// foundServices.remove(uuid); -// } - } - - @Override - public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) { - Log.d(Util.T, "onRouteVolumeChanged: [" + route.getName() + "] [" + route.getDescription() + "]"); - super.onRouteVolumeChanged(router, route); - } - - } -} diff --git a/src/com/connectsdk/service/CastService.java b/src/com/connectsdk/service/CastService.java deleted file mode 100644 index 59fd22b0..00000000 --- a/src/com/connectsdk/service/CastService.java +++ /dev/null @@ -1,944 +0,0 @@ -/* - * CastService - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Hyun Kook Khang on 23 Feb 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 java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.json.JSONException; -import org.json.JSONObject; - -import android.net.Uri; -import android.os.Bundle; -import android.util.Log; - -import com.connectsdk.core.ImageInfo; -import com.connectsdk.core.MediaInfo; -import com.connectsdk.core.Util; -import com.connectsdk.discovery.DiscoveryManager; -import com.connectsdk.service.capability.MediaControl; -import com.connectsdk.service.capability.MediaPlayer; -import com.connectsdk.service.capability.VolumeControl; -import com.connectsdk.service.capability.WebAppLauncher; -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.command.ServiceCommandError; -import com.connectsdk.service.command.ServiceSubscription; -import com.connectsdk.service.command.URLServiceSubscription; -import com.connectsdk.service.config.CastServiceDescription; -import com.connectsdk.service.config.ServiceConfig; -import com.connectsdk.service.config.ServiceDescription; -import com.connectsdk.service.sessions.CastWebAppSession; -import com.connectsdk.service.sessions.LaunchSession; -import com.connectsdk.service.sessions.LaunchSession.LaunchSessionType; -import com.connectsdk.service.sessions.WebAppSession; -import com.google.android.gms.cast.ApplicationMetadata; -import com.google.android.gms.cast.Cast; -import com.google.android.gms.cast.Cast.ApplicationConnectionResult; -import com.google.android.gms.cast.CastDevice; -import com.google.android.gms.cast.CastMediaControlIntent; -import com.google.android.gms.cast.MediaMetadata; -import com.google.android.gms.cast.MediaStatus; -import com.google.android.gms.cast.RemoteMediaPlayer; -import com.google.android.gms.cast.RemoteMediaPlayer.MediaChannelResult; -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.api.GoogleApiClient; -import com.google.android.gms.common.api.ResultCallback; -import com.google.android.gms.common.api.Status; -import com.google.android.gms.common.images.WebImage; - -public class CastService extends DeviceService implements MediaPlayer, MediaControl, VolumeControl, WebAppLauncher { - public interface LaunchWebAppListener{ - void onSuccess(WebAppSession webAppSession); - void onFailure(ServiceCommandError error); - }; - - // @cond INTERNAL - - public static final String ID = "Chromecast"; - public final static String TAG = "Connect SDK"; - - public final static String PLAY_STATE = "PlayState"; - public final static String CAST_SERVICE_VOLUME_SUBSCRIPTION_NAME = "volume"; - public final static String CAST_SERVICE_MUTE_SUBSCRIPTION_NAME = "mute"; - - // @endcond - - String currentAppId; - String launchingAppId; - - GoogleApiClient mApiClient; - CastListener mCastClientListener; - ConnectionCallbacks mConnectionCallbacks; - ConnectionFailedListener mConnectionFailedListener; - - CastDevice castDevice; - RemoteMediaPlayer mMediaPlayer; - - Map sessions; - List> subscriptions; - - float currentVolumeLevel; - boolean currentMuteStatus; - boolean mWaitingForReconnect; - - public CastService(ServiceDescription serviceDescription, ServiceConfig serviceConfig) { - super(serviceDescription, serviceConfig); - - mCastClientListener = new CastListener(); - mConnectionCallbacks = new ConnectionCallbacks(); - mConnectionFailedListener = new ConnectionFailedListener(); - - sessions = new HashMap(); - subscriptions = new ArrayList>(); - - mWaitingForReconnect = false; - } - - @Override - public String getServiceName() { - return ID; - } - - public static JSONObject discoveryParameters() { - JSONObject params = new JSONObject(); - - try { - params.put("serviceId", ID); - params.put("filter", "Chromecast"); - } catch (JSONException e) { - e.printStackTrace(); - } - - return params; - } - - - @Override - public void connect() { - if (connected) - return; - - if (castDevice == null) { - if (serviceDescription instanceof CastServiceDescription) - this.castDevice = ((CastServiceDescription)serviceDescription).getCastDevice(); - } - - if (mApiClient == null) { - Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions - .builder(castDevice, mCastClientListener); - - mApiClient = new GoogleApiClient.Builder(DiscoveryManager.getInstance().getContext()) - .addApi(Cast.API, apiOptionsBuilder.build()) - .addConnectionCallbacks(mConnectionCallbacks) - .addOnConnectionFailedListener(mConnectionFailedListener) - .build(); - - mApiClient.connect(); - } - } - - @Override - public void disconnect() { - if (!connected) - return; - - connected = false; - mWaitingForReconnect = false; - - detachMediaPlayer(); - - Cast.CastApi.leaveApplication(mApiClient); - mApiClient.disconnect(); - mApiClient = null; - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - if (getListener() != null) { - getListener().onDisconnect(CastService.this, null); - } - } - }); - } - - @Override - public MediaControl getMediaControl() { - return this; - } - - @Override - public CapabilityPriorityLevel getMediaControlCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void play(final ResponseListener listener) { - try { - mMediaPlayer.play(mApiClient); - - Util.postSuccess(listener, null); - } catch (Exception e) { - Util.postError(listener, new ServiceCommandError(0, "Unable to play", null)); - } - } - - @Override - public void pause(final ResponseListener listener) { - try { - mMediaPlayer.pause(mApiClient); - - Util.postError(listener, null); - } catch (Exception e) { - Util.postError(listener, new ServiceCommandError(0, "Unable to pause", null)); - } - } - - @Override - public void stop(final ResponseListener listener) { - try { - mMediaPlayer.stop(mApiClient); - - Util.postError(listener, null); - } catch (Exception e) { - Util.postError(listener, new ServiceCommandError(0, "Unable to stop", null)); - } - } - - @Override - public void rewind(ResponseListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void fastForward(ResponseListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void seek(long position, final ResponseListener listener) { - if (mMediaPlayer.getMediaStatus() == null) { - Util.postError(listener, new ServiceCommandError(0, "There is no media currently available", null)); - return; - } - - mMediaPlayer.seek(mApiClient, position, RemoteMediaPlayer.RESUME_STATE_UNCHANGED).setResultCallback( - new ResultCallback() { - - @Override - public void onResult(MediaChannelResult result) { - Status status = result.getStatus(); - - if (status.isSuccess()) { - Util.postSuccess(listener, null); - } else { - Util.postError(listener, new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), status)); - } - } - }); - } - - @Override - public void getDuration(final DurationListener listener) { - if (mMediaPlayer.getMediaStatus() != null) { - Util.postSuccess(listener, mMediaPlayer.getStreamDuration()); - } - else { - Util.postError(listener, new ServiceCommandError(0, "There is no media currently available", null)); - } - } - - @Override - public void getPosition(final PositionListener listener) { - if (mMediaPlayer.getMediaStatus() != null) { - Util.postSuccess(listener, mMediaPlayer.getApproximateStreamPosition()); - } - else { - Util.postError(listener, new ServiceCommandError(0, "There is no media currently available", null)); - } - } - - @Override - public MediaPlayer getMediaPlayer() { - return this; - } - - @Override - public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - private void attachMediaPlayer() { - if (mMediaPlayer != null) { - return; - } - - mMediaPlayer = new RemoteMediaPlayer(); - mMediaPlayer.setOnStatusUpdatedListener(new RemoteMediaPlayer.OnStatusUpdatedListener() { - - @Override - public void onStatusUpdated() { - if (subscriptions.size() > 0) { - for (URLServiceSubscription subscription: subscriptions) { - if (subscription.getTarget().equalsIgnoreCase(PLAY_STATE)) { - for (int i = 0; i < subscription.getListeners().size(); i++) { - @SuppressWarnings("unchecked") - ResponseListener listener = (ResponseListener) subscription.getListeners().get(i); - PlayStateStatus status = convertPlayerStateToPlayStateStatus(mMediaPlayer.getMediaStatus().getPlayerState()); - Util.postSuccess(listener, status); - } - } - } - } - } - }); - - mMediaPlayer.setOnMetadataUpdatedListener(new RemoteMediaPlayer.OnMetadataUpdatedListener() { - @Override - public void onMetadataUpdated() { - Log.d("Connect SDK", "MediaControlChannel.onMetadataUpdated"); - } - }); - - try { - Cast.CastApi.setMessageReceivedCallbacks(mApiClient, mMediaPlayer.getNamespace(), - mMediaPlayer); - } catch (IOException e) { - Log.w("Connect SDK", "Exception while creating media channel", e); - } - } - - private void detachMediaPlayer() { - if (mMediaPlayer != null) { - try { - Cast.CastApi.removeMessageReceivedCallbacks(mApiClient, - mMediaPlayer.getNamespace()); - } catch (IOException e) { - Log.w("Connect SDK", "Exception while launching application", e); - } - mMediaPlayer = null; - } - } - - @Override - public void displayImage(String url, String mimeType, String title, - String description, String iconSrc, LaunchListener listener) { - MediaMetadata mMediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_PHOTO); - mMediaMetadata.putString(MediaMetadata.KEY_TITLE, title); - mMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, description); - - if (iconSrc != null) { - Uri iconUri = Uri.parse(iconSrc); - WebImage image = new WebImage(iconUri, 100, 100); - mMediaMetadata.addImage(image); - } - - com.google.android.gms.cast.MediaInfo mediaInformation = new com.google.android.gms.cast.MediaInfo.Builder(url) - .setContentType(mimeType) - .setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_NONE) - .setMetadata(mMediaMetadata) - .setStreamDuration(0) - .setCustomData(null) - .build(); - - playMedia(mediaInformation, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, listener); - } - - @Override - public void displayImage(MediaInfo mediaInfo, LaunchListener listener) { - ImageInfo imageInfo = mediaInfo.getImages().get(0); - String iconSrc = imageInfo.getUrl(); - - displayImage(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, listener); - } - - @Override - public void playMedia(String url, String mimeType, String title, - String description, String iconSrc, boolean shouldLoop, - LaunchListener listener) { - MediaMetadata mMediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); - mMediaMetadata.putString(MediaMetadata.KEY_TITLE, title); - mMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, description); - - if (iconSrc != null) { - Uri iconUri = Uri.parse(iconSrc); - WebImage image = new WebImage(iconUri, 100, 100); - mMediaMetadata.addImage(image); - } - - com.google.android.gms.cast.MediaInfo mediaInformation = new com.google.android.gms.cast.MediaInfo.Builder(url) - .setContentType(mimeType) - .setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_BUFFERED) - .setMetadata(mMediaMetadata) - .setStreamDuration(1000) - .setCustomData(null) - .build(); - - playMedia(mediaInformation, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, listener); - } - - @Override - public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, LaunchListener listener) { - ImageInfo imageInfo = mediaInfo.getImages().get(0); - String iconSrc = imageInfo.getUrl(); - - playMedia(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, shouldLoop, listener); - } - - private void playMedia(final com.google.android.gms.cast.MediaInfo mediaInformation, String mediaAppId, final LaunchListener listener) { - ApplicationConnectionResultCallback webAppLaunchCallback = new ApplicationConnectionResultCallback(new LaunchWebAppListener() { - - @Override - public void onSuccess(final WebAppSession webAppSession) { - mMediaPlayer.load(mApiClient, mediaInformation, true).setResultCallback(new ResultCallback() { - - @Override - public void onResult(MediaChannelResult result) { - Status status = result.getStatus(); - - if (status.isSuccess()) { - webAppSession.launchSession.setSessionType(LaunchSessionType.Media); - - Util.postSuccess(listener, new MediaLaunchObject(webAppSession.launchSession, CastService.this)); - } - else { - Util.postError(listener, new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), status)); - } - } - }); - } - - @Override - public void onFailure(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - - launchingAppId = mediaAppId; - - boolean relaunchIfRunning = false; - - if (Cast.CastApi.getApplicationStatus(mApiClient) == null || (!mediaAppId.equals(currentAppId))) - relaunchIfRunning = true; - - Cast.CastApi.launchApplication(mApiClient, mediaAppId, relaunchIfRunning).setResultCallback(webAppLaunchCallback); - } - - @Override - public void closeMedia(final LaunchSession launchSession, final ResponseListener listener) { - Cast.CastApi.stopApplication(mApiClient, launchSession.getSessionId()).setResultCallback(new ResultCallback() { - - @Override - public void onResult(Status result) { - if (result.isSuccess()) { - Util.postSuccess(listener, result); - } else { - Util.postError(listener, new ServiceCommandError(result.getStatusCode(), result.getStatusMessage(), result)); - } - } - }); - } - - @Override - public WebAppLauncher getWebAppLauncher() { - return this; - } - - @Override - public CapabilityPriorityLevel getWebAppLauncherCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void launchWebApp(String webAppId, WebAppSession.LaunchListener listener) { - launchWebApp(webAppId, true, listener); - } - - @Override - public void launchWebApp(final String webAppId, final boolean relaunchIfRunning, final WebAppSession.LaunchListener listener) { - launchingAppId = webAppId; - - Cast.CastApi.launchApplication(mApiClient, webAppId, relaunchIfRunning).setResultCallback( - new ApplicationConnectionResultCallback(new LaunchWebAppListener() { - - @Override - public void onSuccess(WebAppSession webAppSession) { - Util.postSuccess(listener, webAppSession); - } - - @Override - public void onFailure(ServiceCommandError error) { - Util.postError(listener, error); - } - }) - ); - } - - @Override - public void launchWebApp(String webAppId, JSONObject params, WebAppSession.LaunchListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void launchWebApp(String webAppId, JSONObject params, boolean relaunchIfRunning, WebAppSession.LaunchListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void joinWebApp(final LaunchSession webAppLaunchSession, final WebAppSession.LaunchListener listener) { - ApplicationConnectionResultCallback webAppLaunchCallback = new ApplicationConnectionResultCallback(new LaunchWebAppListener() { - - @Override - public void onSuccess(final WebAppSession webAppSession) { - webAppSession.connect(new ResponseListener() { - - @Override - public void onSuccess(Object object) { - Util.postSuccess(listener, webAppSession); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - } - - @Override - public void onFailure(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - - launchingAppId = webAppLaunchSession.getAppId(); - - Cast.CastApi.joinApplication(mApiClient, webAppLaunchSession.getAppId()).setResultCallback(webAppLaunchCallback); - } - - @Override - public void joinWebApp(String webAppId, WebAppSession.LaunchListener listener) { - LaunchSession launchSession = LaunchSession.launchSessionForAppId(webAppId); - launchSession.setSessionType(LaunchSessionType.WebApp); - launchSession.setService(this); - - joinWebApp(launchSession, listener); - } - - @Override - public void closeWebApp(LaunchSession launchSession, final ResponseListener listener) { - Cast.CastApi.stopApplication(mApiClient).setResultCallback(new ResultCallback() { - - @Override - public void onResult(Status status) { - if (status.isSuccess()) { - Util.postSuccess(listener, null); - } - else { - Util.postError(listener, new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), status)); - } - } - }); - } - - @Override - public VolumeControl getVolumeControl() { - return this; - } - - @Override - public CapabilityPriorityLevel getVolumeControlCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void volumeUp(final ResponseListener 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 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 listener) { - try { - Cast.CastApi.setVolume(mApiClient, volume); - - Util.postSuccess(listener, null); - } catch (IOException e) { - Util.postError(listener, new ServiceCommandError(0, "setting volume level failed", null)); - } - } - - @Override - public void getVolume(VolumeListener listener) { - Util.postSuccess(listener, currentVolumeLevel); - } - - @Override - public void setMute(boolean isMute, ResponseListener listener) { - try { - Cast.CastApi.setMute(mApiClient, isMute); - - Util.postSuccess(listener, null); - } catch (IOException e) { - Util.postError(listener, new ServiceCommandError(0, "setting mute status failed", null)); - } - } - - @Override - public void getMute(final MuteListener listener) { - Util.postSuccess(listener, currentMuteStatus); - } - - @Override - public ServiceSubscription subscribeVolume(VolumeListener listener) { - URLServiceSubscription request = new URLServiceSubscription(this, CAST_SERVICE_VOLUME_SUBSCRIPTION_NAME, null, null); - request.addListener(listener); - addSubscription(request); - - return request; - } - - @Override - public ServiceSubscription subscribeMute(MuteListener listener) { - URLServiceSubscription request = new URLServiceSubscription(this, CAST_SERVICE_MUTE_SUBSCRIPTION_NAME, null, null); - request.addListener(listener); - addSubscription(request); - - return request; - } - - @Override - protected void updateCapabilities() { - List capabilities = new ArrayList(); - - for (String capability : MediaPlayer.Capabilities) { capabilities.add(capability); } - for (String capability : VolumeControl.Capabilities) { capabilities.add(capability); } - - capabilities.add(Play); - capabilities.add(Pause); - capabilities.add(Stop); - capabilities.add(Duration); - capabilities.add(Seek); - capabilities.add(Position); - capabilities.add(PlayState); - capabilities.add(PlayState_Subscribe); - - capabilities.add(WebAppLauncher.Launch); - capabilities.add(Message_Send); - capabilities.add(Message_Receive); - capabilities.add(Message_Send_JSON); - capabilities.add(Message_Receive_JSON); - capabilities.add(WebAppLauncher.Connect); - capabilities.add(WebAppLauncher.Disconnect); - capabilities.add(WebAppLauncher.Join); - capabilities.add(WebAppLauncher.Close); - - setCapabilities(capabilities); - } - - private class CastListener extends Cast.Listener { - @Override - public void onApplicationDisconnected(int statusCode) { - Log.d("Connect SDK", "Cast.Listener.onApplicationDisconnected: " + statusCode); - - if (currentAppId == null) - return; - - CastWebAppSession webAppSession = sessions.get(currentAppId); - - if (webAppSession == null) - return; - - webAppSession.handleAppClose(); - - currentAppId = null; - } - - @Override - public void onApplicationStatusChanged() { - ApplicationMetadata applicationMetadata = Cast.CastApi.getApplicationMetadata(mApiClient); - - if (applicationMetadata != null) - currentAppId = applicationMetadata.getApplicationId(); - } - - @Override - public void onVolumeChanged() { - try { - currentVolumeLevel = (float) Cast.CastApi.getVolume(mApiClient); - currentMuteStatus = Cast.CastApi.isMute(mApiClient); - } catch (IllegalStateException e) { - e.printStackTrace(); - } - - if (subscriptions.size() > 0) { - for (URLServiceSubscription subscription: subscriptions) { - if (subscription.getTarget().equals(CAST_SERVICE_VOLUME_SUBSCRIPTION_NAME)) { - for (int i = 0; i < subscription.getListeners().size(); i++) { - @SuppressWarnings("unchecked") - ResponseListener listener = (ResponseListener) subscription.getListeners().get(i); - - Util.postSuccess(listener, currentVolumeLevel); - } - } - else if (subscription.getTarget().equals(CAST_SERVICE_MUTE_SUBSCRIPTION_NAME)) { - for (int i = 0; i < subscription.getListeners().size(); i++) { - @SuppressWarnings("unchecked") - ResponseListener listener = (ResponseListener) subscription.getListeners().get(i); - - Util.postSuccess(listener, currentMuteStatus); - } - } - } - } - } - } - - private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks { - @Override - public void onConnectionSuspended(final int cause) { - Log.d("Connect SDK", "ConnectionCallbacks.onConnectionSuspended"); - - mWaitingForReconnect = true; - } - - @Override - public void onConnected(Bundle connectionHint) { - Log.d("Connect SDK", "ConnectionCallbacks.onConnected, wasWaitingForReconnect: " + mWaitingForReconnect); - - if (mWaitingForReconnect) { - mWaitingForReconnect = false; - reconnectChannels(); - } - else { - attachMediaPlayer(); - - connected = true; - - reportConnected(true); - } - } - - private void reconnectChannels() { - if (Cast.CastApi.getApplicationStatus(mApiClient) != null && currentAppId != null) { - CastWebAppSession webAppSession = sessions.get(currentAppId); - - webAppSession.connect(null); - } - } - } - - private class ConnectionFailedListener implements GoogleApiClient.OnConnectionFailedListener { - @Override - public void onConnectionFailed(final ConnectionResult result) { - Log.d("Connect SDK", "ConnectionFailedListener.onConnectionFailed"); - - detachMediaPlayer(); - connected = false; - mWaitingForReconnect = false; - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - if (listener != null) { - ServiceCommandError error = new ServiceCommandError(result.getErrorCode(), "Failed to connect to Google Cast device", result); - - listener.onConnectionFailure(CastService.this, error); - } - } - }); - } - } - - private class ApplicationConnectionResultCallback implements - ResultCallback { - LaunchWebAppListener listener; - - public ApplicationConnectionResultCallback(LaunchWebAppListener listener) { - this.listener = listener; - } - - @Override - public void onResult(ApplicationConnectionResult result) { - Status status = result.getStatus(); - - if (status.isSuccess()) { - ApplicationMetadata applicationMetadata = result.getApplicationMetadata(); - currentAppId = applicationMetadata.getApplicationId(); - - LaunchSession launchSession = LaunchSession.launchSessionForAppId(applicationMetadata.getApplicationId()); - launchSession.setAppName(applicationMetadata.getName()); - launchSession.setSessionId(result.getSessionId()); - launchSession.setSessionType(LaunchSessionType.WebApp); - launchSession.setService(CastService.this); - - CastWebAppSession webAppSession = new CastWebAppSession(launchSession, CastService.this); - webAppSession.setMetadata(applicationMetadata); - - sessions.put(applicationMetadata.getApplicationId(), webAppSession); - - if (listener != null) { - listener.onSuccess(webAppSession); - } - - launchingAppId = null; - } - else { - if (listener != null) { - listener.onFailure(new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), status)); - } - } - } - } - - @Override - public void getPlayState(PlayStateListener listener) { - if (mMediaPlayer == null) { - Util.postError(listener, new ServiceCommandError(0, "Unable to get play state", null)); - return; - } - - PlayStateStatus status = convertPlayerStateToPlayStateStatus(mMediaPlayer.getMediaStatus().getPlayerState()); - Util.postSuccess(listener, status); - } - - private PlayStateStatus convertPlayerStateToPlayStateStatus(int playerState) { - PlayStateStatus status = PlayStateStatus.Unknown; - - switch (playerState) { - case MediaStatus.PLAYER_STATE_BUFFERING: - status = PlayStateStatus.Buffering; - break; - case MediaStatus.PLAYER_STATE_IDLE: - status = PlayStateStatus.Idle; - break; - case MediaStatus.PLAYER_STATE_PAUSED: - status = PlayStateStatus.Paused; - break; - case MediaStatus.PLAYER_STATE_PLAYING: - status = PlayStateStatus.Playing; - break; - case MediaStatus.PLAYER_STATE_UNKNOWN: - default: - status = PlayStateStatus.Unknown; - break; - } - - return status; - } - - public GoogleApiClient getApiClient() { - return mApiClient; - } - - ////////////////////////////////////////////////// - // Device Service Methods - ////////////////////////////////////////////////// - @Override - public boolean isConnectable() { - return true; - } - - @Override - public boolean isConnected() { - return connected; - } - - @Override - public ServiceSubscription subscribePlayState(PlayStateListener listener) { - URLServiceSubscription request = new URLServiceSubscription(this, PLAY_STATE, null, null); - request.addListener(listener); - addSubscription(request); - - return request; - - } - - private void addSubscription(URLServiceSubscription subscription) { - subscriptions.add(subscription); - } - - @Override - public void unsubscribe(URLServiceSubscription subscription) { - subscriptions.remove(subscription); - } - - public List> getSubscriptions() { - return subscriptions; - } - - public void setSubscriptions(List> subscriptions) { - this.subscriptions = subscriptions; - } -} diff --git a/src/com/connectsdk/service/CastServiceChannel.java b/src/com/connectsdk/service/CastServiceChannel.java deleted file mode 100644 index e3c24237..00000000 --- a/src/com/connectsdk/service/CastServiceChannel.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * CastServiceChannel - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Hyun Kook Khang on 24 Feb 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 org.json.JSONException; -import org.json.JSONObject; - -import com.connectsdk.core.Util; -import com.connectsdk.service.sessions.CastWebAppSession; -import com.google.android.gms.cast.Cast; -import com.google.android.gms.cast.CastDevice; - -public class CastServiceChannel implements Cast.MessageReceivedCallback{ - String webAppId; - CastWebAppSession session; - - public CastServiceChannel(String webAppId, CastWebAppSession session) { - this.webAppId = webAppId; - this.session = session; - } - - public String getNamespace() { - return "urn:x-cast:com.connectsdk"; - } - - @Override - public void onMessageReceived(CastDevice castDevice, String namespace, final String message) { - if (session.getWebAppSessionListener() == null) - return; - - JSONObject messageJSON = null; - - try { - messageJSON = new JSONObject(message); - } catch (JSONException e) { } - - final JSONObject mMessage = messageJSON; - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - if (mMessage == null) { - session.getWebAppSessionListener().onReceiveMessage(session, message); - } else { - session.getWebAppSessionListener().onReceiveMessage(session, mMessage); - } - } - }); - } -} diff --git a/src/com/connectsdk/service/MultiScreenService.java b/src/com/connectsdk/service/MultiScreenService.java deleted file mode 100644 index 26c248a3..00000000 --- a/src/com/connectsdk/service/MultiScreenService.java +++ /dev/null @@ -1,567 +0,0 @@ -package com.connectsdk.service; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -import org.json.JSONException; -import org.json.JSONObject; - -import com.connectsdk.core.MediaInfo; -import com.connectsdk.core.Util; -import com.connectsdk.service.capability.MediaControl; -import com.connectsdk.service.capability.MediaPlayer; -import com.connectsdk.service.capability.WebAppLauncher; -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.command.ServiceCommandError; -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.sessions.MultiScreenWebAppSession; -import com.connectsdk.service.sessions.WebAppSession; -import com.samsung.multiscreen.application.Application; -import com.samsung.multiscreen.application.Application.Status; -import com.samsung.multiscreen.application.ApplicationAsyncResult; -import com.samsung.multiscreen.application.ApplicationError; -import com.samsung.multiscreen.device.Device; -import com.samsung.multiscreen.device.DeviceAsyncResult; -import com.samsung.multiscreen.device.DeviceError; -import com.samsung.multiscreen.device.DeviceFactory; - -public class MultiScreenService extends DeviceService implements MediaPlayer, WebAppLauncher { - public static final String ID = "MultiScreen"; - - Device device; - Map sessions; - - public MultiScreenService(ServiceDescription serviceDescription, - ServiceConfig serviceConfig) { - super(serviceDescription, serviceConfig); - - Map map = new HashMap(); - - map.put("DeviceName", serviceDescription.getFriendlyName()); - map.put("DialURI", serviceDescription.getApplicationURL()); - map.put("IP", serviceDescription.getIpAddress()); - map.put("ModelDescription", serviceDescription.getModelDescription()); - map.put("ModelName", serviceDescription.getModelName()); - map.put("ServiceURI", serviceDescription.getServiceURI()); - - this.device = DeviceFactory.createWithMap(map); - } - - public static JSONObject discoveryParameters() { - JSONObject params = new JSONObject(); - - try { - params.put("serviceId", ID); -// params.put("filter", "urn:samsung.com:service:MultiScreenService:1"); - params.put("filter", "urn:dial-multiscreen-org:service:dial:1"); - } catch (JSONException e) { - e.printStackTrace(); - } - - return params; - } - - @Override - public boolean isConnectable() { - return true; - } - - @Override - public boolean isConnected() { - return connected; - } - - @Override - public void connect() { - connected = true; - - sessions = new HashMap(); - - reportConnected(true); - } - - @Override - public void disconnect() { - for (MultiScreenWebAppSession session: sessions.values()) { - session.disconnectFromWebApp(); - } - - connected = false; - - if (mServiceReachability != null) - mServiceReachability.stop(); - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - if (listener != null) - listener.onDisconnect(MultiScreenService.this, null); - } - }); - } - - @Override - public MediaPlayer getMediaPlayer() { - return this; - } - - @Override - public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void displayImage(final String url, final String mimeType, final String title, - final String description, final String iconSrc, final LaunchListener listener) { - final String webAppId = "ConnectSDKSampler"; - - getWebAppLauncher().joinWebApp(webAppId, new WebAppSession.LaunchListener() { - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.getMediaPlayer().displayImage(url, mimeType, title, description, iconSrc, listener); - } - - @Override - public void onError(ServiceCommandError error) { - getWebAppLauncher().launchWebApp(webAppId, new WebAppSession.LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) { - Util.postError(listener, error); - } - } - - @Override - public void onSuccess(final WebAppSession webAppSession) { - webAppSession.connect(new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - webAppSession.getMediaPlayer().displayImage(url, mimeType, title, description, iconSrc, listener); - } - }); - } - }); - } - }); - } - - @Override - public void displayImage(final MediaInfo mediaInfo, final LaunchListener listener) { - final String webAppId = "ConnectSDKSampler"; - - getWebAppLauncher().joinWebApp(webAppId, new WebAppSession.LaunchListener() { - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.getMediaPlayer().displayImage(mediaInfo, listener); - } - - @Override - public void onError(ServiceCommandError error) { - getWebAppLauncher().launchWebApp(webAppId, new WebAppSession.LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) { - Util.postError(listener, error); - } - } - - @Override - public void onSuccess(final WebAppSession webAppSession) { - webAppSession.connect(new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - webAppSession.getMediaPlayer().displayImage(mediaInfo, listener); - } - }); - } - }); - } - }); - } - - @Override - public void playMedia(final String url, final String mimeType, final String title, - final String description, final String iconSrc, final boolean shouldLoop, - final LaunchListener listener) { - final String webAppId = "ConnectSDKSampler"; - - getWebAppLauncher().joinWebApp(webAppId, new WebAppSession.LaunchListener() { - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.playMedia(url, mimeType, title, description, iconSrc, shouldLoop, listener); - } - - @Override - public void onError(ServiceCommandError error) { - getWebAppLauncher().launchWebApp(webAppId, new WebAppSession.LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) { - Util.postError(listener, error); - } - } - - @Override - public void onSuccess(final WebAppSession webAppSession) { - webAppSession.connect(new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - webAppSession.playMedia(url, mimeType, title, description, iconSrc, shouldLoop, listener); - } - }); - } - }); - } - }); - } - - @Override - public void playMedia(final MediaInfo mediaInfo, final boolean shouldLoop, - final LaunchListener listener) { - final String webAppId = "ConnectSDKSampler"; - - getWebAppLauncher().joinWebApp(webAppId, new WebAppSession.LaunchListener() { - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.playMedia(mediaInfo, shouldLoop, listener); - } - - @Override - public void onError(ServiceCommandError error) { - getWebAppLauncher().launchWebApp(webAppId, new WebAppSession.LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) { - Util.postError(listener, error); - } - } - - @Override - public void onSuccess(final WebAppSession webAppSession) { - webAppSession.connect(new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - webAppSession.playMedia(mediaInfo, shouldLoop, listener); - } - }); - } - }); - } - }); - } - - @Override - public void closeMedia(LaunchSession launchSession, - ResponseListener listener) { - getWebAppLauncher().closeWebApp(launchSession, listener); - } - - @Override - public WebAppLauncher getWebAppLauncher() { - return this; - } - - @Override - public CapabilityPriorityLevel getWebAppLauncherCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void launchWebApp(String webAppId, WebAppSession.LaunchListener listener) { - launchWebApp(webAppId, null, true, listener); - } - - @Override - public void launchWebApp( - String webAppId, - JSONObject params, - final com.connectsdk.service.sessions.WebAppSession.LaunchListener listener) { - launchWebApp(webAppId, params, true, listener); - } - - @Override - public void launchWebApp( - String webAppId, - boolean relaunchIfRunning, - com.connectsdk.service.sessions.WebAppSession.LaunchListener listener) { - launchWebApp(webAppId, null, relaunchIfRunning, listener); - } - - @Override - public void launchWebApp( - final String webAppId, - JSONObject params, - boolean relaunchIfRunning, - final com.connectsdk.service.sessions.WebAppSession.LaunchListener listener) { - ServiceCommandError error = null; - - if (webAppId == null || webAppId.length() == 0) { - error = new ServiceCommandError(0, "You must provide a valid web app id", null); - } - - if (device == null) { - error = new ServiceCommandError(0, "Could not find a reference to the native device object", null); - } - - if (error != null) { - if (listener != null) { - Util.postError(listener, error); - } - - return; - } - - if (params == null) { - params = new JSONObject(); - } - final JSONObject fParams = params; - - device.getApplication(webAppId, new DeviceAsyncResult() { - - @Override - public void onResult(final Application application) { - Map parameters = new HashMap(); - - Iterator keys = fParams.keys(); - while (keys.hasNext()) { - String key = (String) keys.next(); - try { - parameters.put(key, fParams.getString(key)); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - application.launch(parameters, new ApplicationAsyncResult() { - - @Override - public void onError(ApplicationError error) { - Util.postError(listener, new ServiceCommandError((int)error.getCode(), error.getMessage(), error)); - } - - @Override - public void onResult(Boolean launchSuccess) { - if (launchSuccess) { - LaunchSession launchSession = LaunchSession.launchSessionForAppId(webAppId); - launchSession.setSessionType(LaunchSessionType.WebApp); - launchSession.setService(MultiScreenService.this); - - MultiScreenWebAppSession webAppSession = sessions.get(webAppId); - - if (webAppSession == null) { - webAppSession = new MultiScreenWebAppSession(launchSession, MultiScreenService.this); - sessions.put(webAppId, webAppSession); - } - - webAppSession.setApplication(application); - - if (listener != null) { - Util.postSuccess(listener, webAppSession); - } - } - else { - if (listener != null) { - Util.postError(listener, new ServiceCommandError(0, "Experienced an unknown error launching app", null)); - } - } - } - }); - } - - @Override - public void onError(DeviceError error) { - Util.postError(listener, new ServiceCommandError((int)error.getCode(), error.getMessage(), error)); - } - }); - } - - @Override - public void joinWebApp( - final LaunchSession webAppLaunchSession, - final com.connectsdk.service.sessions.WebAppSession.LaunchListener listener) { - - device.getApplication(webAppLaunchSession.getAppId(), new DeviceAsyncResult() { - - @Override - public void onResult(Application application) { - final MultiScreenWebAppSession webAppSession; - - if (sessions.containsKey(webAppLaunchSession.getAppId())) { - webAppSession = sessions.get(webAppLaunchSession.getAppId()); - } - else { - webAppSession = new MultiScreenWebAppSession(webAppLaunchSession, MultiScreenService.this); - sessions.put(webAppLaunchSession.getAppId(), webAppSession); - } - - webAppSession.setApplication(application); - webAppSession.join(new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - Util.postSuccess(listener, webAppSession); - } - }); - } - - @Override - public void onError(DeviceError error) { - if (listener != null) { - Util.postError(listener, new ServiceCommandError((int)error.getCode(), error.getMessage(), error)); - } - } - }); - } - - @Override - public void joinWebApp(String webAppId, - com.connectsdk.service.sessions.WebAppSession.LaunchListener listener) { - LaunchSession launchSession = LaunchSession.launchSessionForAppId(webAppId); - launchSession.setSessionType(LaunchSessionType.WebApp); - launchSession.setService(this); - - getWebAppLauncher().joinWebApp(launchSession, listener); - } - - @Override - public void closeWebApp(final LaunchSession launchSession, - final ResponseListener listener) { - ServiceCommandError error = null; - - if (launchSession == null || launchSession.getAppId() == null || launchSession.getAppId().length() == 0) { - error = new ServiceCommandError(0, "You must provide a valid launch session", null); - } - - if (device == null) { - error = new ServiceCommandError(0, "Could not find a reference to the native device object", null); - } - - if (error != null) { - if (listener != null) { - Util.postError(listener, error); - } - - return; - } - - device.getApplication(launchSession.getAppId(), new DeviceAsyncResult() { - - @Override - public void onError(DeviceError error) { - if (listener != null) { - Util.postError(listener, new ServiceCommandError((int)error.getCode(), error.getMessage(), error)); - } - } - - @Override - public void onResult(Application application) { - if (application.getLastKnownStatus() == Status.RUNNING) { - application.terminate(new ApplicationAsyncResult() { - - @Override - public void onResult(Boolean terminateSuccess) { - if (terminateSuccess) { - sessions.remove(launchSession.getAppId()); - - if (listener != null) { - Util.postSuccess(listener, null); - } - } - else { - if (listener != null) { - Util.postError(listener, new ServiceCommandError(0, "Experienced an unknown error terminating app", null)); - } - } - } - - @Override - public void onError(ApplicationError error) { - Util.postError(listener, new ServiceCommandError((int)error.getCode(), error.getMessage(), error)); - } - }); - } - else { - if (listener != null) { - Util.postSuccess(listener, null); - } - } - } - }); - } - - public Device getDevice() { - return device; - } - - @Override - protected void updateCapabilities() { - List capabilities = new ArrayList(); - - for (String capability : MediaPlayer.Capabilities) { capabilities.add(capability); } - - capabilities.add(MediaControl.Play); - capabilities.add(MediaControl.Pause); - capabilities.add(MediaControl.Duration); - capabilities.add(MediaControl.Seek); - capabilities.add(MediaControl.Position); - capabilities.add(MediaControl.PlayState); - capabilities.add(MediaControl.PlayState_Subscribe); - - capabilities.add(Launch); - capabilities.add(Launch_Params); - capabilities.add(Join); - capabilities.add(Connect); - capabilities.add(Disconnect); - capabilities.add(Message_Send); - capabilities.add(Message_Send_JSON); - capabilities.add(Message_Receive); - capabilities.add(Message_Receive_JSON); - capabilities.add(WebAppLauncher.Close); - - setCapabilities(capabilities); - } -} diff --git a/src/com/connectsdk/service/config/CastServiceDescription.java b/src/com/connectsdk/service/config/CastServiceDescription.java deleted file mode 100644 index dbc0f9a9..00000000 --- a/src/com/connectsdk/service/config/CastServiceDescription.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * CastServiceDescription - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Hyun Kook Khang on 20 Feb 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.config; - -import com.google.android.gms.cast.CastDevice; - -public class CastServiceDescription extends ServiceDescription { - CastDevice castDevice; - - public CastServiceDescription(String serviceFilter, String UUID, String ipAddress, CastDevice castDevice) { - super(serviceFilter, UUID, ipAddress); - this.castDevice = castDevice; - } - - public CastDevice getCastDevice() { - return castDevice; - } - - public void setCastDevice(CastDevice castDevice) { - this.castDevice = castDevice; - } - -} diff --git a/src/com/connectsdk/service/sessions/CastWebAppSession.java b/src/com/connectsdk/service/sessions/CastWebAppSession.java deleted file mode 100644 index 48a3773f..00000000 --- a/src/com/connectsdk/service/sessions/CastWebAppSession.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * CastWebAppSession - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Hyun Kook Khang on 07 Mar 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.sessions; - -import java.io.IOException; - -import org.json.JSONObject; - -import android.util.Log; - -import com.connectsdk.core.Util; -import com.connectsdk.service.CastService; -import com.connectsdk.service.CastServiceChannel; -import com.connectsdk.service.DeviceService; -import com.connectsdk.service.capability.MediaPlayer; -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.command.ServiceCommandError; -import com.connectsdk.service.command.URLServiceSubscription; -import com.google.android.gms.cast.ApplicationMetadata; -import com.google.android.gms.cast.Cast; -import com.google.android.gms.common.api.ResultCallback; -import com.google.android.gms.common.api.Status; - -public class CastWebAppSession extends WebAppSession { - private CastService service; - private CastServiceChannel castServiceChannel; - private ApplicationMetadata metadata; - - public CastWebAppSession(LaunchSession launchSession, DeviceService service) { - super(launchSession, service); - - this.service = (CastService) service; - } - - @Override - public void connect(final ResponseListener listener) { - if (castServiceChannel != null) { - disconnectFromWebApp(launchSession); - } - - castServiceChannel = new CastServiceChannel(launchSession.getAppId(), this); - - try { - Cast.CastApi.removeMessageReceivedCallbacks(service.getApiClient(), castServiceChannel.getNamespace()); - - Cast.CastApi.setMessageReceivedCallbacks(service.getApiClient(), - castServiceChannel.getNamespace(), - castServiceChannel); - - Util.postSuccess(listener, null); - } catch (IOException e) { - castServiceChannel = null; - - Util.postError(listener, new ServiceCommandError(0, "Failed to create channel", null)); - } - } - - @Override - public void join(ResponseListener connectionListener) { - connect(connectionListener); - } - - public void disconnectFromWebApp(LaunchSession launchSession) { - if (castServiceChannel == null) - return; - - try { - Cast.CastApi.removeMessageReceivedCallbacks(service.getApiClient(), castServiceChannel.getNamespace()); - castServiceChannel = null; - } catch (IOException e) { - Log.e("Connect SDK", "Exception while removing application", e); - } - } - - public void handleAppClose() { - for (URLServiceSubscription subscription: service.getSubscriptions()) { - if (subscription.getTarget().equalsIgnoreCase("PlayState")) { - for (int i = 0; i < subscription.getListeners().size(); i++) { - @SuppressWarnings("unchecked") - ResponseListener listener = (ResponseListener) subscription.getListeners().get(i); - Util.postSuccess(listener, PlayStateStatus.Idle); - } - } - } - - if (getWebAppSessionListener() != null) { - getWebAppSessionListener().onWebAppSessionDisconnect(this); - } - } - - @Override - public void sendMessage(String message, final ResponseListener listener) { - if (message == null) { - Util.postError(listener, new ServiceCommandError(0, "Cannot send null message", null)); - return; - } - - if (castServiceChannel == null) { - Util.postError(listener, new ServiceCommandError(0, "Cannot send a message to the web app without first connecting", null)); - return; - } - - Cast.CastApi.sendMessage(service.getApiClient(), castServiceChannel.getNamespace(), message).setResultCallback(new ResultCallback() { - - @Override - public void onResult(Status result) { - if (result.isSuccess()) { - Util.postSuccess(listener, null); - } - else { - Util.postError(listener, new ServiceCommandError(result.getStatusCode(), result.toString(), result)); - } - } - }); - } - - @Override - public void sendMessage(JSONObject message, ResponseListener listener) { - sendMessage(message.toString(), listener); - } - - @Override - public void close(ResponseListener listener) { - launchSession.close(listener); - } - - /**************** - * Media Player * - ****************/ - @Override - public MediaPlayer getMediaPlayer() { - return this; - } - - @Override - public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void playMedia(String url, String mimeType, String title, String description, String iconSrc, boolean shouldLoop, MediaPlayer.LaunchListener listener) { - service.playMedia(url, mimeType, title, description, iconSrc, shouldLoop, listener); - } - - @Override - public void closeMedia(LaunchSession launchSession, ResponseListener listener) { - close(listener); - } - - public ApplicationMetadata getMetadata() { - return metadata; - } - - public void setMetadata(ApplicationMetadata metadata) { - this.metadata = metadata; - } -} diff --git a/src/com/connectsdk/service/sessions/MultiScreenWebAppSession.java b/src/com/connectsdk/service/sessions/MultiScreenWebAppSession.java deleted file mode 100644 index 05e431cc..00000000 --- a/src/com/connectsdk/service/sessions/MultiScreenWebAppSession.java +++ /dev/null @@ -1,843 +0,0 @@ -package com.connectsdk.service.sessions; - -import java.util.HashMap; -import java.util.Map; - -import org.json.JSONException; -import org.json.JSONObject; - -import com.connectsdk.core.Util; -import com.connectsdk.service.DeviceService; -import com.connectsdk.service.MultiScreenService; -import com.connectsdk.service.capability.MediaControl; -import com.connectsdk.service.capability.MediaPlayer; -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.samsung.multiscreen.application.Application; -import com.samsung.multiscreen.application.Application.Status; -import com.samsung.multiscreen.application.ApplicationAsyncResult; -import com.samsung.multiscreen.application.ApplicationError; -import com.samsung.multiscreen.channel.Channel; -import com.samsung.multiscreen.channel.ChannelAsyncResult; -import com.samsung.multiscreen.channel.ChannelClient; -import com.samsung.multiscreen.channel.ChannelError; -import com.samsung.multiscreen.channel.IChannelListener; -import com.samsung.multiscreen.device.DeviceAsyncResult; -import com.samsung.multiscreen.device.DeviceError; - -public class MultiScreenWebAppSession extends WebAppSession { - protected MultiScreenService service; - protected Application application; - - private final String channelId = "com.connectsdk.MainChannel"; - - ServiceSubscription playStateSubscription; - Map>> activeCommands; - - private int UID; - - private Channel mChannel; - private IChannelListener channelListener = new IChannelListener() { - - @Override - public void onDisconnect() { - mChannel = null; - } - - @Override - public void onConnect() { - // TODO Auto-generated method stub - - } - - @Override - public void onClientMessage(ChannelClient client, String message) { - try { - JSONObject messageJSON = new JSONObject(message); - - String contentType = messageJSON.optString("contentType"); - String str = new String("connectsdk."); - - if (contentType != null && contentType.contains(str)) { - String payloadKey = contentType.substring(str.length()); - - if (payloadKey == null || payloadKey.length() == 0) - return; - - JSONObject messagePayload = messageJSON.optJSONObject(payloadKey); - - if (messagePayload == null) - return; - - if (payloadKey.equals("mediaEvent")) { - handleMediaEvent(messagePayload); - } - else if (payloadKey.equals("mediaCommandResponse")) { - handleMediaCommandResponse(messagePayload); - } - - } - else { - handleMessage(messageJSON); - } - } catch (JSONException e) { - handleMessage(message); - } - } - - @Override - public void onClientDisconnected(ChannelClient client) { - // TODO Auto-generated method stub - - } - - @Override - public void onClientConnected(ChannelClient client) { - // TODO Auto-generated method stub - - } - }; - - public void handleMediaEvent(JSONObject payload) { - String type = payload.optString("type", null); - - if (type.equals("playState")) { - if (playStateSubscription == null) - return; - - String playStateString = payload.optString("playState"); - PlayStateStatus playState = parsePlayState(playStateString); - - for (PlayStateListener listener: playStateSubscription.getListeners()) { - Util.postSuccess(listener, playState); - } - } - } - - public void handleMediaCommandResponse(JSONObject payload) { - String requestId = payload.optString("requestId"); - - ServiceCommand command = activeCommands.get(requestId); - - if (command == null) - return; - - String error = payload.optString("error", null); - - if (error != null) { - if (command.getResponseListener() != null) { - command.getResponseListener().onError(new ServiceCommandError(0, error, null)); - } - } - else { - if (command.getResponseListener() != null) { - command.getResponseListener().onSuccess(payload); - } - } - - activeCommands.remove(requestId); - } - - public void handleMessage(Object message) { - if (getWebAppSessionListener() != null) { - getWebAppSessionListener().onReceiveMessage(this, message); - } - } - - public MultiScreenWebAppSession(LaunchSession launchSession, - DeviceService service) { - super(launchSession, service); - - this.service = (MultiScreenService) service; - } - - public PlayStateStatus parsePlayState(String playStateString) { - PlayStateStatus playState = PlayStateStatus.Unknown; - - if (playStateString.equals("playing")) - playState = PlayStateStatus.Playing; - else if (playStateString.equals("paused")) - playState = PlayStateStatus.Paused; - else if (playStateString.equals("idle")) - playState = PlayStateStatus.Idle; - else if (playStateString.equals("buffering")) - playState = PlayStateStatus.Buffering; - else if (playStateString.equals("finished")) - playState = PlayStateStatus.Finished; - - return playState; - - } - - public int getNextId() { - return UID++; - } - - @Override - public void connect(final ResponseListener listener) { - if (service == null || service.getDevice() == null) { - if (listener != null) { - Util.postError(listener, new ServiceCommandError(0, "You can only connect to a valid WebAppSession object.", null)); - } - - return; - } - - activeCommands = new HashMap>>(); - UID = 0; - - service.getDevice().connectToChannel(channelId, new DeviceAsyncResult() { - - @Override - public void onResult(Channel channel) { - mChannel= channel; - mChannel.setListener(channelListener); - - if (listener != null) { - Util.postSuccess(listener, null); - } - } - - @Override - public void onError(DeviceError error) { - if (listener != null) { - Util.postError(listener, new ServiceCommandError((int)error.getCode(), error.getMessage(), error)); - } - } - }); - } - - @Override - public void join(final ResponseListener listener) { - application.updateStatus(new ApplicationAsyncResult() { - - @Override - public void onResult(Status status) { - if (status == Status.RUNNING) { - connect(listener); - } - else { - if (listener != null) { - listener.onError(new ServiceCommandError(0, "Cannot join a web app that is not running", null)); - } - } - } - - @Override - public void onError(ApplicationError error) { - if (listener != null) { - Util.postError(listener, new ServiceCommandError((int)error.getCode(), error.getMessage(), error)); - } - } - }); - } - - @Override - public void sendMessage(String message, ResponseListener listener) { - if (message == null || message.length() == 0) { - if (listener != null) { - Util.postError(listener, new ServiceCommandError(0, "Cannot send an empty message", null)); - } - - return; - } - - if (mChannel != null && mChannel.isConnected()) { - mChannel.sendToHost(message); - - if (listener != null) { - Util.postSuccess(listener, null); - } - } - else { - if (listener != null) { - Util.postError(listener, new ServiceCommandError(0, "Connection has not been established or has been lost", null)); - } - } - } - - @Override - public void sendMessage(JSONObject message, ResponseListener listener) { - if (message == null || message.length() == 0) { - if (listener != null) { - Util.postError(listener, new ServiceCommandError(0, "Cannot send an empty message", null)); - } - - return; - } - - sendMessage(message.toString(), listener); - } - - @Override - public void disconnectFromWebApp() { - if (mChannel == null) { - return; - } - - mChannel.disconnect(new ChannelAsyncResult() { - - @Override - public void onResult(Boolean result) { - mChannel.setListener(null); - - if (getWebAppSessionListener() != null) { - getWebAppSessionListener().onWebAppSessionDisconnect(MultiScreenWebAppSession.this); - } - } - - @Override - public void onError(ChannelError error) {} - }); - } - - @Override - public void close(final ResponseListener listener) { - if (mChannel != null && mChannel.isConnected()) { -// // This is a hack to enable closing of bridged web apps that we didn't open - JSONObject closeCommand = new JSONObject(); - JSONObject type = new JSONObject(); - - try { - type.put("type", "close"); - - closeCommand.put("contentType", "connectsdk.serviceCommand"); - closeCommand.put("serviceCommand", type); - } catch (JSONException e) { - e.printStackTrace(); - } - - sendMessage(closeCommand, new ResponseListener() { - - @Override - public void onSuccess(Object object) { - disconnectFromWebApp(); - - if (listener != null) { - Util.postSuccess(listener, null); - } - } - - @Override - public void onError(ServiceCommandError error) { - disconnectFromWebApp(); - - if (listener != null) { - Util.postError(listener, error); - } - } - }); - } - else { - service.getWebAppLauncher().closeWebApp(launchSession, listener); - } - } - - @Override - public MediaPlayer getMediaPlayer() { - return this; - } - - @Override - public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void displayImage(String url, String mimeType, String title, - String description, String iconSrc, final MediaPlayer.LaunchListener listener) { - int requestIdNumber = getNextId(); - String requestId = String.format("req%d", requestIdNumber); - - JSONObject message = new JSONObject(); - JSONObject mediaCommand = new JSONObject(); - - try { - mediaCommand.put("type", "displayImage"); - mediaCommand.put("mediaURL", url); - mediaCommand.put("iconURL", iconSrc); - mediaCommand.put("title", title); - mediaCommand.put("description", description); - mediaCommand.put("mimeType", mimeType); - mediaCommand.put("requestId", requestId); - - message.put("contentType", "connectsdk.mediaCommand"); - message.put("mediaCommand", mediaCommand); - } catch (JSONException e) { - e.printStackTrace(); - } - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) { - Util.postError(listener, error); - } - } - - @Override - public void onSuccess(Object object) { - if (listener != null) { - Util.postSuccess(listener, new MediaLaunchObject(launchSession, getMediaControl())); - } - } - }; - ServiceCommand> command = new ServiceCommand>(null, null, null, responseListener); - activeCommands.put(requestId, command); - - sendMessage(message.toString(), new ResponseListener() { - - @Override - public void onSuccess(Object object) { - // TODO Auto-generated method stub - - } - - @Override - public void onError(ServiceCommandError error) { - // TODO Auto-generated method stub - - } - }); - } - - @Override - public void playMedia(String url, String mimeType, String title, - String description, String iconSrc, boolean shouldLoop, - final MediaPlayer.LaunchListener listener) { - int requestIdNumber = getNextId(); - String requestId = String.format("req%d", requestIdNumber); - - JSONObject message = new JSONObject(); - JSONObject mediaCommand = new JSONObject(); - - try { - mediaCommand.put("type", "playMedia"); - mediaCommand.put("mediaURL", url); - mediaCommand.put("iconURL", iconSrc); - mediaCommand.put("title", title); - mediaCommand.put("description", description); - mediaCommand.put("mimeType", mimeType); - mediaCommand.put("shouldLoop", shouldLoop); - mediaCommand.put("requestId", requestId); - - message.put("contentType", "connectsdk.mediaCommand"); - message.put("mediaCommand", mediaCommand); - } catch (JSONException e) { - e.printStackTrace(); - } - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) { - Util.postError(listener, error); - } - } - - @Override - public void onSuccess(Object object) { - if (listener != null) { - Util.postSuccess(listener, new MediaLaunchObject(launchSession, getMediaControl())); - } - } - }; - ServiceCommand> command = new ServiceCommand>(null, null, null, responseListener); - activeCommands.put(requestId, command); - - sendMessage(message.toString(), new ResponseListener() { - - @Override - public void onSuccess(Object object) { - // TODO Auto-generated method stub - - } - - @Override - public void onError(ServiceCommandError error) { - // TODO Auto-generated method stub - - } - }); - } - - @Override - public void closeMedia(LaunchSession launchSession, ResponseListener listener) { - close(listener); - } - - @Override - public MediaControl getMediaControl() { - return this; - } - - @Override - public CapabilityPriorityLevel getMediaControlCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void play(final ResponseListener listener) { - int requestIdNumber = getNextId(); - String requestId = String.format("req%d", requestIdNumber); - - JSONObject message = new JSONObject(); - JSONObject mediaCommand = new JSONObject(); - - try { - mediaCommand.put("type", "play"); - mediaCommand.put("requestId", requestId); - - message.put("contentType", "connectsdk.mediaCommand"); - message.put("mediaCommand", mediaCommand); - } catch (JSONException e) { - e.printStackTrace(); - } - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) { - Util.postError(listener, error); - } - } - - @Override - public void onSuccess(Object object) { - if (listener != null) { - Util.postSuccess(listener, null); - } - } - }; - ServiceCommand> command = new ServiceCommand>(null, null, null, responseListener); - activeCommands.put(requestId, command); - - sendMessage(message.toString(), new ResponseListener() { - - @Override - public void onSuccess(Object object) { - // TODO Auto-generated method stub - - } - - @Override - public void onError(ServiceCommandError error) { - // TODO Auto-generated method stub - - } - }); - } - - @Override - public void pause(final ResponseListener listener) { - int requestIdNumber = getNextId(); - String requestId = String.format("req%d", requestIdNumber); - - JSONObject message = new JSONObject(); - JSONObject mediaCommand = new JSONObject(); - - try { - mediaCommand.put("type", "pause"); - mediaCommand.put("requestId", requestId); - - message.put("contentType", "connectsdk.mediaCommand"); - message.put("mediaCommand", mediaCommand); - } catch (JSONException e) { - e.printStackTrace(); - } - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) { - Util.postError(listener, error); - } - } - - @Override - public void onSuccess(Object object) { - if (listener != null) { - Util.postSuccess(listener, null); - } - } - }; - ServiceCommand> command = new ServiceCommand>(null, null, null, responseListener); - activeCommands.put(requestId, command); - - sendMessage(message.toString(), new ResponseListener() { - - @Override - public void onSuccess(Object object) { - // TODO Auto-generated method stub - - } - - @Override - public void onError(ServiceCommandError error) { - // TODO Auto-generated method stub - - } - }); - } - - @Override - public void seek(long position, final ResponseListener listener) { - int requestIdNumber = getNextId(); - String requestId = String.format("req%d", requestIdNumber); - - JSONObject message = new JSONObject(); - JSONObject mediaCommand = new JSONObject(); - - try { - mediaCommand.put("type", "seek"); - mediaCommand.put("position", position); - mediaCommand.put("requestId", requestId); - - message.put("contentType", "connectsdk.mediaCommand"); - message.put("mediaCommand", mediaCommand); - } catch (JSONException e) { - e.printStackTrace(); - } - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) { - Util.postError(listener, error); - } - } - - @Override - public void onSuccess(Object object) { - if (listener != null) { - Util.postSuccess(listener, null); - } - } - }; - ServiceCommand> command = new ServiceCommand>(null, null, null, responseListener); - activeCommands.put(requestId, command); - - sendMessage(message.toString(), new ResponseListener() { - - @Override - public void onSuccess(Object object) { - // TODO Auto-generated method stub - - } - - @Override - public void onError(ServiceCommandError error) { - // TODO Auto-generated method stub - - } - }); - } - - @Override - public void getPosition(final PositionListener listener) { - int requestIdNumber = getNextId(); - String requestId = String.format("req%d", requestIdNumber); - - JSONObject message = new JSONObject(); - JSONObject mediaCommand = new JSONObject(); - - try { - mediaCommand.put("type", "getPosition"); - mediaCommand.put("requestId", requestId); - - message.put("contentType", "connectsdk.mediaCommand"); - message.put("mediaCommand", mediaCommand); - } catch (JSONException e) { - e.printStackTrace(); - } - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) { - Util.postError(listener, error); - } - } - - @Override - public void onSuccess(Object object) { - JSONObject responseObject = (JSONObject)object; - String positionString = responseObject.optString("position", null); - float position = 0; - - if (positionString != null) - position = Float.parseFloat(positionString) * 1000; - - if (listener != null) { - Util.postSuccess(listener, (long)position); - } - } - }; - ServiceCommand> command = new ServiceCommand>(null, null, null, responseListener); - activeCommands.put(requestId, command); - - sendMessage(message.toString(), new ResponseListener() { - - @Override - public void onSuccess(Object object) { - // TODO Auto-generated method stub - - } - - @Override - public void onError(ServiceCommandError error) { - // TODO Auto-generated method stub - - } - }); - } - - @Override - public void getDuration(final DurationListener listener) { - int requestIdNumber = getNextId(); - String requestId = String.format("req%d", requestIdNumber); - - JSONObject message = new JSONObject(); - JSONObject mediaCommand = new JSONObject(); - - try { - mediaCommand.put("type", "getDuration"); - mediaCommand.put("requestId", requestId); - - message.put("contentType", "connectsdk.mediaCommand"); - message.put("mediaCommand", mediaCommand); - } catch (JSONException e) { - e.printStackTrace(); - } - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) { - Util.postError(listener, error); - } - } - - @Override - public void onSuccess(Object object) { - JSONObject responseObject = (JSONObject)object; - String durationString = responseObject.optString("duration", null); - float duration = 0; - - if (durationString != null) - duration = Float.parseFloat(durationString) * 1000; - - if (listener != null) { - Util.postSuccess(listener, (long)duration); - } - } - }; - ServiceCommand> command = new ServiceCommand>(null, null, null, responseListener); - activeCommands.put(requestId, command); - - sendMessage(message.toString(), new ResponseListener() { - - @Override - public void onSuccess(Object object) { - // TODO Auto-generated method stub - - } - - @Override - public void onError(ServiceCommandError error) { - // TODO Auto-generated method stub - - } - }); - } - - @Override - public void getPlayState(final PlayStateListener listener) { - int requestIdNumber = getNextId(); - String requestId = String.format("req%d", requestIdNumber); - - JSONObject message = new JSONObject(); - JSONObject mediaCommand = new JSONObject(); - - try { - mediaCommand.put("type", "getPlayState"); - mediaCommand.put("requestId", requestId); - - message.put("contentType", "connectsdk.mediaCommand"); - message.put("mediaCommand", mediaCommand); - } catch (JSONException e) { - e.printStackTrace(); - } - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) { - Util.postError(listener, error); - } - } - - @Override - public void onSuccess(Object object) { - JSONObject responseObject = (JSONObject)object; - String playStateString = responseObject.optString("playState"); - PlayStateStatus playState = parsePlayState(playStateString); - - if (listener != null) { - Util.postSuccess(listener, playState); - } - } - }; - ServiceCommand> command = new ServiceCommand>(null, null, null, responseListener); - activeCommands.put(requestId, command); - - sendMessage(message.toString(), new ResponseListener() { - - @Override - public void onSuccess(Object object) { - // TODO Auto-generated method stub - - } - - @Override - public void onError(ServiceCommandError error) { - // TODO Auto-generated method stub - - } - }); - } - - @Override - public ServiceSubscription subscribePlayState(PlayStateListener listener) { - if (playStateSubscription == null) { - playStateSubscription = new URLServiceSubscription(null, null, null, null); - } - - if (mChannel == null || !mChannel.isConnected()) { - connect(null); - } - - playStateSubscription.addListener(listener); - - return playStateSubscription; - } - - public Application getApplication() { - return application; - } - - public void setApplication(Application application) { - this.application = application; - } -} From db58ba84d22f2f34920f010f1623b7eca16af190 Mon Sep 17 00:00:00 2001 From: Simon Gladkoskok Date: Fri, 15 Aug 2014 17:09:26 -0700 Subject: [PATCH 16/76] add core submodule --- .gitmodules | 3 + core | 1 + src/com/connectsdk/core/AppInfo.java | 129 - src/com/connectsdk/core/ChannelInfo.java | 157 - .../connectsdk/core/ExternalInputInfo.java | 124 - src/com/connectsdk/core/ImageInfo.java | 116 - .../connectsdk/core/JSONDeserializable.java | 28 - src/com/connectsdk/core/JSONSerializable.java | 28 - src/com/connectsdk/core/MediaInfo.java | 177 - src/com/connectsdk/core/ProgramInfo.java | 90 - src/com/connectsdk/core/ProgramList.java | 53 - .../connectsdk/core/TextInputStatusInfo.java | 155 - src/com/connectsdk/core/Util.java | 145 - src/com/connectsdk/core/upnp/Device.java | 357 -- .../connectsdk/core/upnp/parser/Parser.java | 65 - .../connectsdk/core/upnp/service/Action.java | 36 - .../core/upnp/service/Argument.java | 38 - .../connectsdk/core/upnp/service/Service.java | 57 - .../core/upnp/service/StateVariable.java | 44 - src/com/connectsdk/core/upnp/ssdp/SSDP.java | 123 - .../core/upnp/ssdp/SSDPSearchMsg.java | 67 - .../connectsdk/core/upnp/ssdp/SSDPSocket.java | 117 - .../connectsdk/device/ConnectableDevice.java | 1019 ------ .../device/ConnectableDeviceListener.java | 81 - .../device/ConnectableDeviceStore.java | 91 - .../device/DefaultConnectableDeviceStore.java | 389 --- src/com/connectsdk/device/DevicePicker.java | 103 - .../device/DevicePickerAdapter.java | 99 - .../device/DevicePickerListView.java | 112 - .../device/DevicePickerListener.java | 15 - src/com/connectsdk/device/PairingDialog.java | 81 - .../connectsdk/device/SimpleDevicePicker.java | 287 -- .../device/SimpleDevicePickerListener.java | 25 - .../netcast/NetcastAppNumberParser.java | 56 - .../netcast/NetcastApplicationsParser.java | 94 - .../device/netcast/NetcastChannelParser.java | 169 - .../device/netcast/NetcastHttpServer.java | 288 -- .../netcast/NetcastPOSTRequestParser.java | 169 - .../device/netcast/NetcastVolumeParser.java | 72 - .../device/netcast/VirtualKeycodes.java | 112 - .../roku/RokuApplicationListParser.java | 76 - .../discovery/CapabilityFilter.java | 135 - .../discovery/DiscoveryManager.java | 867 ----- .../discovery/DiscoveryManagerListener.java | 66 - .../discovery/DiscoveryProvider.java | 77 - .../discovery/DiscoveryProviderListener.java | 54 - .../provider/SSDPDiscoveryProvider.java | 527 --- .../provider/ZeroconfDiscoveryProvider.java | 314 -- .../etc/helper/DeviceServiceReachability.java | 121 - .../connectsdk/etc/helper/HttpMessage.java | 101 - .../connectsdk/service/AirPlayService.java | 648 ---- src/com/connectsdk/service/DIALService.java | 561 ---- src/com/connectsdk/service/DLNAService.java | 777 ----- src/com/connectsdk/service/DeviceService.java | 663 ---- .../connectsdk/service/NetcastTVService.java | 2404 -------------- src/com/connectsdk/service/RokuService.java | 1152 ------- .../connectsdk/service/WebOSTVService.java | 2925 ----------------- .../service/airplay/PListBuilder.java | 143 - .../service/airplay/PListParser.java | 168 - .../service/capability/CapabilityMethods.java | 51 - .../capability/ExternalInputControl.java | 60 - .../service/capability/KeyControl.java | 59 - .../service/capability/Launcher.java | 143 - .../service/capability/MediaControl.java | 98 - .../service/capability/MediaPlayer.java | 86 - .../service/capability/MouseControl.java | 54 - .../service/capability/PowerControl.java | 41 - .../service/capability/TVControl.java | 121 - .../service/capability/TextInputControl.java | 57 - .../service/capability/ToastControl.java | 54 - .../service/capability/VolumeControl.java | 95 - .../service/capability/WebAppLauncher.java | 67 - .../capability/listeners/ErrorListener.java | 52 - .../listeners/ResponseListener.java | 36 - .../NotSupportedServiceSubscription.java | 50 - .../service/command/ServiceCommand.java | 134 - .../service/command/ServiceCommandError.java | 71 - .../service/command/ServiceSubscription.java | 33 - .../command/URLServiceSubscription.java | 80 - .../config/NetcastTVServiceConfig.java | 66 - .../service/config/ServiceConfig.java | 142 - .../service/config/ServiceDescription.java | 278 -- .../service/config/WebOSTVServiceConfig.java | 136 - .../service/sessions/LaunchSession.java | 240 -- .../service/sessions/WebAppSession.java | 411 --- .../sessions/WebAppSessionListener.java | 39 - .../service/sessions/WebOSWebAppSession.java | 1027 ------ .../service/webos/WebOSTVKeyboardInput.java | 227 -- .../webos/WebOSTVMouseSocketConnection.java | 216 -- .../webos/WebOSTVServiceSocketClient.java | 793 ----- 90 files changed, 4 insertions(+), 22164 deletions(-) create mode 160000 core delete mode 100644 src/com/connectsdk/core/AppInfo.java delete mode 100644 src/com/connectsdk/core/ChannelInfo.java delete mode 100644 src/com/connectsdk/core/ExternalInputInfo.java delete mode 100644 src/com/connectsdk/core/ImageInfo.java delete mode 100644 src/com/connectsdk/core/JSONDeserializable.java delete mode 100644 src/com/connectsdk/core/JSONSerializable.java delete mode 100644 src/com/connectsdk/core/MediaInfo.java delete mode 100644 src/com/connectsdk/core/ProgramInfo.java delete mode 100644 src/com/connectsdk/core/ProgramList.java delete mode 100644 src/com/connectsdk/core/TextInputStatusInfo.java delete mode 100644 src/com/connectsdk/core/Util.java delete mode 100644 src/com/connectsdk/core/upnp/Device.java delete mode 100644 src/com/connectsdk/core/upnp/parser/Parser.java delete mode 100644 src/com/connectsdk/core/upnp/service/Action.java delete mode 100644 src/com/connectsdk/core/upnp/service/Argument.java delete mode 100644 src/com/connectsdk/core/upnp/service/Service.java delete mode 100644 src/com/connectsdk/core/upnp/service/StateVariable.java delete mode 100644 src/com/connectsdk/core/upnp/ssdp/SSDP.java delete mode 100644 src/com/connectsdk/core/upnp/ssdp/SSDPSearchMsg.java delete mode 100644 src/com/connectsdk/core/upnp/ssdp/SSDPSocket.java delete mode 100644 src/com/connectsdk/device/ConnectableDevice.java delete mode 100644 src/com/connectsdk/device/ConnectableDeviceListener.java delete mode 100644 src/com/connectsdk/device/ConnectableDeviceStore.java delete mode 100644 src/com/connectsdk/device/DefaultConnectableDeviceStore.java delete mode 100644 src/com/connectsdk/device/DevicePicker.java delete mode 100644 src/com/connectsdk/device/DevicePickerAdapter.java delete mode 100644 src/com/connectsdk/device/DevicePickerListView.java delete mode 100644 src/com/connectsdk/device/DevicePickerListener.java delete mode 100644 src/com/connectsdk/device/PairingDialog.java delete mode 100644 src/com/connectsdk/device/SimpleDevicePicker.java delete mode 100644 src/com/connectsdk/device/SimpleDevicePickerListener.java delete mode 100644 src/com/connectsdk/device/netcast/NetcastAppNumberParser.java delete mode 100644 src/com/connectsdk/device/netcast/NetcastApplicationsParser.java delete mode 100644 src/com/connectsdk/device/netcast/NetcastChannelParser.java delete mode 100644 src/com/connectsdk/device/netcast/NetcastHttpServer.java delete mode 100644 src/com/connectsdk/device/netcast/NetcastPOSTRequestParser.java delete mode 100644 src/com/connectsdk/device/netcast/NetcastVolumeParser.java delete mode 100644 src/com/connectsdk/device/netcast/VirtualKeycodes.java delete mode 100644 src/com/connectsdk/device/roku/RokuApplicationListParser.java delete mode 100644 src/com/connectsdk/discovery/CapabilityFilter.java delete mode 100644 src/com/connectsdk/discovery/DiscoveryManager.java delete mode 100644 src/com/connectsdk/discovery/DiscoveryManagerListener.java delete mode 100644 src/com/connectsdk/discovery/DiscoveryProvider.java delete mode 100644 src/com/connectsdk/discovery/DiscoveryProviderListener.java delete mode 100644 src/com/connectsdk/discovery/provider/SSDPDiscoveryProvider.java delete mode 100644 src/com/connectsdk/discovery/provider/ZeroconfDiscoveryProvider.java delete mode 100644 src/com/connectsdk/etc/helper/DeviceServiceReachability.java delete mode 100644 src/com/connectsdk/etc/helper/HttpMessage.java delete mode 100644 src/com/connectsdk/service/AirPlayService.java delete mode 100644 src/com/connectsdk/service/DIALService.java delete mode 100644 src/com/connectsdk/service/DLNAService.java delete mode 100644 src/com/connectsdk/service/DeviceService.java delete mode 100644 src/com/connectsdk/service/NetcastTVService.java delete mode 100644 src/com/connectsdk/service/RokuService.java delete mode 100644 src/com/connectsdk/service/WebOSTVService.java delete mode 100644 src/com/connectsdk/service/airplay/PListBuilder.java delete mode 100644 src/com/connectsdk/service/airplay/PListParser.java delete mode 100644 src/com/connectsdk/service/capability/CapabilityMethods.java delete mode 100644 src/com/connectsdk/service/capability/ExternalInputControl.java delete mode 100644 src/com/connectsdk/service/capability/KeyControl.java delete mode 100644 src/com/connectsdk/service/capability/Launcher.java delete mode 100644 src/com/connectsdk/service/capability/MediaControl.java delete mode 100644 src/com/connectsdk/service/capability/MediaPlayer.java delete mode 100644 src/com/connectsdk/service/capability/MouseControl.java delete mode 100644 src/com/connectsdk/service/capability/PowerControl.java delete mode 100644 src/com/connectsdk/service/capability/TVControl.java delete mode 100644 src/com/connectsdk/service/capability/TextInputControl.java delete mode 100644 src/com/connectsdk/service/capability/ToastControl.java delete mode 100644 src/com/connectsdk/service/capability/VolumeControl.java delete mode 100644 src/com/connectsdk/service/capability/WebAppLauncher.java delete mode 100644 src/com/connectsdk/service/capability/listeners/ErrorListener.java delete mode 100644 src/com/connectsdk/service/capability/listeners/ResponseListener.java delete mode 100644 src/com/connectsdk/service/command/NotSupportedServiceSubscription.java delete mode 100644 src/com/connectsdk/service/command/ServiceCommand.java delete mode 100644 src/com/connectsdk/service/command/ServiceCommandError.java delete mode 100644 src/com/connectsdk/service/command/ServiceSubscription.java delete mode 100644 src/com/connectsdk/service/command/URLServiceSubscription.java delete mode 100644 src/com/connectsdk/service/config/NetcastTVServiceConfig.java delete mode 100644 src/com/connectsdk/service/config/ServiceConfig.java delete mode 100644 src/com/connectsdk/service/config/ServiceDescription.java delete mode 100644 src/com/connectsdk/service/config/WebOSTVServiceConfig.java delete mode 100644 src/com/connectsdk/service/sessions/LaunchSession.java delete mode 100644 src/com/connectsdk/service/sessions/WebAppSession.java delete mode 100644 src/com/connectsdk/service/sessions/WebAppSessionListener.java delete mode 100644 src/com/connectsdk/service/sessions/WebOSWebAppSession.java delete mode 100644 src/com/connectsdk/service/webos/WebOSTVKeyboardInput.java delete mode 100644 src/com/connectsdk/service/webos/WebOSTVMouseSocketConnection.java delete mode 100644 src/com/connectsdk/service/webos/WebOSTVServiceSocketClient.java diff --git a/.gitmodules b/.gitmodules index 78f2a41f..b3abf065 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "modules/samsung_multiscreen"] path = modules/samsung_multiscreen url = https://github.com/ConnectSDK/Connect-SDK-Android-Samsung-MultiScreen.git +[submodule "core"] + path = core + url = https://github.com/ConnectSDK/Connect-SDK-Android-Core.git diff --git a/core b/core new file mode 160000 index 00000000..b25c7f49 --- /dev/null +++ b/core @@ -0,0 +1 @@ +Subproject commit b25c7f49cf6baf97ff1c69e8ddd5ec529b97fef8 diff --git a/src/com/connectsdk/core/AppInfo.java b/src/com/connectsdk/core/AppInfo.java deleted file mode 100644 index 054ebbd5..00000000 --- a/src/com/connectsdk/core/AppInfo.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * AppInfo - * 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.core; - -import org.json.JSONException; -import org.json.JSONObject; - -/** - * Normalized reference object for information about a DeviceService's app. This - * object will, in most cases, be used to launch apps. - * - * In some cases, all that is needed to launch an app is the app id. - */ -public class AppInfo implements JSONSerializable { - // @cond INTERNAL - String id; - String name; - JSONObject raw; - - // @endcond - - /** - * Default constructor method. - */ - public AppInfo() { - } - - /** - * Default constructor method. - * - * @param id - * App id to launch - */ - public AppInfo(String id) { - this.id = id; - } - - /** - * Gets the ID of the app on the first screen device. Format is different - * depending on the platform. (ex. youtube.leanback.v4, 0000001134, netflix, - * etc). - */ - public String getId() { - return id; - } - - /** - * Sets the ID of the app on the first screen device. Format is different - * depending on the platform. (ex. youtube.leanback.v4, 0000001134, netflix, - * etc). - */ - public void setId(String id) { - this.id = id; - } - - /** - * Gets the user-friendly name of the app (ex. YouTube, Browser, Netflix, - * etc). - */ - public String getName() { - return name; - } - - /** - * Sets the user-friendly name of the app (ex. YouTube, Browser, Netflix, - * etc). - */ - public void setName(String name) { - this.name = name.trim(); - } - - /** Gets the raw data from the first screen device about the app. */ - public JSONObject getRawData() { - return raw; - } - - /** Sets the raw data from the first screen device about the app. */ - public void setRawData(JSONObject data) { - raw = data; - } - - // @cond INTERNAL - @Override - public JSONObject toJSONObject() throws JSONException { - JSONObject obj = new JSONObject(); - - obj.put("name", name); - obj.put("id", id); - - return obj; - } - - // @endcond - - /** - * Compares two AppInfo objects. - * - * @param o - * Other AppInfo object to compare. - * - * @return true if both AppInfo id values are equal - */ - @Override - public boolean equals(Object o) { - if (o instanceof AppInfo) { - AppInfo ai = (AppInfo) o; - return this.id.equals(ai.id); - } - return super.equals(o); - } -} diff --git a/src/com/connectsdk/core/ChannelInfo.java b/src/com/connectsdk/core/ChannelInfo.java deleted file mode 100644 index 937f9257..00000000 --- a/src/com/connectsdk/core/ChannelInfo.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * ChannelInfo - * 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.core; - -import org.json.JSONException; -import org.json.JSONObject; - -import android.util.Log; - -/** - * Normalized reference object for information about a TVs channels. This object is required to set the channel on a TV. - */ -public class ChannelInfo implements JSONSerializable { - private static final String TAG = "Connect SDK"; - - // @cond INTERNAL - String channelName; - String channelId; - String channelNumber; - int minorNumber; - int majorNumber; - - JSONObject rawData; - // @endcond - - /** - * Default constructor method. - */ - public ChannelInfo() { - } - - /** Gets the raw data from the first screen device about the channel. In most cases, this is an NSDictionary. */ - public JSONObject getRawData() { - return rawData; - } - - /** Sets the raw data from the first screen device about the channel. In most cases, this is an NSDictionary. */ - public void setRawData(JSONObject rawData) { - this.rawData = rawData; - } - - /** Gets the user-friendly name of the channel */ - public String getName() { - return channelName; - } - - /** Sets the user-friendly name of the channel */ - public void setName(String channelName) { - this.channelName = channelName; - } - - /** Gets the TV's unique ID for the channel */ - public String getId() { - return channelId; - } - - /** Sets the TV's unique ID for the channel */ - public void setId(String channelId) { - this.channelId = channelId; - } - - /** Gets the TV channel's number (likely to be a combination of the major & minor numbers) */ - public String getNumber() { - return channelNumber; - } - - /** Sets the TV channel's number (likely to be a combination of the major & minor numbers) */ - public void setNumber(String channelNumber) { - this.channelNumber = channelNumber; - } - - /** Gets the TV channel's minor number */ - public int getMinorNumber() { - return minorNumber; - } - - /** Sets the TV channel's minor number */ - public void setMinorNumber(int minorNumber) { - this.minorNumber = minorNumber; - } - - /** Gets the TV channel's major number */ - public int getMajorNumber() { - return majorNumber; - } - - /** Sets the TV channel's major number */ - public void setMajorNumber(int majorNumber) { - this.majorNumber = majorNumber; - } - - /** - * Compares two ChannelInfo objects. - * - * @param channelInfo ChannelInfo object to compare. - * - * @return YES if both ChannelInfo number & name values are equal - */ - @Override - public boolean equals(Object o) { - if (o instanceof ChannelInfo) { - ChannelInfo other = (ChannelInfo) o; - - if (this.channelId != null) { - if (this.channelId.equals(other.channelId)) - return true; - } else if (this.channelName != null && this.channelNumber != null) { - return this.channelName.equals(other.channelName) - && this.channelNumber.equals(other.channelNumber) - && this.majorNumber == other.majorNumber - && this.minorNumber == other.minorNumber; - } - - Log.d(TAG, "Could not compare channel values, no data to compare against"); - Log.d(TAG, "This channel info: \n" + this.rawData.toString()); - Log.d(TAG, "Other channel info: \n" + other.rawData.toString()); - - return false; - } - - return super.equals(o); - } - - // @cond INTERNAL - @Override - public JSONObject toJSONObject() throws JSONException { - JSONObject obj = new JSONObject(); - - obj.put("name", channelName); - obj.put("id", channelId); - obj.put("number", channelNumber); - obj.put("majorNumber", majorNumber); - obj.put("minorNumber", minorNumber); - obj.put("rawData", rawData); - - return obj; - } - // @endcond -} diff --git a/src/com/connectsdk/core/ExternalInputInfo.java b/src/com/connectsdk/core/ExternalInputInfo.java deleted file mode 100644 index 18051392..00000000 --- a/src/com/connectsdk/core/ExternalInputInfo.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * ExternalInputInfo - * 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.core; - -import org.json.JSONException; -import org.json.JSONObject; - -/** - * Normalized reference object for information about a DeviceService's external inputs. This object is required to set a DeviceService's external input. - */ -public class ExternalInputInfo implements JSONSerializable { - String id; - String name; - boolean connected; - String iconURL; - - JSONObject rawData; - - /** - * Default constructor method. - */ - public ExternalInputInfo() { - } - - /** Gets the ID of the external input on the first screen device. */ - public String getId() { - return id; - } - - /** Sets the ID of the external input on the first screen device. */ - public void setId(String inputId) { - this.id = inputId; - } - - /** Gets the user-friendly name of the external input (ex. AV, HDMI1, etc). */ - public String getName() { - return name; - } - - /** Sets the user-friendly name of the external input (ex. AV, HDMI1, etc). */ - public void setName(String inputName) { - this.name = inputName; - } - - /** Sets the raw data from the first screen device about the external input. */ - public void setRawData(JSONObject rawData) { - this.rawData = rawData; - } - - /** Gets the raw data from the first screen device about the external input. */ - public JSONObject getRawData() { - return rawData; - } - - /** Whether the DeviceService is currently connected to this external input. */ - public boolean isConnected() { - return connected; - } - - /** Sets whether the DeviceService is currently connected to this external input. */ - public void setConnected(boolean connected) { - this.connected = connected; - } - - /** Gets the URL to an icon representing this external input. */ - public String getIconURL() { - return iconURL; - } - - /** Sets the URL to an icon representing this external input. */ - public void setIconURL(String iconURL) { - this.iconURL = iconURL; - } - - // @cond INTERNAL - @Override - public JSONObject toJSONObject() throws JSONException { - JSONObject obj = new JSONObject(); - - obj.put("id", id); - obj.put("name", name); - obj.put("connected", connected); - obj.put("icon", iconURL); - obj.put("rawData", rawData); - - return obj; - } - // @endcond - - /** - * Compares two ExternalInputInfo objects. - * - * @param externalInputInfo ExternalInputInfo object to compare. - * - * @return YES if both ExternalInputInfo id & name values are equal - */ - @Override - public boolean equals(Object o) { - if (o instanceof ExternalInputInfo) { - ExternalInputInfo eii = (ExternalInputInfo) o; - return this.id.equals(eii.id) && - this.name.equals(eii.name); - } - return false; - } -} diff --git a/src/com/connectsdk/core/ImageInfo.java b/src/com/connectsdk/core/ImageInfo.java deleted file mode 100644 index 3e368bed..00000000 --- a/src/com/connectsdk/core/ImageInfo.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.connectsdk.core; - -/** - * Normalized reference object for information about an image file. This object can be used to represent a media file (ex. icon, poster) - * - */ - -public class ImageInfo { - - /** - * Default constructor method. - * @param url - */ - - public ImageInfo(String url) { - super(); - this.url = url; - } - - /** - * Default constructor method. - * @param url, type, width, height - * add type of file, width and height of image. - */ - - public ImageInfo(String url, ImageType type, int width, int height) { - this(url); - this.type = type; - this.width = width; - this.height = height; - } - - - public enum ImageType { - Thumb, Video_Poster, Album_Art, Unknown; - } - - private String url; - private ImageType type; - private int width; - private int height; - - /** - * Gets URL address of an image file. - * - */ - - public String getUrl() { - return url; - } - - - /** - * Sets URL address of an image file. - * - */ - - public void setUrl(String url) { - this.url = url; - } - - /** - * Gets a type of an image file. - * - */ - - public ImageType getType() { - return type; - } - - /** - * Sets a type of an image file. - * - */ - - public void setType(ImageType type) { - this.type = type; - } - - /** - * Gets a width of an image. - * - */ - - public int getWidth() { - return width; - } - - /** - * Sets a width of an image. - * - */ - - public void setWidth(int width) { - this.width = width; - } - - /** - * Gets a height of an image. - * - */ - - public int getHeight() { - return height; - } - - /** - * Sets a height of an image. - * - */ - - public void setHeight(int height) { - this.height = height; - } - -} diff --git a/src/com/connectsdk/core/JSONDeserializable.java b/src/com/connectsdk/core/JSONDeserializable.java deleted file mode 100644 index d904c1e3..00000000 --- a/src/com/connectsdk/core/JSONDeserializable.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * JSONDeserializable - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Jason Lai on 31 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.core; - -import org.json.JSONException; -import org.json.JSONObject; - -public interface JSONDeserializable { - public void fromJSONObject(JSONObject obj) throws JSONException; -} diff --git a/src/com/connectsdk/core/JSONSerializable.java b/src/com/connectsdk/core/JSONSerializable.java deleted file mode 100644 index 7746f367..00000000 --- a/src/com/connectsdk/core/JSONSerializable.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * JSONSerializable - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Jason Lai on 30 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.core; - -import org.json.JSONException; -import org.json.JSONObject; - -public interface JSONSerializable { - public JSONObject toJSONObject() throws JSONException; -} diff --git a/src/com/connectsdk/core/MediaInfo.java b/src/com/connectsdk/core/MediaInfo.java deleted file mode 100644 index 1b29f030..00000000 --- a/src/com/connectsdk/core/MediaInfo.java +++ /dev/null @@ -1,177 +0,0 @@ -package com.connectsdk.core; - -import java.util.ArrayList; - -/** - * Normalized reference object for information about a media to display. This object can be used to pass as a parameter to displayImage or playMedia. - * - */ - -import java.util.List; - -public class MediaInfo { - - /** - * Default constructor method. - */ - - public MediaInfo(String url, String mimeType, String title, - String description) { - super(); - this.url = url; - this.mimeType = mimeType; - this.title = title; - this.description = description; - } - - /** - * Default constructor method. - * - * @param allImages - * list of imageInfo objects where [0] is icon, [1] is poster - */ - - public MediaInfo(String url, String mimeType, String title, - String description, List allImages) { - this(url, mimeType, title, description); - this.allImages = allImages; - } - - // @cond INTERNAL - private String url, mimeType, description, title; - - private List allImages; - - private long duration; - - // @endcond - - /** - * Gets type of a media file. - * - * - */ - - public String getMimeType() { - return mimeType; - } - - /** - * Sets type of a media file. - * - * - */ - - public void setMimeType(String mimeType) { - this.mimeType = mimeType; - } - - /** - * Gets title for a media file. - * - * - */ - - public String getTitle() { - return title; - } - - /** - * Sets title of a media file. - * - * - */ - - public void setTitle(String title) { - this.title = title; - } - - /** - * Gets description for a media. - * - */ - - public String getDescription() { - return description; - } - - /** - * Sets description for a media. - * - */ - - public void setDescription(String description) { - this.description = description; - } - - /** - * Gets list of ImageInfo objects for images representing a media (ex. icon, poster). Where first ([0]) is icon image, and second ([1]) is poster image. - */ - - public List getImages() { - return allImages; - } - - /** - * Sets list of ImageInfo objects for images representing a media (ex. icon, poster). Where first ([0]) is icon image, and second ([1]) is poster image. - - * - */ - - public void setImages(List images) { - this.allImages = images; - } - - /** - * Gets duration of a media file. - * - */ - - public long getDuration() { - return duration; - } - - /** - * Sets duration of a media file. - * - */ - - public void setDuration(long duration) { - this.duration = duration; - } - - /** - * Gets URL address of a media file. - * - */ - - public String getUrl() { - return url; - } - - /** - * Sets URL address of a media file. - * - */ - - public void setUrl(String url) { - this.url = url; - } - - /** - * Stores ImageInfo objects. - * - */ - - public void addImages(ImageInfo... images) { - - List list = new ArrayList(); - for (int i = 0; i < images.length; i++) { - list.add(images[i]); - } - - this.setImages(list); - - } - -} diff --git a/src/com/connectsdk/core/ProgramInfo.java b/src/com/connectsdk/core/ProgramInfo.java deleted file mode 100644 index 71e66ec1..00000000 --- a/src/com/connectsdk/core/ProgramInfo.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * ProgramInfo - * 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.core; - -/** Normalized reference object for information about a TVs program. */ -public class ProgramInfo { - // @cond INTERNAL - private String id; - private String name; - - private ChannelInfo channelInfo; - - private Object rawData; - // @endcond - - /** Gets the ID of the program on the first screen device. Format is different depending on the platform. */ - public String getId() { - return id; - } - - /** Sets the ID of the program on the first screen device. Format is different depending on the platform. */ - public void setId(String id) { - this.id = id; - } - - /** Gets the user-friendly name of the program (ex. Sesame Street, Cosmos, Game of Thrones, etc). */ - public String getName() { - return name; - } - - /** Sets the user-friendly name of the program (ex. Sesame Street, Cosmos, Game of Thrones, etc). */ - public void setName(String name) { - this.name = name; - } - - /** Gets the reference to the ChannelInfo object that this program is associated with */ - public ChannelInfo getChannelInfo() { - return channelInfo; - } - - /** Sets the reference to the ChannelInfo object that this program is associated with */ - public void setChannelInfo(ChannelInfo channelInfo) { - this.channelInfo = channelInfo; - } - - /** Gets the raw data from the first screen device about the program. In most cases, this is an NSDictionary. */ - public Object getRawData() { - return rawData; - } - - /** Sets the raw data from the first screen device about the program. In most cases, this is an NSDictionary. */ - public void setRawData(Object rawData) { - this.rawData = rawData; - } - - /** - * Compares two ProgramInfo objects. - * - * @param programInfo ProgramInfo object to compare. - * - * @return true if both ProgramInfo id & name values are equal - */ - @Override - public boolean equals(Object o) { - if (o instanceof ProgramInfo) { - ProgramInfo pi = (ProgramInfo) o; - return pi.id.equals(pi.id) && - pi.name.equals(pi.name); - } - return super.equals(o); - } -} diff --git a/src/com/connectsdk/core/ProgramList.java b/src/com/connectsdk/core/ProgramList.java deleted file mode 100644 index 7ba3fb61..00000000 --- a/src/com/connectsdk/core/ProgramList.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * ProgramList - * 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.core; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -public class ProgramList implements JSONSerializable { - ChannelInfo channel; - JSONArray programList; - - public ProgramList(ChannelInfo channel, JSONArray programList) { - this.channel = channel; - this.programList = programList; - } - - public ChannelInfo getChannel() { - return channel; - } - - public JSONArray getProgramList() { - return programList; - } - - @Override - public JSONObject toJSONObject() throws JSONException { - JSONObject obj = new JSONObject(); - - obj.put("channel", channel != null ? channel.toString() : null); - obj.put("programList", programList != null ? programList.toString() : null); - - return obj; - } -} \ No newline at end of file diff --git a/src/com/connectsdk/core/TextInputStatusInfo.java b/src/com/connectsdk/core/TextInputStatusInfo.java deleted file mode 100644 index 4dc15a22..00000000 --- a/src/com/connectsdk/core/TextInputStatusInfo.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * TextInputStatusInfo - * 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.core; - -import org.json.JSONObject; - -/** Normalized reference object for information about a text input event. */ -public class TextInputStatusInfo { - // @cond INTERNAL - public enum TextInputType { - DEFAULT, - URL, - NUMBER, - PHONE_NUMBER, - EMAIL - } - - boolean focused = false; - String contentType = null; - boolean predictionEnabled = false; - boolean correctionEnabled = false; - boolean autoCapitalization = false; - boolean hiddenText = false; - boolean focusChanged = false; - - JSONObject rawData; - // @endcond - - public TextInputStatusInfo() { - } - - public boolean isFocused() { - return focused; - } - - public void setFocused(boolean focused) { - this.focused = focused; - } - - /** Gets the type of keyboard that should be displayed to the user. */ - public TextInputType getTextInputType() { - TextInputType textInputType = TextInputType.DEFAULT; - - if ( contentType != null ) { - if ( contentType.equals("number") ) { - textInputType = TextInputType.NUMBER; - } - else if ( contentType.equals("phonenumber")) { - textInputType = TextInputType.PHONE_NUMBER; - } - else if ( contentType.equals("url")) { - textInputType = TextInputType.URL; - } - else if ( contentType.equals("email")) { - textInputType = TextInputType.EMAIL; - } - } - - return textInputType; - } - - /** Sets the type of keyboard that should be displayed to the user. */ - public void setTextInputType(TextInputType textInputType) { - switch ( textInputType ) { - case NUMBER: - contentType = "number"; - break; - case PHONE_NUMBER: - contentType = "phonenumber"; - break; - case URL: - contentType = "url"; - break; - case EMAIL: - contentType = "number"; - break; - case DEFAULT: - default: - contentType = "email"; - break; - } - } - - public void setContentType(String contentType) { - this.contentType = contentType; - } - - public boolean isPredictionEnabled() { - return predictionEnabled; - } - - public void setPredictionEnabled(boolean predictionEnabled) { - this.predictionEnabled = predictionEnabled; - } - - public boolean isCorrectionEnabled() { - return correctionEnabled; - } - - public void setCorrectionEnabled(boolean correctionEnabled) { - this.correctionEnabled = correctionEnabled; - } - - public boolean isAutoCapitalization() { - return autoCapitalization; - } - - public void setAutoCapitalization(boolean autoCapitalization) { - this.autoCapitalization = autoCapitalization; - } - - public boolean isHiddenText() { - return hiddenText; - } - - public void setHiddenText(boolean hiddenText) { - this.hiddenText = hiddenText; - } - - /** Gets the raw data from the first screen device about the text input status. */ - public JSONObject getRawData() { - return rawData; - } - - /** Sets the raw data from the first screen device about the text input status. */ - public void setRawData(JSONObject data) { - rawData = data; - } - - public boolean isFocusChanged() { - return focusChanged; - } - - public void setFocusChanged(boolean focusChanged) { - this.focusChanged = focusChanged; - } -} diff --git a/src/com/connectsdk/core/Util.java b/src/com/connectsdk/core/Util.java deleted file mode 100644 index 5264a259..00000000 --- a/src/com/connectsdk/core/Util.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Util - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Jeffrey Glenn on 27 Feb 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.core; - -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.Date; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; - -import org.apache.http.conn.util.InetAddressUtils; - -import android.content.Context; -import android.net.wifi.WifiInfo; -import android.net.wifi.WifiManager; -import android.os.Handler; -import android.os.Looper; - -import com.connectsdk.service.capability.listeners.ErrorListener; -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.command.ServiceCommandError; - -public final class Util { - static public String T = "Connect SDK"; - static private Handler handler; - static private final int NUM_OF_THREADS = 20; - - static private Executor executor = Executors.newFixedThreadPool(NUM_OF_THREADS, new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - Thread th = new Thread(r); - th.setName("2nd Screen BG"); - return th; - } - }); - - public static void runOnUI(Runnable runnable) { - if (handler == null) { - handler = new Handler(Looper.getMainLooper()); - } - - handler.post(runnable); - } - - public static void runInBackground(Runnable runnable, boolean forceNewThread) { - if (forceNewThread || isMain()) { - executor.execute(runnable); - } else { - runnable.run(); - } - - } - - public static void runInBackground(Runnable runnable) { - runInBackground(runnable, false); - } - - public static Executor getExecutor() { - return executor; - } - - public static boolean isMain() { - return Looper.myLooper() == Looper.getMainLooper(); - } - - public static void postSuccess(final ResponseListener listener, final T object) { - if (listener == null) - return; - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - listener.onSuccess(object); - } - }); - } - - public static void postError(final ErrorListener listener, final ServiceCommandError error) { - if (listener == null) - return; - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - listener.onError(error); - } - }); - } - - public static byte[] convertIpAddress(int ip) { - return new byte[] { - (byte) (ip & 0xFF), - (byte) ((ip >> 8) & 0xFF), - (byte) ((ip >> 16) & 0xFF), - (byte) ((ip >> 24) & 0xFF)}; - } - - public static long getTime() { - return TimeUnit.MILLISECONDS.toSeconds(new Date().getTime()); - } - - public static boolean isIPv4Address(String ipAddress) { - return InetAddressUtils.isIPv4Address(ipAddress); - } - - public static boolean isIPv6Address(String ipAddress) { - return InetAddressUtils.isIPv6Address(ipAddress); - } - - public static InetAddress getIpAddress(Context context) throws UnknownHostException { - WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); - WifiInfo wifiInfo = wifiMgr.getConnectionInfo(); - int ip = wifiInfo.getIpAddress(); - - if (ip == 0) { - return null; - } - else { - byte[] ipAddress = convertIpAddress(ip); - return InetAddress.getByAddress(ipAddress); - } - } -} \ No newline at end of file diff --git a/src/com/connectsdk/core/upnp/Device.java b/src/com/connectsdk/core/upnp/Device.java deleted file mode 100644 index 4fb44a62..00000000 --- a/src/com/connectsdk/core/upnp/Device.java +++ /dev/null @@ -1,357 +0,0 @@ -/* - * Device - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Copyright (c) 2011 stonker.lee@gmail.com https://code.google.com/p/android-dlna/ - * - * 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.core.upnp; - -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLConnection; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Scanner; - -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; - -import org.apache.http.HttpResponse; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.DefaultHttpClient; -import org.xml.sax.Attributes; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; - -import com.connectsdk.core.upnp.service.Service; -import com.connectsdk.core.upnp.ssdp.SSDP; - -public class Device { - public static final String TAG = "device"; - public static final String TAG_DEVICE_TYPE = "deviceType"; - public static final String TAG_FRIENDLY_NAME = "friendlyName"; - public static final String TAG_MANUFACTURER = "manufacturer"; - public static final String TAG_MANUFACTURER_URL = "manufacturerURL"; - public static final String TAG_MODEL_DESCRIPTION = "modelDescription"; - public static final String TAG_MODEL_NAME = "modelName"; - public static final String TAG_MODEL_NUMBER = "modelNumber"; - public static final String TAG_MODEL_URL = "modelURL"; - public static final String TAG_SERIAL_NUMBER = "serialNumber"; - public static final String TAG_UDN = "UDN"; - public static final String TAG_UPC = "UPC"; - public static final String TAG_ICON_LIST = "iconList"; - public static final String TAG_SERVICE_LIST = "serviceList"; - - public static final String TAG_SEC_CAPABILITY = "sec:Capability"; - public static final String TAG_PORT = "port"; - public static final String TAG_LOCATION = "location"; - - public static final String HEADER_SERVER = "Server"; - - /* Required. UPnP device type. */ - public String deviceType; - /* Required. Short description for end user. */ - public String friendlyName; - /* Required. Manufacturer's name. */ - public String manufacturer; - /* Optional. Web site for manufacturer. */ - public String manufacturerURL; - /* Recommended. Long description for end user. */ - public String modelDescription; - /* Required. Model name. */ - public String modelName; - /* Recommended. Model number. */ - public String modelNumber; - /* Optional. Web site for model. */ - public String modelURL; - /* Recommended. Serial number. */ - public String serialNumber; - /* Required. Unique Device Name. */ - public String UDN; - /* Optional. Universal Product Code. */ - public String UPC; - /* Required. */ - List iconList = new ArrayList(); - public String locationXML; - /* Optional. */ - public List serviceList = new ArrayList(); - public String searchTarget; - public String applicationURL; - - public String serviceURI; - - public String baseURL; - public String ipAddress; - public int port; - public String UUID; - - public Map> headers; - - public Device(String url, String searchTarget) throws IOException { - URL urlObject = new URL(url); - - if (urlObject.getPort() == -1) { - baseURL = String.format("%s://%s", urlObject.getProtocol(), urlObject.getHost()); - } else { - baseURL = String.format("%s://%s:%d", urlObject.getProtocol(), urlObject.getHost(), urlObject.getPort()); - } - ipAddress = urlObject.getHost(); - port = urlObject.getPort(); - this.searchTarget = searchTarget; - UUID = null; - - serviceURI = String.format("%s://%s", urlObject.getProtocol(), urlObject.getHost()); - - if ( searchTarget.equalsIgnoreCase("urn:dial-multiscreen-org:service:dial:1") ) - applicationURL = getApplicationURL(url); - } - - public static Device createInstanceFromXML(String url, String searchTarget) { - Device newDevice = null; - try { - newDevice = new Device(url, searchTarget); - } catch(IOException e) { - return null; - } - - final Device device = newDevice; - - DefaultHandler dh = new DefaultHandler() { - String currentValue = null; - Icon currentIcon; - Service currentService; - - @Override - public void characters(char[] ch, int start, int length) throws SAXException { - if (currentValue == null) { - currentValue = new String(ch, start, length); - } else { - // append to existing string (needed for parsing character entities) - currentValue += new String(ch, start, length); - } - } - - @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { - if (Icon.TAG.equals(qName)) { - currentIcon = new Icon(); - } else if (Service.TAG.equals(qName)) { - currentService = new Service(); - currentService.baseURL = device.baseURL; - } - // Samsung MultiScreen Capability - else if (TAG_SEC_CAPABILITY.equals(qName)) { - String port = null; - String location = null; - - for (int i = 0; i < attributes.getLength(); i++) { - if (TAG_PORT.equals(attributes.getLocalName(i))) { - port = attributes.getValue(i); - } - else if (TAG_LOCATION.equals(attributes.getLocalName(i))) { - location = attributes.getValue(i); - } - } - - if (port == null) { - device.serviceURI = String.format("%s%s", device.serviceURI, location); - } - else { - device.serviceURI = String.format("%s:%s%s", device.serviceURI, port, location); - } - } - currentValue = null; - } - - @Override - public void endElement(String uri, String localName, String qName) throws SAXException { -// System.out.println("[DEBUG] qName: " + qName + ", currentValue: " + currentValue); - /* Parse device-specific information */ - if (TAG_DEVICE_TYPE.equals(qName)) { - device.deviceType = currentValue; - } else if (TAG_FRIENDLY_NAME.equals(qName)) { - device.friendlyName = currentValue; - } else if (TAG_MANUFACTURER.equals(qName)) { - device.manufacturer = currentValue; - } else if (TAG_MANUFACTURER_URL.equals(qName)) { - device.manufacturerURL = currentValue; - } else if (TAG_MODEL_DESCRIPTION.equals(qName)) { - device.modelDescription = currentValue; - } else if (TAG_MODEL_NAME.equals(qName)) { - device.modelName = currentValue; - } else if (TAG_MODEL_NUMBER.equals(qName)) { - device.modelNumber = currentValue; - } else if (TAG_MODEL_URL.equals(qName)) { - device.modelURL = currentValue; - } else if (TAG_SERIAL_NUMBER.equals(qName)) { - device.serialNumber = currentValue; - } else if (TAG_UDN.equals(qName)) { - device.UDN = currentValue; - -// device.UUID = Device.parseUUID(currentValue); - } else if (TAG_UPC.equals(qName)) { - device.UPC = currentValue; - } - /* Parse icon-list information */ - else if (Icon.TAG_MIME_TYPE.equals(qName)) { - currentIcon.mimetype = currentValue; - } else if (Icon.TAG_WIDTH.equals(qName)) { - currentIcon.width = currentValue; - } else if (Icon.TAG_HEIGHT.equals(qName)) { - currentIcon.height = currentValue; - } else if (Icon.TAG_DEPTH.equals(qName)) { - currentIcon.depth = currentValue; - } else if (Icon.TAG_URL.equals(qName)) { - currentIcon.url = currentValue; - } else if (Icon.TAG.equals(qName)) { - device.iconList.add(currentIcon); - } - /* Parse service-list information */ - else if (Service.TAG_SERVICE_TYPE.equals(qName)) { - currentService.serviceType = currentValue; - } else if (Service.TAG_SERVICE_ID.equals(qName)) { - currentService.serviceId = currentValue; - } else if (Service.TAG_SCPD_URL.equals(qName)) { - currentService.SCPDURL = currentValue; - } else if (Service.TAG_CONTROL_URL.equals(qName)) { - currentService.controlURL = currentValue; - } else if (Service.TAG_EVENTSUB_URL.equals(qName)) { - currentService.eventSubURL = currentValue; - } else if (Service.TAG.equals(qName)) { - device.serviceList.add(currentService); - } - - currentValue = null; - } - }; - - SAXParserFactory factory = SAXParserFactory.newInstance(); - - SAXParser parser; - try { - URL mURL = new URL(url); - URLConnection urlConnection = mURL.openConnection(); - InputStream in = new BufferedInputStream(urlConnection.getInputStream()); - try { - Scanner s = new Scanner(in).useDelimiter("\\A"); - device.locationXML = s.hasNext() ? s.next() : ""; - - parser = factory.newSAXParser(); - parser.parse(new ByteArrayInputStream(device.locationXML.getBytes()), dh); - } finally { - in.close(); - } - - device.headers = urlConnection.getHeaderFields(); - - return device; - } catch (MalformedURLException e) { - e.printStackTrace(); - } catch (ParserConfigurationException e) { - e.printStackTrace(); - } catch (SAXException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - - return null; - } - - private String getApplicationURL(String url) { - HttpClient client = new DefaultHttpClient(); - - HttpGet get = new HttpGet(url); - - String applicationURL = null; - - try { - HttpResponse response = client.execute(get); - - int code = response.getStatusLine().getStatusCode(); - - if ( code == 200 ) { - if ( response.getFirstHeader(SSDP.APPLICATION_URL) != null ) { - applicationURL = response.getFirstHeader(SSDP.APPLICATION_URL).getValue(); - - if (!applicationURL.substring(applicationURL.length() - 1).equals("/")) { - applicationURL = applicationURL.concat("/"); - } - } - } - } catch (ClientProtocolException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - return applicationURL; - } - - protected static String parseUUID(String str) { - String uuidColon = "uuid:"; - String colonColon = "::"; - if (str == null) - return ""; - - int start = str.indexOf(uuidColon); - - if ( start != -1 ) { - start += uuidColon.length(); - int end = str.indexOf(colonColon); - if ( end != -1 ) - return str.substring(start, end); - else - return str.substring(start); - } - else { - return str; - } - } - - @Override - public String toString() { - return friendlyName; - } - - static class Icon { - static final String TAG = "icon"; - static final String TAG_MIME_TYPE = "mimetype"; - static final String TAG_WIDTH = "width"; - static final String TAG_HEIGHT = "height"; - static final String TAG_DEPTH = "depth"; - static final String TAG_URL = "url"; - - /* Required. Icon's MIME type. */ - String mimetype; - /* Required. Horizontal dimension of icon in pixels. */ - String width; - /* Required. Vertical dimension of icon in pixels. */ - String height; - /* Required. Number of color bits per pixel. */ - String depth; - /* Required. Pointer to icon image. */ - String url; - } -} diff --git a/src/com/connectsdk/core/upnp/parser/Parser.java b/src/com/connectsdk/core/upnp/parser/Parser.java deleted file mode 100644 index a389aef4..00000000 --- a/src/com/connectsdk/core/upnp/parser/Parser.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Parser - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Copyright (c) 2011 stonker.lee@gmail.com https://code.google.com/p/android-dlna/ - * - * 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.core.upnp.parser; - -import java.io.IOException; - -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; - -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; - -public class Parser { - private static Parser instance = null; - - public static Parser getInstance() { - if (instance == null) { - instance = new Parser(); - } - - return instance; - } - - private SAXParser parser; - - private Parser() { - SAXParserFactory factory = SAXParserFactory.newInstance(); - try { - parser = factory.newSAXParser(); - } catch (ParserConfigurationException e) { - e.printStackTrace(); - } catch (SAXException e) { - e.printStackTrace(); - } - } - - public void parse(String url, DefaultHandler handler) { - try { - parser.parse(url, handler); - } catch (SAXException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } -} diff --git a/src/com/connectsdk/core/upnp/service/Action.java b/src/com/connectsdk/core/upnp/service/Action.java deleted file mode 100644 index 0aa6f7e6..00000000 --- a/src/com/connectsdk/core/upnp/service/Action.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Action - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Copyright (c) 2011 stonker.lee@gmail.com https://code.google.com/p/android-dlna/ - * - * 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.core.upnp.service; - -import java.util.List; - - -public class Action { - /* Required. Name of action. */ - String mName; - - /* Required. */ - List mArgumentList; - - public Action(String name) { - this.mName = name; - } -} \ No newline at end of file diff --git a/src/com/connectsdk/core/upnp/service/Argument.java b/src/com/connectsdk/core/upnp/service/Argument.java deleted file mode 100644 index 4d7074b7..00000000 --- a/src/com/connectsdk/core/upnp/service/Argument.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Argument - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Copyright (c) 2011 stonker.lee@gmail.com https://code.google.com/p/android-dlna/ - * - * 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.core.upnp.service; - -public class Argument { - public static final String TAG = "argument"; - public static final String TAG_NAME = "name"; - public static final String TAG_DIRECTION = "direction"; - public static final String TAG_RETVAL = "retval"; - public static final String TAG_RELATED_STATE_VARIABLE = "relatedStateVariable"; - - /* Required. Name of formal parameter. */ - String mName; - /* Required. Defines whether argument is an input or output paramter. */ - String mDirection; - /* Optional. Identifies at most one output argument as the return value. */ - String mRetval; - /* Required. Must be the same of a state variable. */ - String mRelatedStateVariable; -} \ No newline at end of file diff --git a/src/com/connectsdk/core/upnp/service/Service.java b/src/com/connectsdk/core/upnp/service/Service.java deleted file mode 100644 index 9189a350..00000000 --- a/src/com/connectsdk/core/upnp/service/Service.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Service - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Copyright (c) 2011 stonker.lee@gmail.com https://code.google.com/p/android-dlna/ - * - * 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.core.upnp.service; - -import java.util.List; - -//import com.connectsdk.core.upnp.parser.Parser; - -public class Service { - public static final String TAG = "service"; - public static final String TAG_SERVICE_TYPE = "serviceType"; - public static final String TAG_SERVICE_ID = "serviceId"; - public static final String TAG_SCPD_URL = "SCPDURL"; - public static final String TAG_CONTROL_URL = "controlURL"; - public static final String TAG_EVENTSUB_URL = "eventSubURL"; - - public String baseURL; - /* Required. UPnP service type. */ - public String serviceType; - /* Required. Service identifier. */ - public String serviceId; - /* Required. Relative URL for service description. */ - public String SCPDURL; - /* Required. Relative URL for control. */ - public String controlURL; - /* Relative. Relative URL for eventing. */ - public String eventSubURL; - - public List actionList; - public List serviceStateTable; - - /* - * We don't get SCPD, control and eventSub descriptions at service creation. - * So call this method first before you use the service. - */ - public void init() { -// Parser parser = Parser.getInstance(); - } -} \ No newline at end of file diff --git a/src/com/connectsdk/core/upnp/service/StateVariable.java b/src/com/connectsdk/core/upnp/service/StateVariable.java deleted file mode 100644 index 50a6db41..00000000 --- a/src/com/connectsdk/core/upnp/service/StateVariable.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * StateVariable - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Copyright (c) 2011 stonker.lee@gmail.com https://code.google.com/p/android-dlna/ - * - * 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.core.upnp.service; - -public class StateVariable { - public static final String TAG = "stateVariable"; - public static final String TAG_NAME = "name"; - public static final String TAG_DATA_TYPE = "dataType"; - - /* Optional. Defines whether event messages will be generated when the value - * of this state variable changes. Defaut value is "yes". - */ - String mSendEvents = "yes"; - - /* Optional. Defines whether event messages will be delivered using - * multicast eventing. Default value is "no". - */ - String mMulticast = "no"; - - /* Required. Name of state variable. */ - String mName; - - /* Required. Same as data types defined by XML Schema. */ - String mDataType; - -} \ No newline at end of file diff --git a/src/com/connectsdk/core/upnp/ssdp/SSDP.java b/src/com/connectsdk/core/upnp/ssdp/SSDP.java deleted file mode 100644 index 32e5f6c6..00000000 --- a/src/com/connectsdk/core/upnp/ssdp/SSDP.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * SSDP - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Copyright (c) 2011 stonker.lee@gmail.com https://code.google.com/p/android-dlna/ - * - * 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.core.upnp.ssdp; - -import java.net.DatagramPacket; -import java.nio.charset.Charset; -import java.util.HashMap; -import java.util.Map; - -public class SSDP { - /* New line definition */ - public static final String NEWLINE = "\r\n"; - - public static final String ADDRESS = "239.255.255.250"; - public static final int PORT = 1900; - public static final int SOURCE_PORT = 1901; - - public static final String ST = "ST"; - public static final String LOCATION = "LOCATION"; - public static final String NT = "NT"; - public static final String NTS = "NTS"; - public static final String URN = "URN"; - public static final String USN = "USN"; - public static final String APPLICATION_URL = "Application-URL"; - - /* Definitions of start line */ - public static final String SL_NOTIFY = "NOTIFY * HTTP/1.1"; - public static final String SL_MSEARCH = "M-SEARCH * HTTP/1.1"; - public static final String SL_OK = "HTTP/1.1 200 OK"; - - /* Definitions of search targets */ -// public static final String ST_ALL = ST + ": ssdp:all"; - public static final String ST_SSAP = ST + ": urn:lge-com:service:webos-second-screen:1"; - public static final String ST_DIAL = ST + ": urn:dial-multiscreen-org:service:dial:1"; - public static final String DEVICE_MEDIA_SERVER_1 = "urn:schemas-upnp-org:device:MediaServer:1"; - -// public static final String SERVICE_CONTENT_DIRECTORY_1 = "urn:schemas-upnp-org:service:ContentDirectory:1"; -// public static final String SERVICE_CONNECTION_MANAGER_1 = "urn:schemas-upnp-org:service:ConnectionManager:1"; -// public static final String SERVICE_AV_TRANSPORT_1 = "urn:schemas-upnp-org:service:AVTransport:1"; -// -// public static final String ST_ContentDirectory = ST + ":" + UPNP.SERVICE_CONTENT_DIRECTORY_1; - - /* Definitions of notification sub type */ - public static final String NTS_ALIVE = "ssdp:alive"; - public static final String NTS_BYEBYE = "ssdp:byebye"; - public static final String NTS_UPDATE = "ssdp:update"; - - public static ParsedDatagram convertDatagram(DatagramPacket dp) { - return new ParsedDatagram(dp); - } - - public static class ParsedDatagram { - public DatagramPacket dp; - public Map data = new HashMap(); - public String type; - - static Charset ASCII_CHARSET = Charset.forName("US-ASCII"); - - public ParsedDatagram(DatagramPacket packet) { - this.dp = packet; - - String text = new String(dp.getData(), ASCII_CHARSET); - - int pos = 0; - int eolPos = text.indexOf("\r\n"); - - // Get first line - type = text.substring(0, eolPos); - pos = eolPos + 2; - - while (pos < text.length()) { - eolPos = text.indexOf("\r\n", pos); - - if (eolPos < 0) { - break; - } - - String line = text.substring(pos, eolPos); - pos = eolPos + 2; - - int index = line.indexOf(':'); - if (index == -1) { - continue; - } - - String key = asciiUpper(line.substring(0, index)); - String value = line.substring(index + 1).trim(); - - data.put(key, value); - } - } - - // Fast toUpperCase for ASCII strings - private static String asciiUpper(String text) { - char [] chars = text.toCharArray(); - - for (int i = 0; i < chars.length; i++) { - char c = chars[i]; - chars[i] = (c >= 97 && c <= 122) ? (char) (c - 32) : c; - } - - return new String(chars); - } - } -} \ No newline at end of file diff --git a/src/com/connectsdk/core/upnp/ssdp/SSDPSearchMsg.java b/src/com/connectsdk/core/upnp/ssdp/SSDPSearchMsg.java deleted file mode 100644 index da82ddc8..00000000 --- a/src/com/connectsdk/core/upnp/ssdp/SSDPSearchMsg.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SSDPSearchMsg - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Copyright (c) 2011 stonker.lee@gmail.com https://code.google.com/p/android-dlna/ - * - * 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.core.upnp.ssdp; - -public class SSDPSearchMsg { - static final String HOST = "HOST: " + SSDP.ADDRESS + ":" + SSDP.PORT; - static final String MAN = "MAN: \"ssdp:discover\""; - static final String UDAP = "USER-AGENT: UDAP/2.0"; - - int mMX = 5; /* seconds to delay response */ - String mST; /* Search target */ - - public SSDPSearchMsg(String ST) { - mST = ST; - } - - public int getmMX() { - return mMX; - } - - public void setmMX(int mMX) { - this.mMX = mMX; - } - - public String getmST() { - return mST; - } - - public void setmST(String mST) { - this.mST = mST; - } - - @Override - public String toString() { - StringBuilder content = new StringBuilder(); - - content.append(SSDP.SL_MSEARCH).append(SSDP.NEWLINE); - content.append(HOST).append(SSDP.NEWLINE); - content.append(MAN).append(SSDP.NEWLINE); - content.append(SSDP.ST + ": " + mST).append(SSDP.NEWLINE); - content.append("MX: " + mMX).append(SSDP.NEWLINE); - if ( mST.contains("udap") ) { - content.append(UDAP).append(SSDP.NEWLINE); - } - content.append(SSDP.NEWLINE); - - return content.toString(); - } -} \ No newline at end of file diff --git a/src/com/connectsdk/core/upnp/ssdp/SSDPSocket.java b/src/com/connectsdk/core/upnp/ssdp/SSDPSocket.java deleted file mode 100644 index 7c94157f..00000000 --- a/src/com/connectsdk/core/upnp/ssdp/SSDPSocket.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * SSDPSocket - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Copyright (c) 2011 stonker.lee@gmail.com https://code.google.com/p/android-dlna/ - * - * 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.core.upnp.ssdp; - -import java.io.IOException; -import java.net.DatagramPacket; -import java.net.DatagramSocket; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.MulticastSocket; -import java.net.NetworkInterface; -import java.net.SocketAddress; -import java.net.SocketException; - -public class SSDPSocket { - SocketAddress mSSDPMulticastGroup; - - DatagramSocket wildSocket; - MulticastSocket mLocalSocket; - - NetworkInterface mNetIf; - InetAddress localInAddress; - - int timeout = 0; - - public SSDPSocket(InetAddress source) throws IOException { - localInAddress = source; - - mSSDPMulticastGroup = new InetSocketAddress(SSDP.ADDRESS, SSDP.PORT); - - mLocalSocket = new MulticastSocket(SSDP.PORT); - - mNetIf = NetworkInterface.getByInetAddress(localInAddress); - mLocalSocket.joinGroup(mSSDPMulticastGroup, mNetIf); - - wildSocket = new DatagramSocket(null); - wildSocket.setReuseAddress(true); - wildSocket.bind(new InetSocketAddress(localInAddress, SSDP.SOURCE_PORT)); - } - - /** Used to send SSDP packet */ - public void send(String data) throws IOException { - DatagramPacket dp = new DatagramPacket(data.getBytes(), data.length(), mSSDPMulticastGroup); - - wildSocket.send(dp); - } - - - /** Used to receive SSDP Response packet */ - public DatagramPacket responseReceive() throws IOException { - byte[] buf = new byte[1024]; - DatagramPacket dp = new DatagramPacket(buf, buf.length); - - wildSocket.receive(dp); - - return dp; - } - - /** Used to receive SSDP Notify packet */ - public DatagramPacket notifyReceive() throws IOException { - byte[] buf = new byte[1024]; - DatagramPacket dp = new DatagramPacket(buf, buf.length); - - mLocalSocket.receive(dp); - - return dp; - } - -// /** Starts the socket */ -// public void start() { -// -// } - - public boolean isConnected() { - return wildSocket != null && mLocalSocket != null && wildSocket.isConnected() && mLocalSocket.isConnected(); - } - - /** Close the socket */ - public void close() { - if (mLocalSocket != null) { - try { - mLocalSocket.leaveGroup(mSSDPMulticastGroup, mNetIf); - } catch (IOException e) { - e.printStackTrace(); - } - mLocalSocket.close(); - } - - if (wildSocket != null) { - wildSocket.disconnect(); - wildSocket.close(); - } - } - - public void setTimeout(int timeout) throws SocketException { - this.timeout = timeout; - wildSocket.setSoTimeout(this.timeout); - } -} \ No newline at end of file diff --git a/src/com/connectsdk/device/ConnectableDevice.java b/src/com/connectsdk/device/ConnectableDevice.java deleted file mode 100644 index 74358797..00000000 --- a/src/com/connectsdk/device/ConnectableDevice.java +++ /dev/null @@ -1,1019 +0,0 @@ -/* - * ConnectableDevice - * 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.device; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; - -import org.json.JSONException; -import org.json.JSONObject; - -import com.connectsdk.core.Util; -import com.connectsdk.discovery.DiscoveryManager; -import com.connectsdk.service.DeviceService; -import com.connectsdk.service.DeviceService.DeviceServiceListener; -import com.connectsdk.service.DeviceService.PairingType; -import com.connectsdk.service.capability.ExternalInputControl; -import com.connectsdk.service.capability.KeyControl; -import com.connectsdk.service.capability.Launcher; -import com.connectsdk.service.capability.MediaControl; -import com.connectsdk.service.capability.MediaPlayer; -import com.connectsdk.service.capability.MouseControl; -import com.connectsdk.service.capability.PowerControl; -import com.connectsdk.service.capability.TVControl; -import com.connectsdk.service.capability.TextInputControl; -import com.connectsdk.service.capability.ToastControl; -import com.connectsdk.service.capability.VolumeControl; -import com.connectsdk.service.capability.WebAppLauncher; -import com.connectsdk.service.command.ServiceCommandError; -import com.connectsdk.service.config.ServiceDescription; - -/** - * ###Overview - * ConnectableDevice serves as a normalization layer between your app and each of the device's services. It consolidates a lot of key data about the physical device and provides access to underlying functionality. - * - * ###In Depth - * ConnectableDevice consolidates some key information about the physical device, including model name, friendly name, ip address, connected DeviceService names, etc. In some cases, it is not possible to accurately select which DeviceService has the best friendly name, model name, etc. In these cases, the values of these properties are dependent upon the order of DeviceService discovery. - * - * To be informed of any ready/pairing/disconnect messages from each of the DeviceService, you must set a listener. - * - * ConnectableDevice exposes capabilities that exist in the underlying DeviceServices such as TV Control, Media Player, Media Control, Volume Control, etc. These capabilities, when accessed through the ConnectableDevice, will be automatically chosen from the most suitable DeviceService by using that DeviceService's CapabilityPriorityLevel. - */ -public class ConnectableDevice implements DeviceServiceListener { - // @cond INTERNAL - public static final String KEY_ID = "id"; - public static final String KEY_LAST_IP = "lastKnownIPAddress"; - public static final String KEY_FRIENDLY = "friendlyName"; - public static final String KEY_MODEL_NAME = "modelName"; - public static final String KEY_MODEL_NUMBER = "modelNumber"; - public static final String KEY_LAST_SEEN = "lastSeenOnWifi"; - public static final String KEY_LAST_CONNECTED = "lastConnected"; - public static final String KEY_LAST_DETECTED = "lastDetection"; - public static final String KEY_SERVICES = "services"; - - private String ipAddress; - private String friendlyName; - private String modelName; - private String modelNumber; - - private String lastKnownIPAddress; - private String lastSeenOnWifi; - private long lastConnected; - private long lastDetection; - - private String id; - - private ServiceDescription serviceDescription; - - CopyOnWriteArrayList listeners = new CopyOnWriteArrayList(); - - Map services; - - public boolean featuresReady = false; - - public ConnectableDevice() { - services = new ConcurrentHashMap(); - } - - public ConnectableDevice(String ipAddress, String friendlyName, String modelName, String modelNumber) { - this(); - - this.ipAddress = ipAddress; - this.friendlyName = friendlyName; - this.modelName = modelName; - this.modelNumber = modelNumber; - } - - public ConnectableDevice(ServiceDescription description) { - this(); - - update(description); - } - - public ConnectableDevice(JSONObject json) { - this(); - - setId(json.optString(KEY_ID, null)); - setLastKnownIPAddress(json.optString(KEY_LAST_IP, null)); - setFriendlyName(json.optString(KEY_FRIENDLY, null)); - setModelName(json.optString(KEY_MODEL_NAME, null)); - setModelNumber(json.optString(KEY_MODEL_NUMBER, null)); - setLastSeenOnWifi(json.optString(KEY_LAST_SEEN, null)); - setLastConnected(json.optLong(KEY_LAST_CONNECTED, 0)); - setLastDetection(json.optLong(KEY_LAST_DETECTED, 0)); - } - - public static ConnectableDevice createFromConfigString(String ipAddress, String friendlyName, String modelName, String modelNumber) { - return new ConnectableDevice(ipAddress, friendlyName, modelName, modelNumber); - } - - public static ConnectableDevice createWithId(String id, String ipAddress, String friendlyName, String modelName, String modelNumber) { - ConnectableDevice mDevice = new ConnectableDevice(ipAddress, friendlyName, modelName, modelNumber); - mDevice.setId(id); - - return mDevice; - } - - public ServiceDescription getServiceDescription() { - return serviceDescription; - } - - public void setServiceDescription(ServiceDescription serviceDescription) { - this.serviceDescription = serviceDescription; - } - // @endcond - - /** - * Adds a DeviceService to the ConnectableDevice instance. Only one instance of each DeviceService type (webOS, Netcast, etc) may be attached to a single ConnectableDevice instance. If a device contains your service type already, your service will not be added. - * - * @param service DeviceService to be added - */ - public void addService(DeviceService service) { - final List added = getMismatchCapabilities(service.getCapabilities(), getCapabilities()); - - service.setListener(this); - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - for (ConnectableDeviceListener listener : listeners) - listener.onCapabilityUpdated(ConnectableDevice.this, added, new ArrayList()); - } - }); - - services.put(service.getServiceName(), service); - } - - /** - * Removes a DeviceService from the ConnectableDevice instance. - * - * @param service DeviceService to be removed - */ - public void removeService(DeviceService service) { - removeServiceWithId(service.getServiceName()); - } - - /** - * Removes a DeviceService from the ConnectableDevice instance. - * - * @param serviceId ID of the DeviceService to be removed (DLNA, webOS TV, etc) - */ - public void removeServiceWithId(String serviceId) { - DeviceService service = services.get(serviceId); - - if (service == null) - return; - - service.disconnect(); - - services.remove(serviceId); - - final List removed = getMismatchCapabilities(service.getCapabilities(), getCapabilities()); - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - for (ConnectableDeviceListener listener : listeners) - listener.onCapabilityUpdated(ConnectableDevice.this, new ArrayList(), removed); - } - }); - } - - private synchronized List getMismatchCapabilities(List capabilities, List allCapabilities) { - List list = new ArrayList(); - - for (String cap: capabilities) { - if ( !allCapabilities.contains(cap) ) { - list.add(cap); - } - } - - return list; - } - - /** Array of all currently discovered DeviceServices this ConnectableDevice has associated with it. */ - public Collection getServices() { - return services.values(); - } - - /** - * Obtains a service from the ConnectableDevice with the provided serviceName - * - * @param serviceName Service ID of the targeted DeviceService (webOS, Netcast, DLNA, etc) - * @return DeviceService with the specified serviceName or nil, if none exists - */ - public DeviceService getServiceByName(String serviceName) { - for (DeviceService service : getServices()) { - if (service.getServiceName().equals(serviceName)) { - return service; - } - } - - return null; - } - - /** - * Removes a DeviceService form the ConnectableDevice instance. serviceName is used as the identifier because only one instance of each DeviceService type may be attached to a single ConnectableDevice instance. - * - * @param serviceName Name of the DeviceService to be removed from the ConnectableDevice. - */ - public void removeServiceByName(String serviceName) { - removeService(getServiceByName(serviceName)); - } - - /** - * Returns a DeviceService from the ConnectableDevice instance. serviceUUID is used as the identifier because only one instance of each DeviceService type may be attached to a single ConnectableDevice instance. - * - * @param serviceUUID UUID of the DeviceService to be returned - */ - public DeviceService getServiceWithUUID(String serviceUUID) { - for (DeviceService service : getServices()) { - if (service.getServiceDescription().getUUID().equals(serviceUUID)) { - return service; - } - } - - return null; - } - - /** - * Adds the ConnectableDeviceListener to the list of listeners for this ConnectableDevice to receive certain events. - * - * @param listener ConnectableDeviceListener to listen to device events (connect, disconnect, ready, etc) - */ - public void addListener(ConnectableDeviceListener listener) { - if (listeners.contains(listener) == false) { - listeners.add(listener); - } - } - - /** - * Clears the array of listeners and adds the provided `listener` to the array. If `listener` is null, the array will be empty. - * - * @deprecated Since version 1.2.1, use addListener instead - * - * @param listener ConnectableDeviceListener to listen to device events (connect, disconnect, ready, etc) - */ - public void setListener(ConnectableDeviceListener listener) { - listeners = new CopyOnWriteArrayList(); - - if (listener != null) - listeners.add(listener); - } - - /** - * Removes a previously added ConenctableDeviceListener from the list of listeners for this ConnectableDevice. - * - * @param listener ConnectableDeviceListener to be removed - */ - public void removeListener(ConnectableDeviceListener listener) { - listeners.remove(listener); - } - - public List getListeners() { - return listeners; - } - - /** - * Enumerates through all DeviceServices and attempts to connect to each of them. When all of a ConnectableDevice's DeviceServices are ready to receive commands, the ConnectableDevice will send a onDeviceReady message to its listener. - * - * It is always necessary to call connect on a ConnectableDevice, even if it contains no connectable DeviceServices. - */ - public void connect() { - for (DeviceService service : services.values()) { - if (!service.isConnected()) { - service.connect(); - } - } - } - - /** - * Enumerates through all DeviceServices and attempts to disconnect from each of them. - */ - public void disconnect() { - for (DeviceService service: services.values()) { - service.disconnect(); - } - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - for (ConnectableDeviceListener listener : listeners) - listener.onDeviceDisconnected(ConnectableDevice.this); - } - }); - } - - // @cond INTERNAL - public boolean isConnected() { - int connectedCount = 0; - - Iterator iterator = services.values().iterator(); - - while (iterator.hasNext()) { - DeviceService service = iterator.next(); - - if (!service.isConnectable()) { - connectedCount++; - } else { - if (service.isConnected()) - connectedCount++; - } - } - - return connectedCount >= services.size(); - } - // @endcond - - /** - * Whether the device has any DeviceServices that require an active connection (websocket, HTTP registration, etc) - */ - public boolean isConnectable() { - for (DeviceService service: services.values()) { - if ( service.isConnectable() ) - return true; - } - - return false; - } - - /** - * Sends a pairing key to all discovered device services. - * - * @param pairingKey Pairing key to send to services. - */ - public void sendPairingKey(String pairingKey) { - for (DeviceService service: services.values()) { - service.sendPairingKey(pairingKey); - } - } - - /** Explicitly cancels pairing on all services that require pairing. In some services, this will hide a prompt that is displaying on the device. */ - public void cancelPairing() { - for (DeviceService service: services.values()) { - service.cancelPairing(); - } - } - - /** A combined list of all capabilities that are supported among the detected DeviceServices. */ - public synchronized List getCapabilities() { - List caps = new ArrayList(); - - for (DeviceService service: services.values()) { - for (String capability: service.getCapabilities()) { - if ( !caps.contains(capability) ) { - caps.add(capability); - } - } - } - - return caps; - } - - /** - * Test to see if the capabilities array contains a given capability. See the individual Capability classes for acceptable capability values. - * - * It is possible to append a wildcard search term `.Any` to the end of the search term. This method will return true for capabilities that match the term up to the wildcard. - * - * Example: `Launcher.App.Any` - * - * @param capability Capability to test against - */ - public boolean hasCapability(String capability) { - boolean hasCap = false; - - for (DeviceService service: services.values()) { - if ( service.hasCapability(capability) ) { - hasCap = true; - break; - } - } - - return hasCap; - } - - /** - * Test to see if the capabilities array contains at least one capability in a given set of capabilities. See the individual Capability classes for acceptable capability values. - * - * See hasCapability: for a description of the wildcard feature provided by this method. - * - * @param capabilities Array of capabilities to test against - */ - public boolean hasAnyCapability(String... capabilities) { - for (DeviceService service : services.values()) { - if (service.hasAnyCapability(capabilities)) - return true; - } - - return false; - } - - /** - * Test to see if the capabilities array contains a given set of capabilities. See the individual Capability classes for acceptable capability values. - * - * See hasCapability: for a description of the wildcard feature provided by this method. - * - * @param capabilities Array of capabilities to test against - */ - public synchronized boolean hasCapabilities(List capabilities) { - String[] arr = new String[capabilities.size()]; - capabilities.toArray(arr); - return hasCapabilities(arr); - } - - /** - * Test to see if the capabilities array contains a given set of capabilities. See the individual Capability classes for acceptable capability values. - * - * See hasCapability: for a description of the wildcard feature provided by this method. - * - * @param capabilities Array of capabilities to test against - */ - public synchronized boolean hasCapabilities(String... capabilites) { - boolean hasCaps = true; - - for (String capability : capabilites) { - if (!hasCapability(capability)) { - hasCaps = false; - break; - } - } - - return hasCaps; - } - - /** Accessor for highest priority Launcher object */ - public Launcher getLauncher() { - Launcher foundLauncher = null; - - for (DeviceService service: services.values()) { - if ( service.getAPI(Launcher.class) == null ) - continue; - - Launcher launcher = service.getAPI(Launcher.class); - - if ( foundLauncher == null ) { - foundLauncher = launcher; - } - else { - if ( launcher.getLauncherCapabilityLevel().getValue() > foundLauncher.getLauncherCapabilityLevel().getValue() ) { - foundLauncher = launcher; - } - } - } - - return foundLauncher; - } - - /** Accessor for highest priority MediaPlayer object */ - public MediaPlayer getMediaPlayer() { - MediaPlayer foundMediaPlayer = null; - - for (DeviceService service: services.values()) { - if ( service.getAPI(MediaPlayer.class) == null ) - continue; - - MediaPlayer mediaPlayer = service.getAPI(MediaPlayer.class); - - if ( foundMediaPlayer == null ) { - foundMediaPlayer = mediaPlayer; - } - else { - if ( mediaPlayer.getMediaPlayerCapabilityLevel().getValue() > foundMediaPlayer.getMediaPlayerCapabilityLevel().getValue() ) { - foundMediaPlayer = mediaPlayer; - } - } - } - - return foundMediaPlayer; - } - - /** Accessor for highest priority MediaControl object */ - public MediaControl getMediaControl() { - MediaControl foundMediaControl = null; - - for (DeviceService service: services.values()) { - if ( service.getAPI(MediaControl.class) == null ) - continue; - - MediaControl mediaControl = service.getAPI(MediaControl.class); - - if ( foundMediaControl == null ) { - foundMediaControl = mediaControl; - } - else { - if ( mediaControl.getMediaControlCapabilityLevel().getValue() > foundMediaControl.getMediaControlCapabilityLevel().getValue() ) { - foundMediaControl = mediaControl; - } - } - } - - return foundMediaControl; - } - - /** Accessor for highest priority VolumeControl object */ - public VolumeControl getVolumeControl() { - VolumeControl foundVolumeControl = null; - - for (DeviceService service: services.values()) { - if ( service.getAPI(VolumeControl.class) == null ) - continue; - - VolumeControl volumeControl = service.getAPI(VolumeControl.class); - - if ( foundVolumeControl == null ) { - foundVolumeControl = volumeControl; - } - else { - if ( volumeControl.getVolumeControlCapabilityLevel().getValue() > foundVolumeControl.getVolumeControlCapabilityLevel().getValue() ) { - foundVolumeControl = volumeControl; - } - } - } - - return foundVolumeControl; - } - - /** Accessor for highest priority WebAppLauncher object */ - public WebAppLauncher getWebAppLauncher() { - WebAppLauncher foundWebAppLauncher = null; - - for (DeviceService service: services.values()) { - if ( service.getAPI(WebAppLauncher.class) == null ) - continue; - - WebAppLauncher webAppLauncher = service.getAPI(WebAppLauncher.class); - - if ( foundWebAppLauncher == null ) { - foundWebAppLauncher = webAppLauncher; - } - else { - if ( webAppLauncher.getWebAppLauncherCapabilityLevel().getValue() > foundWebAppLauncher.getWebAppLauncherCapabilityLevel().getValue() ) { - foundWebAppLauncher = webAppLauncher; - } - } - } - - return foundWebAppLauncher; - } - - /** Accessor for highest priority TVControl object */ - public TVControl getTVControl() { - TVControl foundTVControl = null; - - for (DeviceService service: services.values()) { - if ( service.getAPI(TVControl.class) == null ) - continue; - - TVControl tvControl = service.getAPI(TVControl.class); - - if ( foundTVControl == null ) { - foundTVControl = tvControl; - } - else { - if ( tvControl.getTVControlCapabilityLevel().getValue() > foundTVControl.getTVControlCapabilityLevel().getValue() ) { - foundTVControl = tvControl; - } - } - } - - return foundTVControl; - } - - /** Accessor for highest priority ToastControl object */ - public ToastControl getToastControl() { - ToastControl foundToastControl = null; - - for (DeviceService service: services.values()) { - if ( service.getAPI(ToastControl.class) == null ) - continue; - - ToastControl toastControl = service.getAPI(ToastControl.class); - - if ( foundToastControl == null ) { - foundToastControl = toastControl; - } - else { - if ( toastControl.getToastControlCapabilityLevel().getValue() > foundToastControl.getToastControlCapabilityLevel().getValue() ) { - foundToastControl = toastControl; - } - } - } - - return foundToastControl; - } - - /** Accessor for highest priority TextInputControl object */ - public TextInputControl getTextInputControl() { - TextInputControl foundTextInputControl = null; - - for (DeviceService service: services.values()) { - if ( service.getAPI(TextInputControl.class) == null ) - continue; - - TextInputControl textInputControl = service.getAPI(TextInputControl.class); - - if ( foundTextInputControl == null ) { - foundTextInputControl = textInputControl; - } - else { - if ( textInputControl.getTextInputControlCapabilityLevel().getValue() > foundTextInputControl.getTextInputControlCapabilityLevel().getValue() ) { - foundTextInputControl = textInputControl; - } - } - } - - return foundTextInputControl; - } - - /** Accessor for highest priority MouseControl object */ - public MouseControl getMouseControl() { - MouseControl foundMouseControl = null; - - for (DeviceService service: services.values()) { - if ( service.getAPI(MouseControl.class) == null ) - continue; - - MouseControl mouseControl = service.getAPI(MouseControl.class); - - if ( foundMouseControl == null ) { - foundMouseControl = mouseControl; - } - else { - if ( mouseControl.getMouseControlCapabilityLevel().getValue() > foundMouseControl.getMouseControlCapabilityLevel().getValue() ) { - foundMouseControl = mouseControl; - } - } - } - - return foundMouseControl; - } - - /** Accessor for highest priority ExternalInputControl object */ - public ExternalInputControl getExternalInputControl() { - ExternalInputControl foundExternalInputControl = null; - - for (DeviceService service: services.values()) { - if ( service.getAPI(ExternalInputControl.class) == null ) - continue; - - ExternalInputControl externalInputControl = service.getAPI(ExternalInputControl.class); - - if ( foundExternalInputControl == null ) { - foundExternalInputControl = externalInputControl; - } - else { - if ( externalInputControl.getExternalInputControlPriorityLevel().getValue() > foundExternalInputControl.getExternalInputControlPriorityLevel().getValue() ) { - foundExternalInputControl = externalInputControl; - } - } - } - - return foundExternalInputControl; - } - - /** Accessor for highest priority PowerControl object */ - public PowerControl getPowerControl() { - PowerControl foundPowerControl = null; - - for (DeviceService service: services.values()) { - if ( service.getAPI(PowerControl.class) == null ) - continue; - - PowerControl powerControl = service.getAPI(PowerControl.class); - - if ( foundPowerControl == null ) { - foundPowerControl = powerControl; - } - else { - if ( powerControl.getPowerControlCapabilityLevel().getValue() > foundPowerControl.getPowerControlCapabilityLevel().getValue() ) { - foundPowerControl = powerControl; - } - } - } - - return foundPowerControl; - } - - /** Accessor for highest priority KeyControl object */ - public KeyControl getKeyControl() { - KeyControl foundKeyControl = null; - - for (DeviceService service: services.values()) { - if ( service.getAPI(KeyControl.class) == null ) - continue; - - KeyControl keyControl = service.getAPI(KeyControl.class); - - if ( foundKeyControl == null ) { - foundKeyControl = keyControl; - } - else { - if ( keyControl.getKeyControlCapabilityLevel().getValue() > foundKeyControl.getKeyControlCapabilityLevel().getValue() ) { - foundKeyControl = keyControl; - } - } - } - - return foundKeyControl; - } - - /** - * Sets the IP address of the ConnectableDevice. - * - * @param ipAddress IP address of the ConnectableDevice - */ - public void setIpAddress(String ipAddress) { - this.ipAddress = ipAddress; - } - - /** Gets the Current IP address of the ConnectableDevice. */ - public String getIpAddress() { - return ipAddress; - } - - /** - * Sets an estimate of the ConnectableDevice's current friendly name. - * - * @param friendlyName Friendly name of the device - */ - public void setFriendlyName(String friendlyName) { - this.friendlyName = friendlyName; - } - - /** Gets an estimate of the ConnectableDevice's current friendly name. */ - public String getFriendlyName() { - return friendlyName; - } - - /** - * Sets the last IP address this ConnectableDevice was discovered at. - * - * @param lastKnownIPAddress Last known IP address of the device & it's services - */ - public void setLastKnownIPAddress(String lastKnownIPAddress) { - this.lastKnownIPAddress = lastKnownIPAddress; - } - - /** Gets the last IP address this ConnectableDevice was discovered at. */ - public String getLastKnownIPAddress() { - return lastKnownIPAddress; - } - - /** - * Sets the name of the last wireless network this ConnectableDevice was discovered on. - * - * @param lastSeenOnWifi Last Wi-Fi network this device & it's services were discovered on - */ - public void setLastSeenOnWifi(String lastSeenOnWifi) { - this.lastSeenOnWifi = lastSeenOnWifi; - } - - /** Gets the name of the last wireless network this ConnectableDevice was discovered on. */ - public String getLastSeenOnWifi() { - return lastSeenOnWifi; - } - - /** - * Sets the last time (in milli seconds from 1970) that this ConnectableDevice was connected to. - * - * @param lastConnected Last connected time - */ - public void setLastConnected(long lastConnected) { - this.lastConnected = lastConnected; - } - - /** Gets the last time (in milli seconds from 1970) that this ConnectableDevice was connected to. */ - public long getLastConnected() { - return lastConnected; - } - - /** - * Sets the last time (in milli seconds from 1970) that this ConnectableDevice was detected. - * - * @param lastDetection Last detected time - */ - public void setLastDetection(long lastDetection) { - this.lastDetection = lastDetection; - } - - /** Gets the last time (in milli seconds from 1970) that this ConnectableDevice was detected. */ - public long getLastDetection() { - return lastDetection; - } - - /** - * Sets an estimate of the ConnectableDevice's current model name. - * - * @param modelName Model name of the ConnectableDevice - */ - public void setModelName(String modelName) { - this.modelName = modelName; - } - - /** Gets an estimate of the ConnectableDevice's current model name. */ - public String getModelName() { - return modelName; - } - - /** - * Sets an estimate of the ConnectableDevice's current model number. - * - * @param modelNumber Model number of the ConnectableDevice - * */ - public void setModelNumber(String modelNumber) { - this.modelNumber = modelNumber; - } - - /** Gets an estimate of the ConnectableDevice's current model number. */ - public String getModelNumber() { - return modelNumber; - } - - /** - * Sets the universally unique id of this particular ConnectableDevice object. This is used internally in the SDK and should not be used. - * @param id New id for the ConnectableDevice - */ - public void setId(String id) { - this.id = id; - } - - /** - * Universally unique id of this particular ConnectableDevice object, persists between sessions in ConnectableDeviceStore for connected devices - */ - public String getId() { - if (this.id == null) - this.id = java.util.UUID.randomUUID().toString(); - - return this.id; - } - - // @cond INTERNAL - public String getConnectedServiceNames() { - int serviceCount = getServices().size(); - - if (serviceCount <= 0) - return null; - - String[] serviceNames = new String[serviceCount]; - int serviceIndex = 0; - - for (DeviceService service : getServices()) { - serviceNames[serviceIndex] = service.getServiceName(); - - serviceIndex++; - } - - // credit: http://stackoverflow.com/a/6623121/2715 - StringBuilder sb = new StringBuilder(); - - for (String serviceName : serviceNames) { - if (sb.length() > 0) - sb.append(", "); - - sb.append(serviceName); - } - - return sb.toString(); - //// - } - - public void update(ServiceDescription description) { - setIpAddress(description.getIpAddress()); - setFriendlyName(description.getFriendlyName()); - setModelName(description.getModelName()); - setModelNumber(description.getModelNumber()); - setLastConnected(description.getLastDetection()); - } - - public JSONObject toJSONObject() { - JSONObject deviceObject = new JSONObject(); - - try { - deviceObject.put(KEY_ID, getId()); - deviceObject.put(KEY_LAST_IP, getIpAddress()); - deviceObject.put(KEY_FRIENDLY, getFriendlyName()); - deviceObject.put(KEY_MODEL_NAME, getModelName()); - deviceObject.put(KEY_MODEL_NUMBER, getModelNumber()); - deviceObject.put(KEY_LAST_SEEN, getLastSeenOnWifi()); - deviceObject.put(KEY_LAST_CONNECTED, getLastConnected()); - deviceObject.put(KEY_LAST_DETECTED, getLastDetection()); - - JSONObject jsonServices = new JSONObject(); - for (DeviceService service: services.values()) { - JSONObject serviceObject = service.toJSONObject(); - - jsonServices.put(service.getServiceConfig().getServiceUUID(), serviceObject); - } - deviceObject.put(KEY_SERVICES, jsonServices); - } catch (JSONException e) { - e.printStackTrace(); - } - - return deviceObject; - } - - public String toString() { - return toJSONObject().toString(); - } - - @Override - public void onCapabilitiesUpdated(DeviceService service, List added, List removed) { - DiscoveryManager.getInstance().onCapabilityUpdated(this, added, removed); - } - - @Override public void onConnectionFailure(DeviceService service, Error error) { - } - - @Override public void onConnectionRequired(DeviceService service) { - } - - @Override - public void onConnectionSuccess(DeviceService service) { - // TODO: iOS is passing to a function for when each service is ready on a device. This is not implemented on Android. - - if (isConnected()) { - ConnectableDeviceStore deviceStore = DiscoveryManager.getInstance().getConnectableDeviceStore(); - if (deviceStore != null) { - deviceStore.addDevice(this); - } - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - for (ConnectableDeviceListener listener : listeners) - listener.onDeviceReady(ConnectableDevice.this); - } - }); - - setLastConnected(Util.getTime()); - } - } - - @Override - public void onDisconnect(DeviceService service, Error error) { - if (getConnectedServiceCount() == 0 || services.size() == 0) { - for (ConnectableDeviceListener listener : listeners) { - listener.onDeviceDisconnected(this); - } - } - } - - @Override - public void onPairingFailed(DeviceService service, Error error) { - for (ConnectableDeviceListener listener : listeners) - listener.onConnectionFailed(this, new ServiceCommandError(0, "Failed to pair with service " + service.getServiceName(), null)); - } - - @Override - public void onPairingRequired(DeviceService service, PairingType pairingType, Object pairingData) { - for (ConnectableDeviceListener listener : listeners) - listener.onPairingRequired(this, service, pairingType); - } - - @Override public void onPairingSuccess(DeviceService service) { - } - - private int getConnectedServiceCount() { - int count = 0; - - for (DeviceService service : services.values()) { - if (service.isConnectable()) { - if (service.isConnected()) - count++; - } else { - count++; - } - } - - return count; - } - - // @endcond -} diff --git a/src/com/connectsdk/device/ConnectableDeviceListener.java b/src/com/connectsdk/device/ConnectableDeviceListener.java deleted file mode 100644 index 7c49c5ba..00000000 --- a/src/com/connectsdk/device/ConnectableDeviceListener.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * ConnectableDeviceListener - * 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.device; - -import java.util.List; - -import com.connectsdk.service.DeviceService; -import com.connectsdk.service.DeviceService.PairingType; -import com.connectsdk.service.command.ServiceCommandError; - -/** - * ConnectableDeviceListener allows for a class to receive messages about ConnectableDevice connection, disconnect, and update events. - * - * It also serves as a proxy for message handling when connecting and pairing with each of a ConnectableDevice's DeviceServices. Each of the DeviceService proxy methods are optional and would only be useful in a few use cases. - * - providing your own UI for the pairing process. - * - interacting directly and exclusively with a single type of DeviceService - */ -public interface ConnectableDeviceListener { - - /** - * A ConnectableDevice sends out a ready message when all of its connectable DeviceServices have been connected and are ready to receive commands. - * - * @param device ConnectableDevice that is ready for commands. - */ - public void onDeviceReady(ConnectableDevice device); - - /** - * When all of a ConnectableDevice's DeviceServices have become disconnected, the disconnected message is sent. - * - * @param device ConnectableDevice that has been disconnected. - */ - public void onDeviceDisconnected(ConnectableDevice device); - - /** - * DeviceService listener proxy method. - * - * This method is called when a DeviceService tries to connect and finds out that it requires pairing information from the user. - * - * @param device ConnectableDevice containing the DeviceService - * @param service DeviceService that requires pairing - * @param pairingType DeviceServicePairingType that the DeviceService requires - */ - public void onPairingRequired(ConnectableDevice device, DeviceService service, PairingType pairingType); - - /** - * When a ConnectableDevice finds & loses DeviceServices, that ConnectableDevice will experience a change in its collective capabilities list. When such a change occurs, this message will be sent with arrays of capabilities that were added & removed. - * - * This message will allow you to decide when to stop/start interacting with a ConnectableDevice, based off of its supported capabilities. - * - * @param device ConnectableDevice that has experienced a change in capabilities - * @param added List of capabilities that are new to the ConnectableDevice - * @param removed List of capabilities that the ConnectableDevice has lost - */ - public void onCapabilityUpdated(ConnectableDevice device, List added, List removed); - - /** - * This method is called when the connection to the ConnectableDevice has failed. - * - * @param device ConnectableDevice that has failed to connect - * @param error ServiceCommandError with a description of the failure - */ - public void onConnectionFailed(ConnectableDevice device, ServiceCommandError error); -} diff --git a/src/com/connectsdk/device/ConnectableDeviceStore.java b/src/com/connectsdk/device/ConnectableDeviceStore.java deleted file mode 100644 index 123a8383..00000000 --- a/src/com/connectsdk/device/ConnectableDeviceStore.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * ConnectableDeviceStore - * 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.device; - -import org.json.JSONObject; - -import com.connectsdk.service.config.ServiceConfig; - -/** - * ConnectableDeviceStore is a interface which can be implemented to save key information about ConnectableDevices that have been connected to. Any class which implements this interface can be used as DiscoveryManager's deviceStore. - * - * A default implementation, DefaultConnectableDeviceStore, will be used by DiscoveryManager if no other ConnectableDeviceStore is provided to DiscoveryManager when startDiscovery is called. - * - * ###Privacy Considerations - * If you chose to implement ConnectableDeviceStore, it is important to keep your users' privacy in mind. - * - There should be UI elements in your app to - * + completely disable ConnectableDeviceStore - * + purge all data from ConnectableDeviceStore (removeAll) - * - Your ConnectableDeviceStore implementation should - * + avoid tracking too much data (indefinitely storing all discovered devices) - * + periodically remove ConnectableDevices from the ConnectableDeviceStore if they haven't been used/connected in X amount of time - */ -public interface ConnectableDeviceStore { - - /** - * Add a ConnectableDevice to the ConnectableDeviceStore. If the ConnectableDevice is already stored, it's record will be updated. - * - * @param device ConnectableDevice to add to the ConnectableDeviceStore - */ - public void addDevice(ConnectableDevice device); - - /** - * Removes a ConnectableDevice's record from the ConnectableDeviceStore. - * - * @param device ConnectableDevice to remove from the ConnectableDeviceStore - */ - public void removeDevice(ConnectableDevice device); - - /** - * Updates a ConnectableDevice's record in the ConnectableDeviceStore. - * - * @param device ConnectableDevice to update in the ConnectableDeviceStore - */ - public void updateDevice(ConnectableDevice device); - - /** - * A JSONObject of all ConnectableDevices in the ConnectableDeviceStore. To gt a strongly-typed ConnectableDevice object, use the `getDevice(String);` method. - */ - public JSONObject getStoredDevices(); - - /** - * Gets a ConnectableDevice object for a provided id. The id may be for the ConnectableDevice object or any of the DeviceServices. - * - * @param uuid Unique ID for a ConnectableDevice or any of its DeviceService objects - * - * @return ConnectableDevice object if a matching uuit was found, otherwise will return null - */ - public ConnectableDevice getDevice(String uuid); - - /** - * Gets a ServcieConfig object for a provided UUID. This is used by DiscoveryManager to retain crucial service information between sessions (pairing code, etc). - * - * @param uuid Unique ID for the service - * - * @return ServiceConfig object if matching UUID was found, otherwise will return null - */ - public ServiceConfig getServiceConfig(String uuid); - - /** - * Clears out the ConnectableDeviceStore, removing all records. - */ - public void removeAll(); -} diff --git a/src/com/connectsdk/device/DefaultConnectableDeviceStore.java b/src/com/connectsdk/device/DefaultConnectableDeviceStore.java deleted file mode 100644 index 41a97ec3..00000000 --- a/src/com/connectsdk/device/DefaultConnectableDeviceStore.java +++ /dev/null @@ -1,389 +0,0 @@ -/* - * DefaultConnectableDeviceStore - * 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.device; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.json.JSONException; -import org.json.JSONObject; - -import android.content.Context; -import android.content.pm.PackageManager.NameNotFoundException; -import android.os.Environment; - -import com.connectsdk.core.Util; -import com.connectsdk.service.DeviceService; -import com.connectsdk.service.config.ServiceConfig; - -public class DefaultConnectableDeviceStore implements ConnectableDeviceStore { - // @cond INTERNAL - - public static final String KEY_VERSION = "version"; - public static final String KEY_CREATED = "created"; - public static final String KEY_UPDATED = "updated"; - public static final String KEY_DEVICES = "devices"; - - static final int CURRENT_VERSION = 0; - - static final String DIRPATH = "/android/data/connect_sdk/"; - static final String FILENAME = "StoredDevices"; - - static final String IP_ADDRESS = "ipAddress"; - static final String FRIENDLY_NAME = "friendlyName"; - static final String MODEL_NAME = "modelName"; - static final String MODEL_NUMBER = "modelNumber"; - static final String SERVICES = "services"; - static final String DESCRIPTION = "description"; - static final String CONFIG = "config"; - - static final String FILTER = "filter"; - static final String UUID = "uuid"; - static final String PORT = "port"; - - static final String SERVICE_UUID = "serviceUUID"; - static final String CLIENT_KEY = "clientKey"; - static final String SERVER_CERTIFICATE = "serverCertificate"; - static final String PAIRING_KEY = "pairingKey"; - - static final String DEFAULT_SERVICE_WEBOSTV = "WebOSTVService"; - static final String DEFAULT_SERVICE_NETCASTTV = "NetcastTVService"; - - // @endcond - - /** Date (in seconds from 1970) that the ConnectableDeviceStore was created. */ - public long created; - /** Date (in seconds from 1970) that the ConnectableDeviceStore was last updated. */ - public long updated; - /** Current version of the ConnectableDeviceStore, may be necessary for migrations */ - public int version; - - /** - * Max length of time for a ConnectableDevice to remain in the ConnectableDeviceStore without being discovered. Default is 3 days, and modifications to this value will trigger a scan for old devices. - */ - public long maxStoreDuration = TimeUnit.DAYS.toSeconds(3); - - // @cond INTERNAL - private String fileFullPath; - - private JSONObject deviceStore; - private JSONObject storedDevices; - private Map activeDevices = new HashMap(); - - private boolean waitToWrite = false; - - public DefaultConnectableDeviceStore(Context context) { - String dirPath; - if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - dirPath = Environment.getExternalStorageDirectory().getAbsolutePath(); - } - else { - dirPath = Environment.MEDIA_UNMOUNTED; - } - fileFullPath = dirPath + DIRPATH + FILENAME; - - try { - fileFullPath = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).applicationInfo.dataDir + "/" + FILENAME; - } catch (NameNotFoundException e) { - e.printStackTrace(); - } - - load(); - } - // @endcond - - @Override - public void addDevice(ConnectableDevice device) { - if (device == null || device.getServices().size() == 0) - return; - - if (!activeDevices.containsKey(device.getId())) - activeDevices.put(device.getId(), device); - - JSONObject storedDevice = storedDevices.optJSONObject(device.getId()); - - if (storedDevice != null) { - updateDevice(device); - } else { - try { - storedDevices.put(device.getId(), device.toJSONObject()); - } catch (JSONException e) { - e.printStackTrace(); - } - - store(); - } - } - - @Override - public void removeDevice(ConnectableDevice device) { - if (device == null) - return; - - activeDevices.remove(device.getId()); - storedDevices.remove(device.getId()); - - store(); - } - - @Override - public void updateDevice(ConnectableDevice device) { - if (device == null || device.getServices().size() == 0) - return; - - JSONObject storedDevice = getStoredDevice(device.getId()); - - if (storedDevice == null) - return; - - try { - storedDevice.put(ConnectableDevice.KEY_LAST_IP, device.getLastKnownIPAddress()); - storedDevice.put(ConnectableDevice.KEY_LAST_SEEN, device.getLastSeenOnWifi()); - storedDevice.put(ConnectableDevice.KEY_LAST_CONNECTED, device.getLastConnected()); - storedDevice.put(ConnectableDevice.KEY_LAST_DETECTED, device.getLastDetection()); - - JSONObject services = storedDevice.optJSONObject(ConnectableDevice.KEY_SERVICES); - - if (services == null) - services = new JSONObject(); - - for (DeviceService service : device.getServices()) { - JSONObject serviceInfo = service.toJSONObject(); - - if (serviceInfo != null) - services.put(service.getServiceDescription().getUUID(), serviceInfo); - } - - storedDevice.put(ConnectableDevice.KEY_SERVICES, services); - - storedDevices.put(device.getId(), storedDevice); - activeDevices.put(device.getId(), device); - - store(); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void removeAll() { - activeDevices.clear(); - storedDevices = new JSONObject(); - - store(); - } - - @Override - public JSONObject getStoredDevices() { - return storedDevices; - } - - @Override - public ConnectableDevice getDevice(String uuid) { - if (uuid == null || uuid.length() == 0) - return null; - - ConnectableDevice foundDevice = getActiveDevice(uuid); - - if (foundDevice == null) { - JSONObject foundDeviceInfo = getStoredDevice(uuid); - - if (foundDeviceInfo != null) - foundDevice = new ConnectableDevice(foundDeviceInfo); - } - - return foundDevice; - } - - private ConnectableDevice getActiveDevice(String uuid) { - ConnectableDevice foundDevice = activeDevices.get(uuid); - - if (foundDevice == null) { - for (ConnectableDevice device : activeDevices.values()) { - for (DeviceService service : device.getServices()) { - if (uuid.equals(service.getServiceDescription().getUUID())) { - return foundDevice; - } - } - } - } - return foundDevice; - } - - private JSONObject getStoredDevice(String uuid) { - JSONObject foundDevice = storedDevices.optJSONObject(uuid); - - if (foundDevice == null) { - @SuppressWarnings("unchecked") - Iterator iter = storedDevices.keys(); - while (iter.hasNext()) { - String key = iter.next(); - JSONObject device = storedDevices.optJSONObject(key); - - JSONObject services = device.optJSONObject(ConnectableDevice.KEY_SERVICES); - - if (services != null && services.has(uuid)) - return device; - } - } - return foundDevice; - } - - @Override - public ServiceConfig getServiceConfig(String uuid) { - if (uuid == null || uuid.length() == 0) - return null; - - JSONObject device = getStoredDevice(uuid); - if (device != null) { - JSONObject services = device.optJSONObject(ConnectableDevice.KEY_SERVICES); - if (services != null) { - JSONObject service = services.optJSONObject(uuid); - if (service != null) { - JSONObject serviceConfigInfo = service.optJSONObject(DeviceService.KEY_CONFIG); - if (serviceConfigInfo != null) { - return ServiceConfig.getConfig(serviceConfigInfo); - } - } - } - } - - return null; - } - - // @cond INTERNAL - private void load() { - String line; - - BufferedReader in = null; - - File file = new File(fileFullPath); - - if (!file.exists()) { - version = CURRENT_VERSION; - - created = Util.getTime(); - updated = Util.getTime(); - - storedDevices = new JSONObject(); - } else { - boolean encounteredException = false; - - try { - in = new BufferedReader(new FileReader(file)); - - StringBuilder sb = new StringBuilder(); - - while ((line = in.readLine()) != null) { - sb.append(line); - } - - in.close(); - - JSONObject data = new JSONObject(sb.toString()); - storedDevices = data.optJSONObject(KEY_DEVICES); - if (storedDevices == null) - storedDevices = new JSONObject(); - - version = data.optInt(KEY_VERSION, CURRENT_VERSION); - created = data.optLong(KEY_CREATED, 0); - updated = data.optLong(KEY_UPDATED, 0); - } catch (IOException e) { - e.printStackTrace(); - - // it is likely that the device store has been corrupted - encounteredException = true; - } catch (JSONException e) { - e.printStackTrace(); - - // it is likely that the device store has been corrupted - encounteredException = true; - } - - if (encounteredException && storedDevices == null) { - file.delete(); - - version = CURRENT_VERSION; - - created = Util.getTime(); - updated = Util.getTime(); - - storedDevices = new JSONObject(); - } - } - } - - private void store() { - - updated = Util.getTime(); - - deviceStore = new JSONObject(); - try { - deviceStore.put(KEY_VERSION, version); - deviceStore.put(KEY_CREATED, created); - deviceStore.put(KEY_UPDATED, updated); - deviceStore.put(KEY_DEVICES, storedDevices); - } catch (JSONException e) { - e.printStackTrace(); - } - - if (!waitToWrite) - writeStoreToDisk(); - } - - private void writeStoreToDisk() { - final double lastUpdate = updated; - waitToWrite = true; - - Util.runInBackground(new Runnable() { - - @Override - public void run() { - FileWriter out; - try { - File output = new File(fileFullPath); - - if (!output.exists()) - output.getParentFile().mkdirs(); - - out = new FileWriter(output); - out.write(deviceStore.toString()); - out.close(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - waitToWrite = false; - } - - if (lastUpdate != updated) - writeStoreToDisk(); - } - }); - } - // @endcond -} diff --git a/src/com/connectsdk/device/DevicePicker.java b/src/com/connectsdk/device/DevicePicker.java deleted file mode 100644 index a3e920e0..00000000 --- a/src/com/connectsdk/device/DevicePicker.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * DevicePicker - * 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.device; - -import android.app.Activity; -import android.app.AlertDialog; -import android.view.View; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.ListView; -import android.widget.TextView; - -/** - * ###Overview - * The DevicePicker is provided by the DiscoveryManager as a simple way for you to present a list of available devices to your users. - * - * ###In Depth - * By calling the getPickerDialog you will get a reference to the AlertDialog that will be updated automatically updated as compatible devices are discovered. - */ -public class DevicePicker { - Activity activity; - ConnectableDevice device; - - /** - * Creates a new DevicePicker - * - * @param activity Activity that DevicePicker will appear in - */ - public DevicePicker(Activity activity) { - this.activity = activity; - } - - public ListView getListView() { - return new DevicePickerListView(activity); - } - - /** - * Sets a selected device. - * - * @param device Device that has been selected. - */ - public void pickDevice(ConnectableDevice device) { - this.device = device; - } - - /** - * Cancels pairing with the currently selected device. - */ - public void cancelPicker() { - if (device != null) { - device.cancelPairing(); - } - device = null; - } - - /** - * This method will return an AlertDialog that contains a ListView with an item for each discovered ConnectableDevice. - * - * @param message The title for the AlertDialog - * @param listener The listener for the ListView to get the item that was clicked on - */ - public AlertDialog getPickerDialog(String message, final OnItemClickListener listener) { - final DevicePickerListView view = new DevicePickerListView(activity); - - TextView title = (TextView) activity.getLayoutInflater().inflate(android.R.layout.simple_list_item_1, null); - title.setText(message); - - final AlertDialog pickerDialog = new AlertDialog.Builder(activity) - .setCustomTitle(title) - .setCancelable(true) - .setView(view) - .create(); - - view.setOnItemClickListener(new OnItemClickListener () { - @Override - public void onItemClick(AdapterView arg0, View arg1, int arg2, - long arg3) { - listener.onItemClick(arg0, arg1, arg2, arg3); - pickerDialog.dismiss(); - } - }); - - return pickerDialog; - } -} diff --git a/src/com/connectsdk/device/DevicePickerAdapter.java b/src/com/connectsdk/device/DevicePickerAdapter.java deleted file mode 100644 index bb83f692..00000000 --- a/src/com/connectsdk/device/DevicePickerAdapter.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * DevicePickerAdaper - * 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.device; - -import java.util.HashMap; - -import com.connectsdk.discovery.DiscoveryManager; - -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.graphics.Color; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.TextView; - - -public class DevicePickerAdapter extends ArrayAdapter { - int resource, textViewResourceId, subTextViewResourceId; - HashMap currentDevices = new HashMap(); - Context context; - - DevicePickerAdapter(Context context) { - this(context, android.R.layout.simple_list_item_2); - } - - DevicePickerAdapter(Context context, int resource) { - this(context, resource, android.R.id.text1, android.R.id.text2); - } - - DevicePickerAdapter(Context context, int resource, int textViewResourceId, int subTextViewResourceId) { - super(context, resource, textViewResourceId); - this.context = context; - this.resource = resource; - this.textViewResourceId = textViewResourceId; - this.subTextViewResourceId = subTextViewResourceId; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = convertView; - - if (convertView == null) { - view = View.inflate(getContext(), resource, null); - } - - ConnectableDevice device = this.getItem(position); - String text; - if ( device.getFriendlyName() != null ) { - text = device.getFriendlyName(); - } - else { - text = device.getModelName(); - } - - view.setBackgroundColor(Color.BLACK); - - TextView textView = (TextView) view.findViewById(textViewResourceId); - textView.setText(text); - textView.setTextColor(Color.WHITE); - - boolean isDebuggable = ( 0 != ( context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE ) ); - boolean hasNoFilters = DiscoveryManager.getInstance().getCapabilityFilters().size() == 0; - - String serviceNames = device.getConnectedServiceNames(); - boolean hasServiceNames = (serviceNames != null && serviceNames.length() > 0); - - boolean shouldShowServiceNames = hasServiceNames && (isDebuggable || hasNoFilters); - - TextView subTextView = (TextView) view.findViewById(subTextViewResourceId); - - if (shouldShowServiceNames) { - subTextView.setText(serviceNames); - subTextView.setTextColor(Color.WHITE); - } else { - subTextView.setText(null); - } - - return view; - } -} diff --git a/src/com/connectsdk/device/DevicePickerListView.java b/src/com/connectsdk/device/DevicePickerListView.java deleted file mode 100644 index 060c7d35..00000000 --- a/src/com/connectsdk/device/DevicePickerListView.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * DevicePickerListView - * 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.device; - -import android.content.Context; -import android.widget.ListView; - -import com.connectsdk.core.Util; -import com.connectsdk.discovery.DiscoveryManager; -import com.connectsdk.discovery.DiscoveryManagerListener; -import com.connectsdk.service.command.ServiceCommandError; - -public class DevicePickerListView extends ListView implements DiscoveryManagerListener { - DevicePickerAdapter pickerAdapter; - - public DevicePickerListView(Context context) { - super(context); - - pickerAdapter = new DevicePickerAdapter(context); - - setAdapter(pickerAdapter); - - DiscoveryManager.getInstance().addListener(this); - } - - @Override - public void onDiscoveryFailed(DiscoveryManager manager, ServiceCommandError error) { - Util.runOnUI(new Runnable () { - @Override - public void run() { - pickerAdapter.clear(); - } - }); - } - - @Override - public void onDeviceAdded(DiscoveryManager manager, final ConnectableDevice device) { - Util.runOnUI(new Runnable () { - @Override - public void run() { - int index = -1; - for ( int i = 0; i < pickerAdapter.getCount(); i++ ) { - ConnectableDevice d = pickerAdapter.getItem(i); - - String newDeviceName = device.getFriendlyName(); - String dName = d.getFriendlyName(); - - if ( newDeviceName == null ) { - newDeviceName = device.getModelName(); - } - - if ( dName == null ) { - dName = d.getModelName(); - } - - if ( d.getIpAddress().equals(device.getIpAddress()) ) { - pickerAdapter.remove(d); - pickerAdapter.insert(device, i); - return; - } - - if ( newDeviceName.compareToIgnoreCase(dName) < 0 ) { - index = i; - pickerAdapter.insert(device, index); - break; - } - } - - if ( index == -1 ) - pickerAdapter.add(device); - } - }); - } - - @Override - public void onDeviceUpdated(DiscoveryManager manager, final ConnectableDevice device) { - Util.runOnUI(new Runnable () { - @Override - public void run() { - pickerAdapter.notifyDataSetChanged(); - } - }); - } - - @Override - public void onDeviceRemoved(DiscoveryManager manager, final ConnectableDevice device) { - Util.runOnUI(new Runnable () { - @Override - public void run() { - pickerAdapter.remove(device); - } - }); - } -} diff --git a/src/com/connectsdk/device/DevicePickerListener.java b/src/com/connectsdk/device/DevicePickerListener.java deleted file mode 100644 index 0a43fc95..00000000 --- a/src/com/connectsdk/device/DevicePickerListener.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.connectsdk.device; - -public interface DevicePickerListener { - /** - * Called when the user selects a device. - * @param device - */ - public void onPickDevice(ConnectableDevice device); - - /** - * Called when the picker fails or was cancelled by the user. - * @param true if picker was canceled by user, false if due to error - */ - public void onPickDeviceFailed(boolean canceled); -} diff --git a/src/com/connectsdk/device/PairingDialog.java b/src/com/connectsdk/device/PairingDialog.java deleted file mode 100644 index 5037ebd2..00000000 --- a/src/com/connectsdk/device/PairingDialog.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * PairingDialog - * 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.device; - -import com.connectsdk.service.DeviceService; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.text.InputType; -import android.widget.EditText; -import android.widget.TextView; - - -public class PairingDialog { - Activity activity; - ConnectableDevice device; - - public PairingDialog(Activity activity, ConnectableDevice device) { - this.activity = activity; - this.device = device; - } - - public AlertDialog getSimplePairingDialog(int titleResId, int messageResId) { - return new AlertDialog.Builder(activity) - .setTitle(titleResId) - .setMessage(messageResId) - .setPositiveButton(android.R.string.cancel, null) - .create(); - } - - public AlertDialog getPairingDialog(int resId) { - return getPairingDialog(activity.getString(resId)); - } - - public AlertDialog getPairingDialog(String message) { - TextView title = (TextView) activity.getLayoutInflater().inflate(android.R.layout.simple_list_item_1, null); - title.setText(message); - - final EditText input = new EditText(activity); - input.setInputType(InputType.TYPE_CLASS_NUMBER); - - final AlertDialog pickerDialog = new AlertDialog.Builder(activity) - .setCustomTitle(title) - .setView(input) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - String value = input.getText().toString().trim(); - for (DeviceService service : device.getServices()) - service.sendPairingKey(value); - } - }) - .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - dialog.cancel(); - // pickerDialog.dismiss(); - } - }) - .create(); - - return pickerDialog; - } -} diff --git a/src/com/connectsdk/device/SimpleDevicePicker.java b/src/com/connectsdk/device/SimpleDevicePicker.java deleted file mode 100644 index 56108bd5..00000000 --- a/src/com/connectsdk/device/SimpleDevicePicker.java +++ /dev/null @@ -1,287 +0,0 @@ -/* - * SimpleDevicePicker - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Jason Lai 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.device; - -import java.util.List; - -import android.app.Activity; -import android.app.Dialog; -import android.content.res.Resources; -import android.util.Log; -import android.view.View; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.Toast; - -import com.connectsdk.core.Util; -import com.connectsdk.service.DeviceService; -import com.connectsdk.service.DeviceService.PairingType; -import com.connectsdk.service.command.ServiceCommandError; - -/** - * A device picker that automatically connects to the device - * and automatically displays pairing dialogs when needed. - * - * NOTE: Most methods MUST be called from the main ui thread. - */ -public class SimpleDevicePicker implements ConnectableDeviceListener { - protected Activity activity; - protected DevicePicker picker; - protected Dialog pickerDialog; - protected Dialog pairingDialog; - - // Device that we're in the process of connecting to - protected ConnectableDevice pendingDevice; - - // Connected, active device - protected ConnectableDevice activeDevice; - - protected int selectDeviceResId; - protected int simplePairingTitleResId; - protected int simplePairingPromptResId; - protected int pinPairingPromptResId; - protected int connectionFailedResId; - - SimpleDevicePickerListener listener; - - public SimpleDevicePicker(Activity activity) { - this.activity = activity; - this.picker = new DevicePicker(activity); - - loadStringIds(); - } - - /** - * Get the currently selected device - * @return current connected device - */ - public ConnectableDevice getCurrentDevice() { - return activeDevice; - } - - protected void loadStringIds() { - selectDeviceResId = getStringId("connect_sdk_picker_select_device"); - simplePairingTitleResId = getStringId("connect_sdk_pairing_simple_title_tv"); - simplePairingPromptResId = getStringId("connect_sdk_pairing_simple_prompt_tv"); - pinPairingPromptResId = getStringId("connect_sdk_pairing_pin_prompt_tv"); - connectionFailedResId = getStringId("connect_sdk_connection_failed"); - } - - protected int getStringId(String key) { - // First try to get resource from application - int id = this.activity.getResources().getIdentifier(key, "string", activity.getPackageName()); - - // Then try to get from Connect SDK library - if (id == 0) { - id = this.activity.getResources().getIdentifier(key, "string", "com.connectsdk"); - } - - if (id == 0) { - Log.w("ConnectSDK", "missing string resource for \"" + key + "\""); - - throw new Resources.NotFoundException(key); - } - - return id; - } - - public void setListener(SimpleDevicePickerListener listener) { - this.listener = listener; - } - - protected void cleanupPending() { - if (pendingDevice != null) { - pendingDevice.removeListener(this); - pendingDevice = null; - } - } - - protected void cleanupActive() { - if (pendingDevice != null) { - pendingDevice.removeListener(this); - pendingDevice = null; - } - } - - public void showPicker() { - cleanupPending(); // remove any currently pending device - hidePicker(); - - pickerDialog = picker.getPickerDialog(activity.getString(selectDeviceResId), new OnItemClickListener() { - @Override - public void onItemClick(AdapterView adapter, View view, int pos, - long id) { - ConnectableDevice device = (ConnectableDevice) adapter.getItemAtPosition(pos); - - selectDevice(device); - } - }); - - pickerDialog.show(); - } - - public void hidePicker() { - if (pickerDialog != null) { - pickerDialog.dismiss(); - pickerDialog = null; - } - } - - /** - * Connect to a device - * - * @param device - */ - public void selectDevice(ConnectableDevice device) { - if (device != null) { - pendingDevice = device; - pendingDevice.addListener(this); - - if (listener != null) { - // Give listener a chance to setup device before connecting - listener.onPrepareDevice(device); - } - - if (!device.isConnected()) { - device.connect(); - } else { - onDeviceReady(device); - } - } else { - cleanupPending(); - } - } - - protected Dialog createSimplePairingDialog() { - PairingDialog dialog = new PairingDialog(activity, pendingDevice); - return dialog.getSimplePairingDialog(simplePairingTitleResId, simplePairingPromptResId); - } - - protected Dialog createPinPairingDialog() { - PairingDialog dialog = new PairingDialog(activity, pendingDevice); - return dialog.getPairingDialog(pinPairingPromptResId); - } - - protected void showPairingDialog(PairingType pairingType) { - switch (pairingType) { - case FIRST_SCREEN: - pairingDialog = createSimplePairingDialog(); - break; - - case PIN_CODE: - pairingDialog = createPinPairingDialog(); - break; - - case NONE: - default: - break; - } - - if (pairingDialog != null) { - pairingDialog.show(); - } - } - - /** - * Hide the current pairing dialog and cancels the pairing attempt. - */ - public void hidePairingDialog() { - // cancel pairing - if (pairingDialog != null) { - pairingDialog.dismiss(); - pairingDialog = null; - } - } - - - @Override - public void onDeviceReady(final ConnectableDevice device) { - Util.runOnUI(new Runnable() { - @Override - public void run() { - hidePairingDialog(); - - if (device == pendingDevice) { - activeDevice = pendingDevice; - - if (listener != null) - listener.onPickDevice(pendingDevice); - } - } - }); - } - - @Override - public void onDeviceDisconnected(final ConnectableDevice device) { - if (device == pendingDevice) { - pickFailed(device); - } - - if (device == activeDevice) { - cleanupActive(); - } - } - - @Override - public void onCapabilityUpdated(ConnectableDevice device, List added, List removed) { - } - - @Override - public void onConnectionFailed(ConnectableDevice device, ServiceCommandError error) { - if (device == pendingDevice) { - pickFailed(device); - } - - if (device == activeDevice) { - cleanupActive(); - } - } - - @Override - public void onPairingRequired(ConnectableDevice device, DeviceService service, final PairingType pairingType) { - Log.d("SimpleDevicePicker", "pairing required for device " + device.getFriendlyName()); - - Util.runOnUI(new Runnable() { - @Override - public void run() { - showPairingDialog(pairingType); - } - }); - } - - protected void pickFailed(final ConnectableDevice device) { - Util.runOnUI(new Runnable() { - @Override - public void run() { - if (pendingDevice == device) { - // Device failed before successfully picking device - if (listener != null) { - listener.onPickDeviceFailed(false); - } - } - - cleanupPending(); - - Toast.makeText(activity, connectionFailedResId, Toast.LENGTH_SHORT).show(); - } - }); - } -} diff --git a/src/com/connectsdk/device/SimpleDevicePickerListener.java b/src/com/connectsdk/device/SimpleDevicePickerListener.java deleted file mode 100644 index b758116a..00000000 --- a/src/com/connectsdk/device/SimpleDevicePickerListener.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.connectsdk.device; - -public interface SimpleDevicePickerListener extends DevicePickerListener { - /** - * Called when the user selects a device. - * This callback can be used to prepare the device (request permissions, etc) - * just before attempting to connect. - * - * @param device - */ - public void onPrepareDevice(ConnectableDevice device); - - /** - * Called when device is ready to use (requested permissions approved). - * @param device - */ - public void onPickDevice(ConnectableDevice device); - - /** - * Called when the picker is canceled by the user or if pairing - * was unsuccessful. - * @param true if picker was canceled by user, false if due to error - */ - public void onPickDeviceFailed(boolean canceled); -} diff --git a/src/com/connectsdk/device/netcast/NetcastAppNumberParser.java b/src/com/connectsdk/device/netcast/NetcastAppNumberParser.java deleted file mode 100644 index 029c6ebc..00000000 --- a/src/com/connectsdk/device/netcast/NetcastAppNumberParser.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * NetcastAppNumberParser - * 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.device.netcast; - -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; - -public class NetcastAppNumberParser extends DefaultHandler { - public String value; - - public final String TYPE = "type"; - public final String NUMBER = "number"; - - int count; - - public NetcastAppNumberParser() { - value = null; - } - - @Override - public void endElement(String uri, String localName, String qName) throws SAXException { - if (qName.equalsIgnoreCase(TYPE)) { - } - else if (qName.equalsIgnoreCase(NUMBER)) { - count = Integer.parseInt(value); - } - value = null; - } - - @Override - public void characters(char[] ch, int start, int length) throws SAXException { - value = new String(ch, start, length); - } - - public int getApplicationNumber() { - return count; - } -} diff --git a/src/com/connectsdk/device/netcast/NetcastApplicationsParser.java b/src/com/connectsdk/device/netcast/NetcastApplicationsParser.java deleted file mode 100644 index 9d4e9df5..00000000 --- a/src/com/connectsdk/device/netcast/NetcastApplicationsParser.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * NetcastApplicationsParser - * 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.device.netcast; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.xml.sax.Attributes; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; - -public class NetcastApplicationsParser extends DefaultHandler { - public JSONArray applicationList; - public JSONObject application; - - public String value; - - public final String DATA = "data"; - public final String AUID = "auid"; - public final String NAME = "name"; - public final String TYPE = "type"; - public final String CPID = "cpid"; - public final String ADULT = "adult"; - public final String ICON_NAME = "icon_name"; - - public NetcastApplicationsParser() { - applicationList = new JSONArray(); - value = null; - } - - @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { - if (qName.equalsIgnoreCase(DATA)) { - application = new JSONObject(); - } - } - - @Override - public void endElement(String uri, String localName, String qName) throws SAXException { - try { - if (qName.equalsIgnoreCase(DATA)) { - applicationList.put(application); - } - else if (qName.equalsIgnoreCase(AUID)) { - application.put("id", value); - } - else if (qName.equalsIgnoreCase(NAME)) { - application.put("title", value); - } - else if (qName.equalsIgnoreCase(TYPE)) { - application.put(TYPE, value); - } - else if (qName.equalsIgnoreCase(CPID)) { - application.put(CPID, value); - } - else if (qName.equalsIgnoreCase(ADULT)) { - application.put(ADULT, value); - } - else if (qName.equalsIgnoreCase(ICON_NAME)) { - application.put(ICON_NAME, value); - } - value = null; - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void characters(char[] ch, int start, int length) throws SAXException { - value = new String(ch, start, length); - } - - public JSONArray getApplications() { - return applicationList; - } -} diff --git a/src/com/connectsdk/device/netcast/NetcastChannelParser.java b/src/com/connectsdk/device/netcast/NetcastChannelParser.java deleted file mode 100644 index 021f0189..00000000 --- a/src/com/connectsdk/device/netcast/NetcastChannelParser.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * NetcastChannelParser - * 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.device.netcast; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.xml.sax.Attributes; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; - -import com.connectsdk.core.ChannelInfo; - -public class NetcastChannelParser extends DefaultHandler { - public JSONArray channelArray; - public JSONObject channel; - - public String value; - - public final String CHANNEL_TYPE = "chtype"; - public final String MAJOR = "major"; - public final String MINOR = "minor"; - public final String DISPLAY_MAJOR = "displayMajor"; - public final String DISPLAY_MINOR = "displayMinor"; - public final String SOURCE_INDEX = "sourceIndex"; - public final String PHYSICAL_NUM = "physicalNum"; - public final String CHANNEL_NAME = "chname"; - public final String PROGRAM_NAME = "progName"; - public final String AUDIO_CHANNEL = "audioCh"; - public final String INPUT_SOURCE_NAME = "inputSourceName"; - public final String INPUT_SOURCE_TYPE = "inputSourceType"; - public final String LABEL_NAME = "labelName"; - public final String INPUT_SOURCE_INDEX = "inputSourceIdx"; - - public NetcastChannelParser() { - channelArray = new JSONArray(); - value = null; - } - - @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { - if (qName.equalsIgnoreCase("data")) { - channel = new JSONObject(); - } - } - - @Override - public void endElement(String uri, String localName, String qName) throws SAXException { - try { - if (qName.equalsIgnoreCase("data")) { - channelArray.put(channel); - } - else if (qName.equalsIgnoreCase(CHANNEL_TYPE)) { - channel.put("channelModeName", value); - } - else if (qName.equalsIgnoreCase(MAJOR)) { - channel.put("majorNumber", Integer.parseInt(value)); - } - else if (qName.equalsIgnoreCase(DISPLAY_MAJOR)) { - channel.put("displayMajorNumber", Integer.parseInt(value)); - } - else if (qName.equalsIgnoreCase(MINOR)) { - channel.put("minorNumber", Integer.parseInt(value)); - } - else if (qName.equalsIgnoreCase(DISPLAY_MINOR)) { - channel.put("displayMinorNumber", Integer.parseInt(value)); - } - else if (qName.equalsIgnoreCase(SOURCE_INDEX)) { - channel.put("sourceIndex", value); - } - else if (qName.equalsIgnoreCase(PHYSICAL_NUM)) { - channel.put("physicalNumber", Integer.parseInt(value)); - } - else if (qName.equalsIgnoreCase(CHANNEL_NAME)) { - channel.put("channelName", value); - } - else if (qName.equalsIgnoreCase(PROGRAM_NAME)) { - channel.put("programName", value); - } - else if (qName.equalsIgnoreCase(AUDIO_CHANNEL)) { - channel.put("audioCh", value); - } - else if (qName.equalsIgnoreCase(INPUT_SOURCE_NAME)) { - channel.put("inputSourceName", value); - } - else if (qName.equalsIgnoreCase(INPUT_SOURCE_TYPE)) { - channel.put("inputSourceType", value); - } - else if (qName.equalsIgnoreCase(LABEL_NAME)) { - channel.put("labelName", value); - } - else if (qName.equalsIgnoreCase(INPUT_SOURCE_INDEX)) { - channel.put("inputSourceIndex", value); - } - value = null; - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void characters(char[] ch, int start, int length) throws SAXException { - value = new String(ch, start, length); - } - - public JSONArray getJSONChannelArray() { - return channelArray; - } - - public static ChannelInfo parseRawChannelData(JSONObject channelRawData) { - String channelName = null; - String channelId = null; - String channelNumber = null; - int minorNumber = 0; - int majorNumber = 0; - - ChannelInfo channelInfo = new ChannelInfo(); - channelInfo.setRawData(channelRawData); - - try { - if ( !channelRawData.isNull("channelName") ) - channelName = (String) channelRawData.get("channelName"); - - if ( !channelRawData.isNull("channelId") ) - channelId = (String) channelRawData.get("channelId"); - - if ( !channelRawData.isNull("majorNumber")) - majorNumber = (Integer) channelRawData.get("majorNumber"); - - if ( !channelRawData.isNull("minorNumber")) - minorNumber = (Integer) channelRawData.get("minorNumber"); - - if ( !channelRawData.isNull("channelNumber") ) - channelNumber = (String) channelRawData.get("channelNumber"); - else { - channelNumber = String.format(String.valueOf(majorNumber) + "-" + String.valueOf(minorNumber)); - } - - channelInfo.setName(channelName); - channelInfo.setId(channelId); - channelInfo.setNumber(channelNumber); - channelInfo.setMajorNumber(majorNumber); - channelInfo.setMinorNumber(minorNumber); - - } catch (JSONException e) { - e.printStackTrace(); - } - - return channelInfo; - } -} diff --git a/src/com/connectsdk/device/netcast/NetcastHttpServer.java b/src/com/connectsdk/device/netcast/NetcastHttpServer.java deleted file mode 100644 index b0dbc89b..00000000 --- a/src/com/connectsdk/device/netcast/NetcastHttpServer.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * NetcastHttpServer - * 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.device.netcast; - -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.PrintWriter; -import java.io.UnsupportedEncodingException; -import java.net.ServerSocket; -import java.net.Socket; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.List; -import java.util.Locale; -import java.util.TimeZone; - -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; - -import org.json.JSONException; -import org.json.JSONObject; -import org.xml.sax.SAXException; - -import android.util.Log; - -import com.connectsdk.core.ChannelInfo; -import com.connectsdk.core.TextInputStatusInfo; -import com.connectsdk.core.Util; -import com.connectsdk.service.NetcastTVService; -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.command.URLServiceSubscription; - -public class NetcastHttpServer { - static final String UDAP_PATH_EVENT = "/udap/api/event"; - - NetcastTVService service; - ServerSocket welcomeSocket; - ResponseListener textChangedListener; - - int port = -1; - - List> subscriptions; - - boolean running = false; - - public NetcastHttpServer(NetcastTVService service, int port, ResponseListener textChangedListener) { - this.service = service; - this.port = port; - this.textChangedListener = textChangedListener; - } - - public void start() { - if (running) - return; - - running = true; - - try { - welcomeSocket = new ServerSocket(this.port); - } catch (IOException ex) { - ex.printStackTrace(); - } - - while (running) { - if (welcomeSocket == null || welcomeSocket.isClosed()) { - stop(); - break; - } - - Socket connectionSocket = null; - BufferedReader inFromClient = null; - DataOutputStream outToClient = null; - - try { - connectionSocket = welcomeSocket.accept(); - } catch (IOException ex) { - ex.printStackTrace(); - // this socket may have been closed, so we'll stop - stop(); - return; - } - - String str = null; - int c; - StringBuilder sb = new StringBuilder(); - - try { - inFromClient = new BufferedReader(new InputStreamReader(connectionSocket.getInputStream())); - - while ( (str = inFromClient.readLine()) != null ) { - if ( str.equals("") ) { - break; - } - } - - while ( (c = inFromClient.read()) != -1 ) { - sb.append((char)c); - String temp = sb.toString(); - - if ( temp.endsWith("") ) - break; - } - } catch (IOException ex) { - ex.printStackTrace(); - } - - String body = sb.toString(); - - Log.d("Connect SDK", "got message body: " + body); - - Calendar calendar = Calendar.getInstance(); - SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); - dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); - String date = dateFormat.format(calendar.getTime()); - String androidOSVersion = android.os.Build.VERSION.RELEASE; - - PrintWriter out = null; - - try { - outToClient = new DataOutputStream(connectionSocket.getOutputStream()); - out = new PrintWriter(outToClient); - out.println("HTTP/1.1 200 OK"); - out.println("Server: Android/" + androidOSVersion + " UDAP/2.0 ConnectSDK/1.2.1"); - out.println("Cache-Control: no-store, no-cache, must-revalidate"); - out.println("Date: " + date); - out.println("Connection: Close"); - out.println("Content-Length: 0"); - out.flush(); - } catch (IOException ex) { - ex.printStackTrace(); - } finally { - try { - inFromClient.close(); - out.close(); - outToClient.close(); - connectionSocket.close(); - } catch (IOException ex) { - ex.printStackTrace(); - } - } - - SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); - InputStream stream = null; - - try { - stream = new ByteArrayInputStream(body.getBytes("UTF-8")); - } catch (UnsupportedEncodingException ex) { - ex.printStackTrace(); - } - - NetcastPOSTRequestParser handler = new NetcastPOSTRequestParser(); - - SAXParser saxParser; - try { - saxParser = saxParserFactory.newSAXParser(); - saxParser.parse(stream, handler); - } catch (IOException ex) { - ex.printStackTrace(); - } catch (ParserConfigurationException e) { - e.printStackTrace(); - } catch (SAXException e) { - e.printStackTrace(); - } - - if ( body.contains("ChannelChanged") ) { - ChannelInfo channel = NetcastChannelParser.parseRawChannelData(handler.getJSONObject()); - - Log.d("Connect SDK", "Channel Changed: " + channel.getNumber()); - - for (URLServiceSubscription sub: subscriptions) { - if ( sub.getTarget().equalsIgnoreCase("ChannelChanged") ) { - for (int i = 0; i < sub.getListeners().size(); i++) { - @SuppressWarnings("unchecked") - ResponseListener listener = (ResponseListener) sub.getListeners().get(i); - Util.postSuccess(listener, channel); - } - } - } - } - else if ( body.contains("KeyboardVisible") ) { - boolean focused = false; - - TextInputStatusInfo keyboard = new TextInputStatusInfo(); - keyboard.setRawData(handler.getJSONObject()); - - try { - JSONObject currentWidget = (JSONObject) handler.getJSONObject().get("currentWidget"); - focused = (Boolean) currentWidget.get("focus"); - keyboard.setFocused(focused); - } catch (JSONException e) { - e.printStackTrace(); - } - - Log.d("Connect SDK", "KeyboardFocused?: " + focused); - - for (URLServiceSubscription sub: subscriptions) { - if ( sub.getTarget().equalsIgnoreCase("KeyboardVisible") ) { - for (int i = 0; i < sub.getListeners().size(); i++) { - @SuppressWarnings("unchecked") - ResponseListener listener = (ResponseListener) sub.getListeners().get(i); - Util.postSuccess(listener, keyboard); - } - } - } - } - else if ( body.contains("TextEdited") ) { - System.out.println("TextEdited"); - - String newValue = ""; - - try { - newValue = handler.getJSONObject().getString("value"); - } catch (JSONException ex) { - ex.printStackTrace(); - } - - Util.postSuccess(textChangedListener, newValue); - } - else if ( body.contains("3DMode") ) { - try { - String enabled = (String) handler.getJSONObject().get("value"); - boolean bEnabled; - - if ( enabled.equalsIgnoreCase("true") ) - bEnabled = true; - else - bEnabled = false; - - for (URLServiceSubscription sub: subscriptions) { - if ( sub.getTarget().equalsIgnoreCase("3DMode") ) { - for (int i = 0; i < sub.getListeners().size(); i++) { - @SuppressWarnings("unchecked") - ResponseListener listener = (ResponseListener) sub.getListeners().get(i); - Util.postSuccess(listener, bEnabled); - } - } - } - } catch (JSONException e) { - e.printStackTrace(); - } - } - } - } - - public void stop() { - if (!running) - return; - - if (welcomeSocket != null && !welcomeSocket.isClosed()) { - try { - welcomeSocket.close(); - } catch (IOException ex) { - ex.printStackTrace(); - } - } - - welcomeSocket = null; - running = false; - } - - public void setSubscriptions(List> subscriptions) { - this.subscriptions = subscriptions; - } - -} \ No newline at end of file diff --git a/src/com/connectsdk/device/netcast/NetcastPOSTRequestParser.java b/src/com/connectsdk/device/netcast/NetcastPOSTRequestParser.java deleted file mode 100644 index 6a63269e..00000000 --- a/src/com/connectsdk/device/netcast/NetcastPOSTRequestParser.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * NetcastPOSTRequestParser - * 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.device.netcast; - -import org.json.JSONException; -import org.json.JSONObject; -import org.xml.sax.Attributes; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; - -public class NetcastPOSTRequestParser extends DefaultHandler { - public JSONObject object; - public JSONObject subObject; - - boolean textEditMode = false; - boolean keyboardVisibleMode = false; - - public String value; - - public final String CHANNEL_TYPE = "chtype"; - public final String MAJOR = "major"; - public final String MINOR = "minor"; - public final String DISPLAY_MAJOR = "displayMajor"; - public final String DISPLAY_MINOR = "displayMinor"; - public final String SOURCE_INDEX = "sourceIndex"; - public final String PHYSICAL_NUM = "physicalNum"; - public final String CHANNEL_NAME = "chname"; - public final String PROGRAM_NAME = "progName"; - public final String AUDIO_CHANNEL = "audioCh"; - public final String INPUT_SOURCE_NAME = "inputSourceName"; - public final String INPUT_SOURCE_TYPE = "inputSourceType"; - public final String LABEL_NAME = "labelName"; - public final String INPUT_SOURCE_INDEX = "inputSourceIdx"; - - public final String VALUE = "value"; - public final String MODE = "mode"; - public final String STATE = "state"; - - public NetcastPOSTRequestParser() { - object = new JSONObject(); - subObject = new JSONObject(); - value = null; - } - - @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { - - } - - @Override - public void endElement(String uri, String localName, String qName) throws SAXException { - try { - System.out.println("XML key: " + qName + ", value: " + value); - if (qName.equalsIgnoreCase(CHANNEL_TYPE)) { - object.put("channelModeName", value); - } - else if (qName.equalsIgnoreCase(MAJOR)) { - object.put("majorNumber", Integer.parseInt(value)); - } - else if (qName.equalsIgnoreCase(DISPLAY_MAJOR)) { - object.put("displayMajorNumber", Integer.parseInt(value)); - } - else if (qName.equalsIgnoreCase(MINOR)) { - object.put("minorNumber", Integer.parseInt(value)); - } - else if (qName.equalsIgnoreCase(DISPLAY_MINOR)) { - object.put("displayMinorNumber", Integer.parseInt(value)); - } - else if (qName.equalsIgnoreCase(SOURCE_INDEX)) { - object.put("sourceIndex", value); - } - else if (qName.equalsIgnoreCase(PHYSICAL_NUM)) { - object.put("physicalNumber", Integer.parseInt(value)); - } - else if (qName.equalsIgnoreCase(CHANNEL_NAME)) { - object.put("channelName", value); - } - else if (qName.equalsIgnoreCase(PROGRAM_NAME)) { - object.put("programName", value); - } - else if (qName.equalsIgnoreCase(AUDIO_CHANNEL)) { - object.put("audioCh", value); - } - else if (qName.equalsIgnoreCase(INPUT_SOURCE_NAME)) { - object.put("inputSourceName", value); - } - else if (qName.equalsIgnoreCase(INPUT_SOURCE_TYPE)) { - object.put("inputSourceType", value); - } - else if (qName.equalsIgnoreCase(LABEL_NAME)) { - object.put("labelName", value); - } - else if (qName.equalsIgnoreCase(INPUT_SOURCE_INDEX)) { - object.put("inputSourceIndex", value); - } - else if (qName.equalsIgnoreCase(VALUE)) { - if ( keyboardVisibleMode == true ) { - if ( value.equalsIgnoreCase("true") ) - subObject.put("focus", true); - else - subObject.put("focus", false); - object.put("currentWidget", subObject); - } - else { - object.put("value", value); - } - } - else if (qName.equalsIgnoreCase(MODE)) { - if ( keyboardVisibleMode == true ) { - if ( value.equalsIgnoreCase("default") ) - subObject.put("hiddenText", false); - else - subObject.put("hiddenText", true); - object.put("currentWidget", subObject); - } - } - else if (qName.equalsIgnoreCase(STATE)) { - - } - else if ( value != null && value.equalsIgnoreCase("KeyboardVisible") ) { - keyboardVisibleMode = true; - - try { - subObject.put("contentType", "normal"); - subObject.put("focus", false); - subObject.put("hiddenText", false); - subObject.put("predictionEnabled", false); - subObject.put("correctionEnabled", false); - subObject.put("autoCapitalization", false); - } catch (JSONException e) { - e.printStackTrace(); - } - } - else if ( value != null && value.equalsIgnoreCase("TextEdited") ) { - textEditMode = true; - } - value = null; - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void characters(char[] ch, int start, int length) throws SAXException { - value = new String(ch, start, length); - } - - public JSONObject getJSONObject() { - return object; - } -} diff --git a/src/com/connectsdk/device/netcast/NetcastVolumeParser.java b/src/com/connectsdk/device/netcast/NetcastVolumeParser.java deleted file mode 100644 index 09f14237..00000000 --- a/src/com/connectsdk/device/netcast/NetcastVolumeParser.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * NetcastVolumeParser - * 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.device.netcast; - -import org.json.JSONException; -import org.json.JSONObject; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; - -public class NetcastVolumeParser extends DefaultHandler { - public JSONObject volumeStatus; - - public String value; - - public final String MUTE = "mute"; - public final String MIN_LEVEL = "minLevel"; - public final String MAX_LEVEL = "maxLevel"; - public final String LEVEL = "level"; - - public NetcastVolumeParser() { - volumeStatus = new JSONObject(); - value = null; - } - - @Override - public void endElement(String uri, String localName, String qName) throws SAXException { - try { - if (qName.equalsIgnoreCase(MUTE)) { - volumeStatus.put(MUTE, Boolean.parseBoolean(value)); - } - else if (qName.equalsIgnoreCase(MIN_LEVEL)) { - volumeStatus.put(MIN_LEVEL, Integer.parseInt(value)); - } - else if (qName.equalsIgnoreCase(MAX_LEVEL)) { - volumeStatus.put(MAX_LEVEL, Integer.parseInt(value)); - } - else if (qName.equalsIgnoreCase(LEVEL)) { - volumeStatus.put(LEVEL, Integer.parseInt(value)); - } - value = null; - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void characters(char[] ch, int start, int length) throws SAXException { - value = new String(ch, start, length); - } - - public JSONObject getVolumeStatus() { - return volumeStatus; - } -} diff --git a/src/com/connectsdk/device/netcast/VirtualKeycodes.java b/src/com/connectsdk/device/netcast/VirtualKeycodes.java deleted file mode 100644 index 59ae395c..00000000 --- a/src/com/connectsdk/device/netcast/VirtualKeycodes.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * VirtualKeycodes - * 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.device.netcast; - -public enum VirtualKeycodes { - POWER (1), - NUMBER_0 (2), - NUMBER_1 (3), - NUMBER_2 (4), - NUMBER_3 (5), - NUMBER_4 (6), - NUMBER_5 (7), - NUMBER_6 (8), - NUMBER_7 (9), - NUMBER_8 (10), - NUMBER_9 (11), - - KEY_UP (12), - KEY_DOWN (13), - KEY_LEFT (14), - KEY_RIGHT (15), - - OK (20), - HOME (21), - MENU (22), - BACK (23), // PREVIOUS_KEY - - VOLUME_UP (24), - VOLUME_DOWN (25), - MUTE (26), - - CHANNEL_UP (27), - CHANNEL_DOWN (28), - - BLUE (29), - GREEN (30), - RED (31), - YELLOW (32), - - PLAY (33), - PAUSE (34), - STOP (35), - FAST_FORWARD (36), - REWIND (37), - SKIP_FORWARD (38), - SKIP_BACKWARD (39), - RECORD (40), - RECORDING_LIST (41), - REPEAT (42), - LIVE_TV (43), - EPG (44), - CURRENT_PROGRAM_INFO (45), - - ASPECT_RATIO (46), - EXTERNAL_INPUT (47), - PIP_SECONDARY_VIDEO (48), - SHOW_CHANGE_SUBTITLE (49), - PROGRAM_LIST (50), - - TELE_TEXT (51), - MARK (52), - - VIDEO_3D (400), - AUDIO_3D_L_R (401), - - DASH (402), - PREVIOUS_CHANNEL (403), // FLASH BACK - FAVORITE_CHANNEL (404), - - QUICK_MENU (405), - TEXT_OPTION (406), - AUDIO_DESCRIPTION (407), - NETCAST_KEY (408), // SAME WITH HOME MENU - ENERGY_SAVING (409), - AV_MODE (410), - SIMPLINK (411), - EXIT (412), - RESERVATION_PROGRAM_LIST (413), - - PIP_CHANNEL_UP (414), - PIP_CHANNEL_DOWN (415), - SWITCHING_PRIMARY_SECONDARY_VIDEO (416), - MY_APPS (417); - - private final int code; - - private VirtualKeycodes (int code) { - this.code = code; - } - - public int getCode() { - return code; - } -} diff --git a/src/com/connectsdk/device/roku/RokuApplicationListParser.java b/src/com/connectsdk/device/roku/RokuApplicationListParser.java deleted file mode 100644 index 0b007ff8..00000000 --- a/src/com/connectsdk/device/roku/RokuApplicationListParser.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * RokuApplicationListParser - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Hyun Kook Khang on 26 Feb 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.device.roku; - -import java.util.ArrayList; -import java.util.List; - -import org.xml.sax.Attributes; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; - -import com.connectsdk.core.AppInfo; - -public class RokuApplicationListParser extends DefaultHandler { - public String value; - - public final String APP = "app"; - public final String ID = "id"; - - public List appList; - public AppInfo appInfo; - - public RokuApplicationListParser() { - value = null; - appList = new ArrayList(); - } - - @Override - public void startElement(String uri, String localName, String qName, final Attributes attributes) throws SAXException { - if (qName.equalsIgnoreCase(APP)) { - final int index = attributes.getIndex(ID); - - if ( index != -1 ) { - appInfo = new AppInfo() {{ - setId(attributes.getValue(index)); - }}; - } - } - } - - @Override - public void endElement(String uri, String localName, String qName) throws SAXException { - if (qName.equalsIgnoreCase(APP)) { - appInfo.setName(value); - appList.add(appInfo); - } - value = null; - } - - @Override - public void characters(char[] ch, int start, int length) throws SAXException { - value = new String(ch, start, length); - } - - public List getApplicationList() { - return appList; - } -} diff --git a/src/com/connectsdk/discovery/CapabilityFilter.java b/src/com/connectsdk/discovery/CapabilityFilter.java deleted file mode 100644 index d9130ac5..00000000 --- a/src/com/connectsdk/discovery/CapabilityFilter.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * CapabilityFilter - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Hyun Kook Khang on 01 Feb 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.discovery; - -import java.util.ArrayList; -import java.util.List; - -/** - * CapabilityFilter is an object that wraps a List of required capabilities. This CapabilityFilter is used for determining which devices will appear in DiscoveryManager's compatibleDevices array. The contents of a CapabilityFilter's array must be any of the string constants defined in the Capability Class constants. - * - * ###CapabilityFilter values - * Here are some examples of values for the Capability constants. - * - * - MediaPlayer.Display_Video = "MediaPlayer.Display.Video" - * - MediaPlayer.Display_Image = "MediaPlayer.Display.Image" - * - VolumeControl.Volume_Subscribe = "VolumeControl.Subscribe" - * - MediaControl.Any = "MediaControl.Any" - * - * All Capability header files also define a constant array of all capabilities defined in that header (ex. kVolumeControlCapabilities). - * - * ###AND/OR Filtering - * CapabilityFilter is an AND filter. A ConnectableDevice would need to satisfy all conditions of a CapabilityFilter to pass. - * - * The DiscoveryManager capabilityFilters is an OR filter. a ConnectableDevice only needs to satisfy one condition (CapabilityFilter) to pass. - * - * ###Examples - * Filter for all devices that support video playback AND any media controls AND volume up/down. - * -@code - List capabilities = new ArrayList(); - capabilities.add(MediaPlayer.Display_Video); - capabilities.add(MediaControl.Any); - capabilities.add(VolumeControl.Volume_Up_Down); - - CapabilityFilter filter = - new CapabilityFilter(capabilities); - - DiscoveryManager.getInstance().setCapabilityFilters(filter); -@endcode - * - * Filter for all devices that support (video playback AND any media controls AND volume up/down) OR (image display). - * -@code - CapabilityFilter videoFilter = - new CapabilityFilter( - MediaPlayer.Display_Video, - MediaControl.Any, - VolumeControl.Volume_Up_Down); - - CapabilityFilter imageFilter = - new CapabilityFilter( - MediaPlayer.Display_Image); - - DiscoveryManager.getInstance().setCapabilityFilters(videoFilter, imageFilter); -@endcode - */ -public class CapabilityFilter { - - /** - * List of capabilities required by this filter. This property is readonly -- use the addCapability or addCapabilities to build this object. - */ - public List capabilities = new ArrayList(); - - /** - * Create an empty CapabilityFilter. - */ - public CapabilityFilter() { - } - - /** - * Create a CapabilityFilter with the given array of required capabilities. - * - * @param capabilities Capabilities to be added to the new filter - */ - public CapabilityFilter(String ... capabilities) { - for (String capability : capabilities) { - addCapability(capability); - } - } - - /** - * Create a CapabilityFilter with the given array of required capabilities. - * - * @param capabilities List of capability names (see capability class files for String constants) - */ - public CapabilityFilter(List capabilities) { - addCapabilities(capabilities); - } - - /** - * Add a required capability to the filter. - * - * @param capability Capability name to add (see capability class files for String constants) - */ - public void addCapability(String capability) { - capabilities.add(capability); - } - - /** - * Add array of required capabilities to the filter. (see capability class files for String constants) - * - * @param capabilities List of capability names - */ - public void addCapabilities(List capabilities) { - this.capabilities.addAll(capabilities); - } - - /** - * Add array of required capabilities to the filter. (see capability classes files for String constants) - * - * @param capabilities String[] of capability names - */ - public void addCapabilities(String... capabilities) { - for (String capability : capabilities) - this.capabilities.add(capability); - } -} diff --git a/src/com/connectsdk/discovery/DiscoveryManager.java b/src/com/connectsdk/discovery/DiscoveryManager.java deleted file mode 100644 index 2ba0c802..00000000 --- a/src/com/connectsdk/discovery/DiscoveryManager.java +++ /dev/null @@ -1,867 +0,0 @@ -/* - * DiscoveryManager - * 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.discovery; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Timer; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; - -import org.json.JSONException; -import org.json.JSONObject; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.wifi.WifiManager; -import android.net.wifi.WifiManager.MulticastLock; -import android.util.Log; - -import com.connectsdk.DefaultPlatform; -import com.connectsdk.core.Util; -import com.connectsdk.device.ConnectableDevice; -import com.connectsdk.device.ConnectableDeviceListener; -import com.connectsdk.device.ConnectableDeviceStore; -import com.connectsdk.device.DefaultConnectableDeviceStore; -import com.connectsdk.service.DLNAService; -import com.connectsdk.service.DeviceService; -import com.connectsdk.service.DeviceService.PairingType; -import com.connectsdk.service.NetcastTVService; -import com.connectsdk.service.command.ServiceCommandError; -import com.connectsdk.service.config.ServiceConfig; -import com.connectsdk.service.config.ServiceConfig.ServiceConfigListener; -import com.connectsdk.service.config.ServiceDescription; - -/** - * ###Overview - * - * At the heart of Connect SDK is DiscoveryManager, a multi-protocol service discovery engine with a pluggable architecture. Much of your initial experience with Connect SDK will be with the DiscoveryManager class, as it consolidates discovered service information into ConnectableDevice objects. - * - * ###In depth - * DiscoveryManager supports discovering services of differing protocols by using DiscoveryProviders. Many services are discoverable over [SSDP][0] and are registered to be discovered with the SSDPDiscoveryProvider class. - * - * As services are discovered on the network, the DiscoveryProviders will notify DiscoveryManager. DiscoveryManager is capable of attributing multiple services, if applicable, to a single ConnectableDevice instance. Thus, it is possible to have a mixed-mode ConnectableDevice object that is theoretically capable of more functionality than a single service can provide. - * - * DiscoveryManager keeps a running list of all discovered devices and maintains a filtered list of devices that have satisfied any of your CapabilityFilters. This filtered list is used by the DevicePicker when presenting the user with a list of devices. - * - * Only one instance of the DiscoveryManager should be in memory at a time. To assist with this, DiscoveryManager has static method at sharedManager. - * - * Example: - * - * @capability kMediaControlPlay - * - @code - DiscoveryManager.init(getApplicationContext()); - DiscoveryManager discoveryManager = DiscoveryManager.getInstance(); - discoveryManager.addListener(this); - discoveryManager.start(); - @endcode - * - * [0]: http://tools.ietf.org/html/draft-cai-ssdp-v1-03 - */ -public class DiscoveryManager implements ConnectableDeviceListener, DiscoveryProviderListener, ServiceConfigListener { - - public enum PairingLevel { - OFF, - ON - } - - // @cond INTERNAL - - public static String CONNECT_SDK_VERSION = "1.3.2"; - - private static DiscoveryManager instance; - - Context context; - ConnectableDeviceStore connectableDeviceStore; - - int rescanInterval = 10; - - private ConcurrentHashMap allDevices; - private ConcurrentHashMap compatibleDevices; - - private ConcurrentHashMap> deviceClasses; - private CopyOnWriteArrayList discoveryProviders; - - private CopyOnWriteArrayList discoveryListeners; - List capabilityFilters; - - MulticastLock multicastLock; - BroadcastReceiver receiver; - boolean isBroadcastReceiverRegistered = false; - - Timer rescanTimer; - - PairingLevel pairingLevel; - - private boolean mSearching = false; - - // @endcond - - /** - * Initilizes the Discovery manager with a valid context. This should be done as soon as possible and it should use getApplicationContext() as the Discovery manager could persist longer than the current Activity. - * - @code - DiscoveryManager.init(getApplicationContext()); - @endcode - */ - public static synchronized void init(Context context) { - instance = new DiscoveryManager(context); - } - - public static synchronized void destroy() { - instance.onDestroy(); - } - - /** - * Initilizes the Discovery manager with a valid context. This should be done as soon as possible and it should use getApplicationContext() as the Discovery manager could persist longer than the current Activity. - * - * This accepts a ConnectableDeviceStore to use instead of the default device store. - * - @code - MyConnectableDeviceStore myDeviceStore = new MyConnectableDeviceStore(); - DiscoveryManager.init(getApplicationContext(), myDeviceStore); - @endcode - */ - public static synchronized void init(Context context, ConnectableDeviceStore connectableDeviceStore) { - instance = new DiscoveryManager(context, connectableDeviceStore); - } - - /** - * Get a shared instance of DiscoveryManager. - */ - public static synchronized DiscoveryManager getInstance() { - if (instance == null) - throw new Error("Call DiscoveryManager.init(Context) first"); - - return instance; - } - - // @cond INTERNAL - /** - * Create a new instance of DiscoveryManager. - * Direct use of this constructor is not recommended. In most cases, - * you should use DiscoveryManager.getInstance() instead. - */ - public DiscoveryManager(Context context) { - this(context, new DefaultConnectableDeviceStore(context)); - } - - /** - * Create a new instance of DiscoveryManager. - * Direct use of this constructor is not recommended. In most cases, - * you should use DiscoveryManager.getInstance() instead. - */ - public DiscoveryManager(Context context, ConnectableDeviceStore connectableDeviceStore) { - this.context = context; - this.connectableDeviceStore = connectableDeviceStore; - - allDevices = new ConcurrentHashMap(8, 0.75f, 2); - compatibleDevices = new ConcurrentHashMap(8, 0.75f, 2); - - deviceClasses = new ConcurrentHashMap>(4, 0.75f, 2); - discoveryProviders = new CopyOnWriteArrayList(); - - discoveryListeners = new CopyOnWriteArrayList(); - - WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); - multicastLock = wifiMgr.createMulticastLock("Connect SDK"); - multicastLock.setReferenceCounted(true); - - capabilityFilters = new ArrayList(); - pairingLevel = PairingLevel.OFF; - - receiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - - if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { - NetworkInfo networkInfo = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); - - switch (networkInfo.getState()) { - case CONNECTED: - if (mSearching) { - for (DiscoveryProvider provider : discoveryProviders) { - provider.start(); - } - } - - break; - - case DISCONNECTED: - Log.w("Connect SDK", "Network connection is disconnected"); - - for (DiscoveryProvider provider : discoveryProviders) { - provider.reset(); - } - - allDevices.clear(); - - for (ConnectableDevice device: compatibleDevices.values()) { - handleDeviceLoss(device); - } - compatibleDevices.clear(); - - for (DiscoveryProvider provider : discoveryProviders) { - provider.stop(); - } - - break; - - case CONNECTING: - break; - case DISCONNECTING: - break; - case SUSPENDED: - break; - case UNKNOWN: - break; - } - } - } - }; - - registerBroadcastReceiver(); - } - // @endcond - - private void registerBroadcastReceiver() { - if (isBroadcastReceiverRegistered == false) { - isBroadcastReceiverRegistered = true; - - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); - context.registerReceiver(receiver, intentFilter); - } - } - - private void unregisterBroadcastReceiver() { - if (isBroadcastReceiverRegistered == true) { - isBroadcastReceiverRegistered = false; - - context.unregisterReceiver(receiver); - } - } - - /** - * Listener which should receive discovery updates. It is not necessary to set this listener property unless you are implementing your own device picker. Connect SDK provides a default DevicePicker which acts as a DiscoveryManagerListener, and should work for most cases. - * - * If you have provided a capabilityFilters array, the listener will only receive update messages for ConnectableDevices which satisfy at least one of the CapabilityFilters. If no capabilityFilters array is provided, the listener will receive update messages for all ConnectableDevice objects that are discovered. - */ - public void addListener(DiscoveryManagerListener listener) { - // notify listener of all devices so far - for (ConnectableDevice device: compatibleDevices.values()) { - listener.onDeviceAdded(this, device); - } - discoveryListeners.add(listener); - } - - /** - * Removes a previously added listener - */ - public void removeListener(DiscoveryManagerListener listener) { - discoveryListeners.remove(listener); - } - - public void setCapabilityFilters(CapabilityFilter ... capabilityFilters) { - setCapabilityFilters(Arrays.asList(capabilityFilters)); - } - - public void setCapabilityFilters(List capabilityFilters) { - this.capabilityFilters = capabilityFilters; - - for (ConnectableDevice device: compatibleDevices.values()) { - handleDeviceLoss(device); - } - - compatibleDevices.clear(); - - for (ConnectableDevice device: allDevices.values()) { - if (deviceIsCompatible(device)) { - compatibleDevices.put(device.getIpAddress(), device); - - handleDeviceAdd(device); - } - } - } - - /** - * Returns the list of capability filters. - */ - public List getCapabilityFilters() { - return capabilityFilters; - } - - public boolean deviceIsCompatible(ConnectableDevice device) { - if (capabilityFilters == null || capabilityFilters.size() == 0) { - return true; - } - - boolean isCompatible = false; - - for (CapabilityFilter filter: this.capabilityFilters) { - if (device.hasCapabilities(filter.capabilities)) { - isCompatible = true; - break; - } - } - - return isCompatible; - } - // @cond INTERNAL - - /** - * Registers a commonly-used set of DeviceServices with DiscoveryManager. This method will be called on first call of startDiscovery if no DeviceServices have been registered. - * - * - CastDiscoveryProvider - * + CastService - * - SSDPDiscoveryProvider - * + DIALService - * + DLNAService (limited to LG TVs, currently) - * + NetcastTVService - * + RokuService - * + WebOSTVService - * + MultiScreenService - * - ZeroconfDiscoveryProvider - * + AirPlayService - */ - @SuppressWarnings("unchecked") - public void registerDefaultDeviceTypes() { - - HashMap deviceServiceMap = new HashMap(); - DefaultPlatform dp = new DefaultPlatform(); - deviceServiceMap = dp.getDeviceServiceMap(); - - for (Map.Entry entry : deviceServiceMap.entrySet()) { - try { - registerDeviceService((Class)Class.forName(entry.getKey()), (Class)Class.forName(entry.getValue())); - } catch (ClassNotFoundException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - -// registerDeviceService(WebOSTVService.class, SSDPDiscoveryProvider.class); -// registerDeviceService(NetcastTVService.class, SSDPDiscoveryProvider.class); -// registerDeviceService(DLNAService.class, SSDPDiscoveryProvider.class); -// registerDeviceService(DIALService.class, SSDPDiscoveryProvider.class); -// registerDeviceService(RokuService.class, SSDPDiscoveryProvider.class); -// registerDeviceService(CastService.class, CastDiscoveryProvider.class); -// registerDeviceService(AirPlayService.class, ZeroconfDiscoveryProvider.class); -// registerDeviceService(MultiScreenService.class, SSDPDiscoveryProvider.class); - } - - - - /** - * Registers a DeviceService with DiscoveryManager and tells it which DiscoveryProvider to use to find it. Each DeviceService has a JSONObject of discovery parameters that its DiscoveryProvider will use to find it. - * - * @param deviceClass Class for object that should be instantiated when DeviceService is found - * @param discoveryClass Class for object that should discover this DeviceService. If a DiscoveryProvider of this class already exists, then the existing DiscoveryProvider will be used. - */ - public void registerDeviceService(Class deviceClass, Class discoveryClass) { - if (!DeviceService.class.isAssignableFrom(deviceClass)) - return; - - if (!DiscoveryProvider.class.isAssignableFrom(discoveryClass)) - return; - - try { - DiscoveryProvider discoveryProvider = null; - - for (DiscoveryProvider dp : discoveryProviders) { - if (dp.getClass().isAssignableFrom(discoveryClass)) { - discoveryProvider = dp; - break; - } - } - - if (discoveryProvider == null) { - Constructor myConstructor = discoveryClass.getConstructor(Context.class); - Object myObj = myConstructor.newInstance(new Object[]{context}); - discoveryProvider = (DiscoveryProvider) myObj; - - discoveryProvider.addListener(this); - discoveryProviders.add(discoveryProvider); - } - Method m = deviceClass.getMethod("discoveryParameters"); - Object result = m.invoke(null); - JSONObject discoveryParameters = (JSONObject) result; - String serviceFilter = (String) discoveryParameters.get("serviceId"); - - deviceClasses.put(serviceFilter, deviceClass); - - discoveryProvider.addDeviceFilter(discoveryParameters); - } catch (SecurityException e) { - e.printStackTrace(); - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } catch (InstantiationException e) { - e.printStackTrace(); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - /** - * Unregisters a DeviceService with DiscoveryManager. If no other DeviceServices are set to being discovered with the associated DiscoveryProvider, then that DiscoveryProvider instance will be stopped and shut down. - * - * @param deviceClass Class for DeviceService that should no longer be discovered - * @param discoveryClass Class for DiscoveryProvider that is discovering DeviceServices of deviceClass type - */ - public void unregisterDeviceService(Class deviceClass, Class discoveryClass) { - if (!deviceClass.isAssignableFrom(DeviceService.class)) { - return; - } - - if (!discoveryClass.isAssignableFrom(DiscoveryProvider.class)) { - return; - } - - try { - DiscoveryProvider discoveryProvider = null; - - for (DiscoveryProvider dp: discoveryProviders) { - if (dp.getClass().isAssignableFrom(discoveryClass)) { - discoveryProvider = dp; - break; - } - } - - if (discoveryProvider == null) - return; - - Method m = deviceClass.getMethod("discoveryParameters"); - Object result = m.invoke(null); - JSONObject discoveryParameters = (JSONObject) result; - String serviceFilter = (String) discoveryParameters.get("serviceId"); - - deviceClasses.remove(serviceFilter); - - discoveryProvider.removeDeviceFilter(discoveryParameters); - - if (discoveryProvider.isEmpty()) { - discoveryProvider.stop(); - discoveryProviders.remove(discoveryProvider); - } - } catch (SecurityException e) { - e.printStackTrace(); - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } catch (JSONException e) { - e.printStackTrace(); - } - } - // @endcond - - /** - * Start scanning for devices on the local network. - */ - public void start() { - if (mSearching) - return; - - if (discoveryProviders == null) { - return; - } - - mSearching = true; - multicastLock.acquire(); - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - if (discoveryProviders.size() == 0) { - registerDefaultDeviceTypes(); - } - - ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo mWifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); - - if (mWifi.isConnected()) { - for (DiscoveryProvider provider : discoveryProviders) { - provider.start(); - } - } else { - Log.w("Connect SDK", "Wifi is not connected yet"); - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - for (DiscoveryManagerListener listener : discoveryListeners) - listener.onDiscoveryFailed(DiscoveryManager.this, new ServiceCommandError(0, "No wifi connection", null)); - } - }); - } - } - }); - } - - /** - * Stop scanning for devices. - * - * This method will be called when your app enters a background state. When your app resumes, startDiscovery will be called. - */ - public void stop() { - if (!mSearching) - return; - - mSearching = false; - - for (DiscoveryProvider provider : discoveryProviders) { - provider.stop(); - } - - if (multicastLock.isHeld()) { - multicastLock.release(); - } - } - - /** - * ConnectableDeviceStore object which loads & stores references to all discovered devices. Pairing codes/keys, SSL certificates, recent access times, etc are kept in the device store. - * - * ConnectableDeviceStore is a protocol which may be implemented as needed. A default implementation, DefaultConnectableDeviceStore, exists for convenience and will be used if no other device store is provided. - * - * In order to satisfy user privacy concerns, you should provide a UI element in your app which exposes the ConnectableDeviceStore removeAll method. - * - * To disable the ConnectableDeviceStore capabilities of Connect SDK, set this value to nil. This may be done at the time of instantiation with `DiscoveryManager.init(context, null);`. - */ - public void setConnectableDeviceStore(ConnectableDeviceStore connectableDeviceStore) { - this.connectableDeviceStore = connectableDeviceStore; - } - - /** - * ConnectableDeviceStore object which loads & stores references to all discovered devices. Pairing codes/keys, SSL certificates, recent access times, etc are kept in the device store. - * - * ConnectableDeviceStore is a protocol which may be implemented as needed. A default implementation, DefaultConnectableDeviceStore, exists for convenience and will be used if no other device store is provided. - * - * In order to satisfy user privacy concerns, you should provide a UI element in your app which exposes the ConnectableDeviceStore removeAll method. - * - * To disable the ConnectableDeviceStore capabilities of Connect SDK, set this value to nil. This may be done at the time of instantiation with `DiscoveryManager.init(context, null);`. - */ - public ConnectableDeviceStore getConnectableDeviceStore() { - return connectableDeviceStore; - } - - // @cond INTERNAL - public void handleDeviceAdd(ConnectableDevice device) { - if (!deviceIsCompatible(device)) - return; - - compatibleDevices.put(device.getIpAddress(), device); - - for (DiscoveryManagerListener listenter: discoveryListeners) { - listenter.onDeviceAdded(this, device); - } - } - - public void handleDeviceUpdate(ConnectableDevice device) { - if (deviceIsCompatible(device)) { - if (device.getIpAddress() != null && compatibleDevices.containsKey(device.getIpAddress())) { - for (DiscoveryManagerListener listenter: discoveryListeners) { - listenter.onDeviceUpdated(this, device); - } - } - else { - handleDeviceAdd(device); - } - } - else { - compatibleDevices.remove(device.getIpAddress()); - handleDeviceLoss(device); - } - } - - public void handleDeviceLoss(ConnectableDevice device) { - for (DiscoveryManagerListener listenter: discoveryListeners) { - listenter.onDeviceRemoved(this, device); - } - - device.disconnect(); - } - - public boolean isNetcast(ServiceDescription description) { - boolean isNetcastTV = false; - - String modelName = description.getModelName(); - String modelDescription = description.getModelDescription(); - - if (modelName != null && modelName.toUpperCase(Locale.US).equals("LG TV")) { - if (modelDescription != null && !(modelDescription.toUpperCase().contains("WEBOS"))) { - if (description.getServiceID().equals(NetcastTVService.ID)); { - isNetcastTV = true; - } - } - } - - return isNetcastTV; - } - - public boolean isSamsungMultiScreen(ServiceDescription description) { - boolean isSamsungMultiScreen = false; - - String locationXML = description.getLocationXML(); - - if (locationXML != null && (locationXML.contains("samsung:multiscreen:1"))) { - isSamsungMultiScreen = true; - } - - return isSamsungMultiScreen; - } - // @endcond - - /** - * List of all devices discovered by DiscoveryManager. Each ConnectableDevice object is keyed against its current IP address. - */ - public Map getAllDevices() { - return allDevices; - } - - /** - * Filtered list of discovered ConnectableDevices, limited to devices that match at least one of the CapabilityFilters in the capabilityFilters array. Each ConnectableDevice object is keyed against its current IP address. - */ - public Map getCompatibleDevices() { - return compatibleDevices; - } - - /** - * The pairingLevel property determines whether capabilities that require pairing (such as entering a PIN) will be available. - * - * If pairingLevel is set to ConnectableDevicePairingLevelOn, ConnectableDevices that require pairing will prompt the user to pair when connecting to the ConnectableDevice. - * - * If pairingLevel is set to ConnectableDevicePairingLevelOff (the default), connecting to the device will avoid requiring pairing if possible but some capabilities may not be available. - */ - public PairingLevel getPairingLevel() { - return pairingLevel; - } - - /** - * The pairingLevel property determines whether capabilities that require pairing (such as entering a PIN) will be available. - * - * If pairingLevel is set to ConnectableDevicePairingLevelOn, ConnectableDevices that require pairing will prompt the user to pair when connecting to the ConnectableDevice. - * - * If pairingLevel is set to ConnectableDevicePairingLevelOff (the default), connecting to the device will avoid requiring pairing if possible but some capabilities may not be available. - */ - public void setPairingLevel(PairingLevel pairingLevel) { - this.pairingLevel = pairingLevel; - } - - // @cond INTERNAL - public Context getContext() { - return context; - } - - public void onDestroy() { - unregisterBroadcastReceiver(); - } - - @Override - public void onServiceConfigUpdate(ServiceConfig serviceConfig) { - - } - - @Override - public void onCapabilityUpdated(ConnectableDevice device, List added, List removed) { - handleDeviceUpdate(device); - } - - @Override public void onConnectionFailed(ConnectableDevice device, ServiceCommandError error) { } - @Override public void onDeviceDisconnected(ConnectableDevice device) { } - @Override public void onDeviceReady(ConnectableDevice device) { } - @Override public void onPairingRequired(ConnectableDevice device, DeviceService service, PairingType pairingType) { } - - @Override - public void onServiceAdded(DiscoveryProvider provider, ServiceDescription serviceDescription) { - Log.d("Connect SDK", "Service added: " + serviceDescription.getFriendlyName() + " (" + serviceDescription.getServiceID() + ")"); - - boolean deviceIsNew = !allDevices.containsKey(serviceDescription.getIpAddress()); - ConnectableDevice device = null; - - if (deviceIsNew) { - if (connectableDeviceStore != null) { - device = connectableDeviceStore.getDevice(serviceDescription.getUUID()); - - if (device != null) { - allDevices.put(serviceDescription.getIpAddress(), device); - device.setIpAddress(serviceDescription.getIpAddress()); - } - } - } else { - device = allDevices.get(serviceDescription.getIpAddress()); - } - - if (device == null) { - device = new ConnectableDevice(serviceDescription); - device.setIpAddress(serviceDescription.getIpAddress()); - allDevices.put(serviceDescription.getIpAddress(), device); - deviceIsNew = true; - } - - device.setFriendlyName(serviceDescription.getFriendlyName()); - device.setLastDetection(Util.getTime()); - device.setLastKnownIPAddress(serviceDescription.getIpAddress()); - // TODO: Implement the currentSSID Property in DiscoveryManager -// device.setLastSeenOnWifi(currentSSID); - - addServiceDescriptionToDevice(serviceDescription, device); - - if (device.getServices().size() == 0) { - // we get here when a non-LG DLNA TV is found - - allDevices.remove(serviceDescription.getIpAddress()); - device = null; - - return; - } - - if (deviceIsNew) - handleDeviceAdd(device); - else - handleDeviceUpdate(device); - } - - @Override - public void onServiceRemoved(DiscoveryProvider provider, ServiceDescription serviceDescription) { - if (serviceDescription == null) { - Log.w("Connect SDK", "onServiceRemoved: unknown service description"); - Log.w("Connect SDK", Thread.currentThread().getStackTrace().toString()); - - return; - } - - Log.d("Connect SDK", "onServiceRemoved: friendlyName: " + serviceDescription.getFriendlyName()); - - ConnectableDevice device = allDevices.get(serviceDescription.getIpAddress()); - - if (device != null) { - device.removeServiceWithId(serviceDescription.getServiceID()); - - if (device.getServices().isEmpty()) { - allDevices.remove(serviceDescription.getIpAddress()); - - handleDeviceLoss(device); - } - else { - handleDeviceUpdate(device); - } - } - } - - @Override - public void onServiceDiscoveryFailed(DiscoveryProvider provider, ServiceCommandError error) { - Log.w("Connect SDK", "DiscoveryProviderListener, Service Discovery Failed"); - } - - @SuppressWarnings("unchecked") - public void addServiceDescriptionToDevice(ServiceDescription desc, ConnectableDevice device) { - Log.d("Connect SDK", "Adding service " + desc.getServiceID() + " to device with address " + device.getIpAddress() + " and id " + device.getId()); - - Class deviceServiceClass = (Class) deviceClasses.get(desc.getServiceID()); - - if (deviceServiceClass == DLNAService.class) { - if (desc.getLocationXML() == null) - return; - - // we only support LG DLNA devices, currently - if (!desc.getLocationXML().contains("LG")) - return; - } else if (deviceServiceClass == NetcastTVService.class) { - if (!isNetcast(desc)) - return; - } else if (deviceServiceClass.getSimpleName().equals("MultiScreenService")){ - if (!isSamsungMultiScreen(desc)) - return; - } - - if (deviceServiceClass == null) - return; - - ServiceConfig serviceConfig = null; - - if (connectableDeviceStore != null) - serviceConfig = connectableDeviceStore.getServiceConfig(desc.getUUID()); - - if (serviceConfig == null) - serviceConfig = new ServiceConfig(desc); - - serviceConfig.setListener(DiscoveryManager.this); - - boolean hasType = false; - boolean hasService = false; - - for (DeviceService service : device.getServices()) { - if (service.getServiceDescription().getServiceID().equals(desc.getServiceID())) { - hasType = true; - if (service.getServiceDescription().getUUID().equals(desc.getUUID())) { - hasService = true; - } - break; - } - } - - if (hasType) { - if (hasService) { - device.setServiceDescription(desc); - - DeviceService alreadyAddedService = device.getServiceByName(desc.getServiceID()); - - if (alreadyAddedService != null) - alreadyAddedService.setServiceDescription(desc); - - return; - } - - device.removeServiceByName(desc.getServiceID()); - } - - DeviceService deviceService = DeviceService.getService(deviceServiceClass, desc, serviceConfig); - deviceService.setServiceDescription(desc); - device.addService(deviceService); - } - // @endcond -} diff --git a/src/com/connectsdk/discovery/DiscoveryManagerListener.java b/src/com/connectsdk/discovery/DiscoveryManagerListener.java deleted file mode 100644 index 2e98e65f..00000000 --- a/src/com/connectsdk/discovery/DiscoveryManagerListener.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * DiscoveryManagerListener - * 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.discovery; - -import com.connectsdk.device.ConnectableDevice; -import com.connectsdk.service.command.ServiceCommandError; - -/** - * ###Overview - * The DiscoveryManagerListener will receive events on the addition/removal/update of ConnectableDevice objects. - * - * ###In Depth - * It is important to note that, unless you are implementing your own device picker, this listener is not needed in your code. Connect SDK's DevicePicker internally acts a separate listener to the DiscoveryManager and handles all of the same method calls. - */ -public interface DiscoveryManagerListener { - - /** - * This method will be fired upon the first discovery of one of a ConnectableDevice's DeviceServices. - * - * @param manager DiscoveryManager that found device - * @param device ConnectableDevice that was found - */ - public void onDeviceAdded(DiscoveryManager manager, ConnectableDevice device); - - /** - * This method is called when a ConnectableDevice gains or loses a DeviceService in discovery. - * - * @param manager DiscoveryManager that updated device - * @param device ConnectableDevice that was updated - */ - public void onDeviceUpdated(DiscoveryManager manager, ConnectableDevice device); - - /** - * This method is called when connections to all of a ConnectableDevice's DeviceServices are lost. This will usually happen when a device is powered off or loses internet connectivity. - * - * @param manager DiscoveryManager that lost device - * @param device ConnectableDevice that was lost - */ - public void onDeviceRemoved(DiscoveryManager manager, ConnectableDevice device); - - /** - * In the event of an error in the discovery phase, this method will be called. - * - * @param manager DiscoveryManager that experienced the error - * @param error NSError with a description of the failure - */ - public void onDiscoveryFailed(DiscoveryManager manager, ServiceCommandError error); -} diff --git a/src/com/connectsdk/discovery/DiscoveryProvider.java b/src/com/connectsdk/discovery/DiscoveryProvider.java deleted file mode 100644 index 80da7e3f..00000000 --- a/src/com/connectsdk/discovery/DiscoveryProvider.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * DiscoveryProvider - * 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.discovery; - -import org.json.JSONObject; - -/** - * ###Overview - * From a high-level perspective, DiscoveryProvider completely abstracts the functionality of discovering services of a particular protocol (SSDP, Cast, etc). The DiscoveryProvider will pass service information to the DiscoveryManager, which will then create a DeviceService object and attach it to a ConnectableDevice object. - * - * ###In Depth - * DiscoveryProvider is an abstract class that is meant to be extended. You shouldn't ever use DiscoveryProvider directly, unless extending it to provide support for another discovery protocol. - * - * By default, DiscoveryManager will set itself as a DiscoveryProvider's listener. You should not change the listener as it could cause unexpected inconsistencies within the discovery process. - * - * See CastDiscoveryProvider and SSDPDiscoveryProvider for implementations. - */ -public interface DiscoveryProvider { - - /** - * Starts the DiscoveryProvider. - */ - public void start(); - - /** - * Stops the DiscoveryProvider. - */ - public void stop(); - - /** - * Resets the DiscoveryProvider. - */ - public void reset(); - - /** Adds a DiscoveryProviderListener, which should be the DiscoveryManager */ - public void addListener(DiscoveryProviderListener listener); - - /** Removes a DiscoveryProviderListener. */ - public void removeListener(DiscoveryProviderListener listener); - - /** - * Adds a device filter for a particular DeviceService. - * - * @param parameters Parameters to be used for discovering a particular DeviceService - */ - public void addDeviceFilter(JSONObject parameters); - - /** - * Removes a device filter for a particular DeviceService. If the DiscoveryProvider has no other devices to be searching for, the DiscoveryProvider will be stopped and de-referenced. - * - * @param parameters Parameters to be used for discovering a particular DeviceService - */ - public void removeDeviceFilter(JSONObject parameters); - - /** - * Whether or not the DiscoveryProvider has any services it is supposed to be searching for. If YES, then the DiscoveryProvider will be stopped and de-referenced by the DiscoveryManager. - */ - public boolean isEmpty(); -} diff --git a/src/com/connectsdk/discovery/DiscoveryProviderListener.java b/src/com/connectsdk/discovery/DiscoveryProviderListener.java deleted file mode 100644 index e1a5368b..00000000 --- a/src/com/connectsdk/discovery/DiscoveryProviderListener.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * DiscoveryProviderListener - * 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.discovery; - -import com.connectsdk.service.command.ServiceCommandError; -import com.connectsdk.service.config.ServiceDescription; - -/** - * The DiscoveryProviderListener is mechanism for passing service information to the DiscoveryManager. You likely will not be using the DiscoveryProviderListener class directly, as DiscoveryManager acts as a listener to all of the DiscoveryProviders. - */ -public interface DiscoveryProviderListener { - - /** - * This method is called when the DiscoveryProvider discovers a service that matches one of its DeviceService filters. The ServiceDescription is created and passed to the listener (which should be the DiscoveryManager). The ServiceDescription is used to create a DeviceService, which is then attached to a ConnectableDevice object. - * - * @param provider DiscoveryProvider that found the service - * @param description ServiceDescription of the service that was found - */ - public void onServiceAdded(DiscoveryProvider provider, ServiceDescription serviceDescription); - - /** - * This method is called when the DiscoveryProvider's internal mechanism loses reference to a service that matches one of its DeviceService filters. - * - * @param provider DiscoveryProvider that lost the service - * @param description ServiceDescription of the service that was lost - */ - public void onServiceRemoved(DiscoveryProvider provider, ServiceDescription serviceDescription); - - /** - * This method is called on any error/failure within the DiscoveryProvider. - * - * @param provider DiscoveryProvider that failed - * @param error ServiceCommandError providing a information about the failure - */ - public void onServiceDiscoveryFailed(DiscoveryProvider provider, ServiceCommandError error); -} diff --git a/src/com/connectsdk/discovery/provider/SSDPDiscoveryProvider.java b/src/com/connectsdk/discovery/provider/SSDPDiscoveryProvider.java deleted file mode 100644 index 78a13ca4..00000000 --- a/src/com/connectsdk/discovery/provider/SSDPDiscoveryProvider.java +++ /dev/null @@ -1,527 +0,0 @@ -/* - * SSDPDiscoveryProvider - * 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.discovery.provider; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.json.JSONException; -import org.json.JSONObject; - -import android.content.Context; -import android.util.Log; - -import com.connectsdk.core.Util; -import com.connectsdk.core.upnp.Device; -import com.connectsdk.core.upnp.ssdp.SSDP; -import com.connectsdk.core.upnp.ssdp.SSDP.ParsedDatagram; -import com.connectsdk.core.upnp.ssdp.SSDPSearchMsg; -import com.connectsdk.core.upnp.ssdp.SSDPSocket; -import com.connectsdk.discovery.DiscoveryProvider; -import com.connectsdk.discovery.DiscoveryProviderListener; -import com.connectsdk.service.config.ServiceDescription; - -public class SSDPDiscoveryProvider implements DiscoveryProvider { - Context context; - - private final static int RESCAN_INTERVAL = 10000; - private final static int RESCAN_ATTEMPTS = 3; - private final static int SSDP_TIMEOUT = RESCAN_INTERVAL * RESCAN_ATTEMPTS; - - boolean needToStartSearch = false; - - private CopyOnWriteArrayList serviceListeners; - - private ConcurrentHashMap foundServices = new ConcurrentHashMap(); - private ConcurrentHashMap discoveredServices = new ConcurrentHashMap(); - - List serviceFilters; - - private SSDPSocket mSSDPSocket; - - private Timer dataTimer; - - private Pattern uuidReg; - - private Thread responseThread; - private Thread notifyThread; - - public SSDPDiscoveryProvider(Context context) { - this.context = context; - - uuidReg = Pattern.compile("(?<=uuid:)(.+?)(?=(::)|$)"); - - serviceListeners = new CopyOnWriteArrayList(); - serviceFilters = new CopyOnWriteArrayList(); - } - - private void openSocket() { - if (mSSDPSocket != null && mSSDPSocket.isConnected()) - return; - - try { - InetAddress source = Util.getIpAddress(context); - if (source == null) - return; - - mSSDPSocket = new SSDPSocket(source); - } catch (UnknownHostException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public void start() { - stop(); - - openSocket(); - - dataTimer = new Timer(); - dataTimer.schedule(new TimerTask() { - - @Override - public void run() { - sendSearch(); - } - }, 100, RESCAN_INTERVAL); - - responseThread = new Thread(mResponseHandler); - notifyThread = new Thread(mRespNotifyHandler); - - responseThread.start(); - notifyThread.start(); - } - - public void sendSearch() { - List killKeys = new ArrayList(); - - long killPoint = new Date().getTime() - SSDP_TIMEOUT; - - for (String key : foundServices.keySet()) { - ServiceDescription service = foundServices.get(key); - if (service == null || service.getLastDetection() < killPoint) { - killKeys.add(key); - } - } - - for (String key : killKeys) { - final ServiceDescription service = foundServices.get(key); - - if (service != null) { - notifyListenersOfLostService(service); - } - - if (foundServices.containsKey(key)) - foundServices.remove(key); - } - - for (JSONObject searchTarget : serviceFilters) { - SSDPSearchMsg search = null; - try { - search = new SSDPSearchMsg(searchTarget.getString("filter")); - } catch (JSONException e) { - e.printStackTrace(); - return; - } - - final String message = search.toString(); - - Timer timer = new Timer(); - /* Send 3 times like WindowsMedia */ - for (int i = 0; i < 3; i++) { - TimerTask task = new TimerTask() { - - @Override - public void run() { - try { - if (mSSDPSocket != null) - mSSDPSocket.send(message); - } catch (IOException e) { - e.printStackTrace(); - } - } - }; - - timer.schedule(task, i * 1000); - } - }; - } - - @Override - public void stop() { - if (dataTimer != null) { - dataTimer.cancel(); - } - - if (responseThread != null) { - responseThread.interrupt(); - } - - if (notifyThread != null) { - notifyThread.interrupt(); - } - - if (mSSDPSocket != null) { - mSSDPSocket.close(); - mSSDPSocket = null; - } - } - - @Override - public void reset() { - stop(); - foundServices.clear(); - discoveredServices.clear(); - } - - @Override - public void addDeviceFilter(JSONObject parameters) { - if ( !parameters.has("filter") ) { - Log.e("Connect SDK", "This device filter does not have ssdp filter info"); - } else { -// String newFilter = null; -// try { -// newFilter = parameters.getString("filter"); -// for ( int i = 0; i < serviceFilters.size(); i++) { -// String filter = serviceFilters.get(i).getString("filter"); -// -// if ( newFilter.equals(filter) ) -// return; -// } -// } catch (JSONException e) { -// e.printStackTrace(); -// } - - serviceFilters.add(parameters); - -// if ( newFilter != null ) -// controlPoint.addFilter(newFilter); - } - } - - @Override - public void removeDeviceFilter(JSONObject parameters) { - String removalServiceId; - boolean shouldRemove = false; - int removalIndex = -1; - - try { - removalServiceId = parameters.getString("serviceId"); - - for (int i = 0; i < serviceFilters.size(); i++) { - JSONObject serviceFilter = serviceFilters.get(i); - String serviceId = (String) serviceFilter.get("serviceId"); - - if ( serviceId.equals(removalServiceId) ) { - shouldRemove = true; - removalIndex = i; - break; - } - } - } catch (JSONException e) { - e.printStackTrace(); - } - - if ( shouldRemove ) { - serviceFilters.remove(removalIndex); - } - } - - @Override - public boolean isEmpty() { - return serviceFilters.size() == 0; - } - - private Runnable mResponseHandler = new Runnable() { - @Override - public void run() { - while (mSSDPSocket != null) { - try { - handleDatagramPacket(SSDP.convertDatagram(mSSDPSocket.responseReceive())); - } catch (IOException e) { - e.printStackTrace(); - break; - } - } - } - }; - - private Runnable mRespNotifyHandler = new Runnable() { - @Override - public void run() { - while (mSSDPSocket != null) { - try { - handleDatagramPacket(SSDP.convertDatagram(mSSDPSocket.notifyReceive())); - } catch (IOException e) { - e.printStackTrace(); - break; - } - } - } - }; - - private void handleDatagramPacket(final ParsedDatagram pd) { - // Debugging stuff -// Util.runOnUI(new Runnable() { -// -// @Override -// public void run() { -// Log.d("Connect SDK Socket", "Packet received | type = " + pd.type); -// -// for (String key : pd.data.keySet()) { -// Log.d("Connect SDK Socket", " " + key + " = " + pd.data.get(key)); -// } -// Log.d("Connect SDK Socket", "__________________________________________"); -// } -// }); - // End Debugging stuff - - String serviceFilter = pd.data.get(pd.type.equals(SSDP.SL_NOTIFY) ? SSDP.NT : SSDP.ST); - - if (serviceFilter == null || SSDP.SL_MSEARCH.equals(pd.type) || !isSearchingForFilter(serviceFilter)) - return; - - String usnKey = pd.data.get(SSDP.USN); - - if (usnKey == null || usnKey.length() == 0) - return; - - Matcher m = uuidReg.matcher(usnKey); - - if (!m.find()) - return; - - String uuid = m.group(); - - if (SSDP.NTS_BYEBYE.equals(pd.data.get(SSDP.NTS))) { - final ServiceDescription service = foundServices.get(uuid); - - if (service != null) { - notifyListenersOfLostService(service); - } - } else { - String location = pd.data.get(SSDP.LOCATION); - - if (location == null || location.length() == 0) - return; - - ServiceDescription foundService = foundServices.get(uuid); - ServiceDescription discoverdService = discoveredServices.get(uuid); - - boolean isNew = foundService == null && discoverdService == null; - - if (isNew) { - foundService = new ServiceDescription(); - foundService.setUUID(uuid); - foundService.setServiceFilter(serviceFilter); - foundService.setIpAddress(pd.dp.getAddress().getHostAddress()); - foundService.setPort(3001); - - discoveredServices.put(uuid, foundService); - - getLocationData(location, uuid, serviceFilter); - } - - if (foundService != null) - foundService.setLastDetection(new Date().getTime()); - } - -// for (JSONObject filterObj : serviceFilters) { -// String filter = null; -// try { -// filter = filterObj.getString("filter"); -// } catch (JSONException e) { -// e.printStackTrace(); -// continue; -// } -// if (filter.indexOf(datagramFilter) != -1) { -// skip = false; -// break; -// } -// } -// -// if (skip) -// return; -// -// if (SSDP.SL_OK.equals(pd.type)) { -// handleRespMsg(pd); -// } else if (SSDP.SL_NOTIFY.equals(pd.type)) { -// handleNotifyMsg(pd); -// } - } - - public void getLocationData(final String location, final String uuid, final String serviceFilter) { - Util.runInBackground(new Runnable() { - - @Override - public void run() { - Device device = Device.createInstanceFromXML(location, serviceFilter); - - if (device != null) { - if (true) {//device.friendlyName != null) { - device.UUID = uuid; - boolean hasServices = containsServicesWithFilter(device, serviceFilter); - - if (hasServices) { - final ServiceDescription service = discoveredServices.get(uuid); - - if (service != null) { - service.setServiceFilter(serviceFilter); - service.setFriendlyName(device.friendlyName); - service.setModelName(device.modelName); - service.setModelNumber(device.modelNumber); - service.setModelDescription(device.modelDescription); - service.setManufacturer(device.manufacturer); - service.setApplicationURL(device.applicationURL); - service.setServiceList(device.serviceList); - service.setResponseHeaders(device.headers); - service.setLocationXML(device.locationXML); - service.setServiceURI(device.serviceURI); - - foundServices.put(uuid, service); - - notifyListenersOfNewService(service); - } - } - } - } - - discoveredServices.remove(uuid); - } - }, true); - - } - - private void notifyListenersOfNewService(ServiceDescription service) { - List serviceIds = serviceIdsForFilter(service.getServiceFilter()); - - for (String serviceId : serviceIds) { - ServiceDescription _newService = service.clone(); - _newService.setServiceID(serviceId); - - final ServiceDescription newService = _newService; - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - - for (DiscoveryProviderListener listener : serviceListeners) { - listener.onServiceAdded(SSDPDiscoveryProvider.this, newService); - } - } - }); - } - } - - private void notifyListenersOfLostService(ServiceDescription service) { - List serviceIds = serviceIdsForFilter(service.getServiceFilter()); - - for (String serviceId : serviceIds) { - ServiceDescription _newService = service.clone(); - _newService.setServiceID(serviceId); - - final ServiceDescription newService = _newService; - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - for (DiscoveryProviderListener listener : serviceListeners) { - listener.onServiceRemoved(SSDPDiscoveryProvider.this, newService); - } - } - }); - } - } - - public List serviceIdsForFilter(String filter) { - ArrayList serviceIds = new ArrayList(); - - for (JSONObject serviceFilter : serviceFilters) { - String ssdpFilter; - try { - ssdpFilter = serviceFilter.getString("filter"); - - if (ssdpFilter.equals(filter)) { - String serviceId = serviceFilter.getString("serviceId"); - - if (serviceId != null) - serviceIds.add(serviceId); - } - } catch (JSONException e) { - e.printStackTrace(); - continue; - } - } - - return serviceIds; - } - - public boolean isSearchingForFilter(String filter) { - for (JSONObject serviceFilter : serviceFilters) { - try { - String ssdpFilter = serviceFilter.getString("filter"); - - if (ssdpFilter.equals(filter)) - return true; - } catch (JSONException e) { - e.printStackTrace(); - continue; - } - } - - return false; - } - - public boolean containsServicesWithFilter(Device device, String filter) { -// List servicesRequired = new ArrayList(); -// -// for (JSONObject serviceFilter : serviceFilters) { -// } - - // TODO Implement this method. Not sure why needs to happen since there are now required services. - - return true; - } - - @Override - public void addListener(DiscoveryProviderListener listener) { - serviceListeners.add(listener); - } - - @Override - public void removeListener(DiscoveryProviderListener listener) { - serviceListeners.remove(listener); - } -} diff --git a/src/com/connectsdk/discovery/provider/ZeroconfDiscoveryProvider.java b/src/com/connectsdk/discovery/provider/ZeroconfDiscoveryProvider.java deleted file mode 100644 index 2fe4d983..00000000 --- a/src/com/connectsdk/discovery/provider/ZeroconfDiscoveryProvider.java +++ /dev/null @@ -1,314 +0,0 @@ -/* - * ZeroconfDiscoveryProvider - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Hyun Kook Khang on 18 Apr 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.discovery.provider; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; - -import javax.jmdns.JmDNS; -import javax.jmdns.ServiceEvent; -import javax.jmdns.ServiceListener; - -import org.json.JSONException; -import org.json.JSONObject; - -import android.content.Context; -import android.util.Log; - -import com.connectsdk.core.Util; -import com.connectsdk.discovery.DiscoveryProvider; -import com.connectsdk.discovery.DiscoveryProviderListener; -import com.connectsdk.service.config.ServiceDescription; - -public class ZeroconfDiscoveryProvider implements DiscoveryProvider { - private static final String HOSTNAME = "connectsdk"; - - JmDNS jmdns; - InetAddress srcAddress; - - private final static int RESCAN_INTERVAL = 10000; - private final static int RESCAN_ATTEMPTS = 3; - private final static int TIMEOUT = RESCAN_INTERVAL * RESCAN_ATTEMPTS; - - private Timer dataTimer; - - List serviceFilters; - - private ConcurrentHashMap foundServices; - private CopyOnWriteArrayList serviceListeners; - - ServiceListener jmdnsListener = new ServiceListener() { - - @Override - public void serviceResolved(ServiceEvent ev) { - @SuppressWarnings("deprecation") - String ipAddress = ev.getInfo().getHostAddress(); - if (!Util.isIPv4Address(ipAddress)) { - // Currently, we only support ipv4 - return; - } - - String uuid = ipAddress; - String friendlyName = ev.getInfo().getName(); - int port = ev.getInfo().getPort(); - - ServiceDescription foundService = foundServices.get(uuid); - - boolean isNew = foundService == null; - boolean listUpdateFlag = false; - - if (isNew) { - foundService = new ServiceDescription(); - foundService.setUUID(uuid); - foundService.setServiceFilter(ev.getInfo().getType()); - foundService.setIpAddress(ipAddress); - foundService.setServiceID(serviceIdForFilter(ev.getInfo().getType())); - foundService.setPort(port); - foundService.setFriendlyName(friendlyName); - - listUpdateFlag = true; - } - else { - if (!foundService.getFriendlyName().equals(friendlyName)) { - foundService.setFriendlyName(friendlyName); - listUpdateFlag = true; - } - } - - if (foundService != null) - foundService.setLastDetection(new Date().getTime()); - - foundServices.put(uuid, foundService); - - if (listUpdateFlag) { - for ( DiscoveryProviderListener listener: serviceListeners) { - listener.onServiceAdded(ZeroconfDiscoveryProvider.this, foundService); - } - } - } - - @Override - public void serviceRemoved(ServiceEvent ev) { - @SuppressWarnings("deprecation") - String uuid = ev.getInfo().getHostAddress(); - final ServiceDescription service = foundServices.get(uuid); - - if (service != null) { - Util.runOnUI(new Runnable() { - - @Override - public void run() { - for (DiscoveryProviderListener listener : serviceListeners) { - listener.onServiceRemoved(ZeroconfDiscoveryProvider.this, service); - } - } - }); - } - } - - @Override - public void serviceAdded(ServiceEvent event) { - // Required to force serviceResolved to be called again - // (after the first search) - jmdns.requestServiceInfo(event.getType(), event.getName(), 1); - } - }; - - public ZeroconfDiscoveryProvider(Context context) { - foundServices = new ConcurrentHashMap(8, 0.75f, 2); - - serviceListeners = new CopyOnWriteArrayList(); - serviceFilters = new CopyOnWriteArrayList(); - - try { - srcAddress = Util.getIpAddress(context); - } catch (UnknownHostException e) { - e.printStackTrace(); - } - } - - @Override - public void start() { - stop(); - - dataTimer = new Timer(); - MDNSSearchTask sendSearch = new MDNSSearchTask(); - dataTimer.schedule(sendSearch, 100, RESCAN_INTERVAL); - } - - private class MDNSSearchTask extends TimerTask { - - @Override - public void run() { - List killKeys = new ArrayList(); - - long killPoint = new Date().getTime() - TIMEOUT; - - for (String key : foundServices.keySet()) { - ServiceDescription service = foundServices.get(key); - if (service == null || service.getLastDetection() < killPoint) { - killKeys.add(key); - } - } - - for (String key : killKeys) { - final ServiceDescription service = foundServices.get(key); - - if (service != null) { - Util.runOnUI(new Runnable() { - - @Override - public void run() { - for (DiscoveryProviderListener listener : serviceListeners) { - listener.onServiceRemoved(ZeroconfDiscoveryProvider.this, service); - } - } - }); - } - - if (foundServices.containsKey(key)) - foundServices.remove(key); - } - - if (srcAddress != null) { - try { - if (jmdns != null) { - jmdns.close(); - } - jmdns = JmDNS.create(srcAddress, HOSTNAME); - - for (JSONObject searchTarget : serviceFilters) { - try { - String filter = searchTarget.getString("filter"); - jmdns.addServiceListener(filter, jmdnsListener); - } catch (JSONException e) { - e.printStackTrace(); - } - }; - } catch (IOException e) { - e.printStackTrace(); - } - } - } - } - - @Override - public void stop() { - if (dataTimer != null) { - dataTimer.cancel(); - } - - if (jmdns != null) { - for (JSONObject searchTarget : serviceFilters) { - try { - String filter = searchTarget.getString("filter"); - jmdns.removeServiceListener(filter, jmdnsListener); - } catch (JSONException e) { - e.printStackTrace(); - } - }; - } - } - - @Override - public void reset() { - foundServices.clear(); - } - - @Override - public void addListener(DiscoveryProviderListener listener) { - serviceListeners.add(listener); - } - - @Override - public void removeListener(DiscoveryProviderListener listener) { - serviceListeners.remove(listener); - } - - @Override - public void addDeviceFilter(JSONObject parameters) { - if (!parameters.has("filter")) { - Log.e("Connect SDK", "This device filter does not have zeroconf filter info"); - } else { - serviceFilters.add(parameters); - } - } - - @Override - public void removeDeviceFilter(JSONObject parameters) { - String removalServiceId; - boolean shouldRemove = false; - int removalIndex = -1; - - try { - removalServiceId = parameters.getString("serviceId"); - - for (int i = 0; i < serviceFilters.size(); i++) { - JSONObject serviceFilter = serviceFilters.get(i); - String serviceId = (String) serviceFilter.get("serviceId"); - - if ( serviceId.equals(removalServiceId) ) { - shouldRemove = true; - removalIndex = i; - break; - } - } - } catch (JSONException e) { - e.printStackTrace(); - } - - if (shouldRemove) { - serviceFilters.remove(removalIndex); - } - } - - @Override - public boolean isEmpty() { - return serviceFilters.size() == 0; - } - - public String serviceIdForFilter(String filter) { - String serviceId = ""; - - for (JSONObject serviceFilter : serviceFilters) { - String ssdpFilter; - try { - ssdpFilter = serviceFilter.getString("filter"); - if (ssdpFilter.equals(filter)) { - return serviceFilter.getString("serviceId"); - } - } catch (JSONException e) { - e.printStackTrace(); - continue; - } - } - - return serviceId; - } -} diff --git a/src/com/connectsdk/etc/helper/DeviceServiceReachability.java b/src/com/connectsdk/etc/helper/DeviceServiceReachability.java deleted file mode 100644 index ad5be8d5..00000000 --- a/src/com/connectsdk/etc/helper/DeviceServiceReachability.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * DeviceServiceReachability - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Jeffrey Glenn on 16 Apr 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.etc.helper; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; - -public class DeviceServiceReachability { - private static final int TIMEOUT = 10000; - private InetAddress ipAddress; - private Thread testThread; - - private DeviceServiceReachabilityListener listener; - - public DeviceServiceReachability() { } - - public DeviceServiceReachability(InetAddress ipAddress) { - this.ipAddress = ipAddress; - } - - public DeviceServiceReachability(InetAddress ipAddress, DeviceServiceReachabilityListener listener) { - this.ipAddress = ipAddress; - this.listener = listener; - } - - public static DeviceServiceReachability getReachability(InetAddress ipAddress, DeviceServiceReachabilityListener listener) { - return new DeviceServiceReachability(ipAddress, listener); - } - - public static DeviceServiceReachability getReachability(final String ipAddress, DeviceServiceReachabilityListener listener) { - InetAddress addr; - try { - addr = InetAddress.getByName(ipAddress); - } catch (UnknownHostException e) { - return null; - } - return getReachability(addr, listener); - } - - public InetAddress getIpAddress() { - return ipAddress; - } - - public void setIpAddress(InetAddress ipAddress) { - this.ipAddress = ipAddress; - } - - public boolean isRunning() { - return testThread != null && testThread.isAlive(); - } - - public DeviceServiceReachabilityListener getListener() { - return listener; - } - - public void setListener(DeviceServiceReachabilityListener listener) { - this.listener = listener; - } - - public void start() { - if (isRunning()) - return; - - testThread = new Thread(testReachability); - testThread.start(); - } - - public void stop() { - if (!isRunning()) - return; - - testThread.interrupt(); - testThread = null; - } - - private void unreachable() { - stop(); - - if (listener != null) - listener.onLoseReachability(this); - } - - private Runnable testReachability = new Runnable() { - - @Override - public void run() { - try { - while (true) { - if (!ipAddress.isReachable(TIMEOUT)) - unreachable(); - Thread.sleep(TIMEOUT); - } - } catch (IOException e) { - unreachable(); - } catch (InterruptedException e) { } - } - }; - - public interface DeviceServiceReachabilityListener { - public void onLoseReachability(DeviceServiceReachability reachability); - } -} diff --git a/src/com/connectsdk/etc/helper/HttpMessage.java b/src/com/connectsdk/etc/helper/HttpMessage.java deleted file mode 100644 index dd3eda10..00000000 --- a/src/com/connectsdk/etc/helper/HttpMessage.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * HttpMessage - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Hyun Kook Khang on 13 Feb 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.etc.helper; - -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.net.URLEncoder; - -import org.apache.http.client.methods.HttpDelete; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; - -public class HttpMessage { - public final static String CONTENT_TYPE_HEADER = "Content-Type"; - public final static String CONTENT_TYPE_TEXT_XML = "text/xml; charset=utf-8"; - public final static String CONTENT_TYPE_APPLICATION_PLIST = "application/x-apple-binary-plist"; - public final static String UDAP_USER_AGENT = "UDAP/2.0"; - public final static String LG_ELECTRONICS = "LG Electronics"; - public final static String USER_AGENT = "User-Agent"; - public final static String SOAP_ACTION = "\"urn:schemas-upnp-org:service:AVTransport:1#%s\""; - public final static String SOAP_HEADER = "Soapaction"; - - public static HttpPost getHttpPost(String uri) { - HttpPost post = null; - try { - post = new HttpPost(uri); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } - post.setHeader("Content-Type", CONTENT_TYPE_TEXT_XML); - - return post; - } - - public static HttpPost getUDAPHttpPost(String uri) { - HttpPost post = getHttpPost(uri); - post.setHeader("User-Agent", UDAP_USER_AGENT); - - return post; - } - - public static HttpPost getDLNAHttpPost(String uri, String action) { - String soapAction = "\"urn:schemas-upnp-org:service:AVTransport:1#" + action + "\""; - - HttpPost post = getHttpPost(uri); - post.setHeader("Soapaction", soapAction); - - return post; - } - - public static HttpGet getHttpGet(String url) { - return new HttpGet(url); - } - - public static HttpGet getUDAPHttpGet(String uri) { - HttpGet get = getHttpGet(uri); - get.setHeader("User-Agent", UDAP_USER_AGENT); - - return get; - } - - public static HttpDelete getHttpDelete(String url) { - return new HttpDelete(url); - } - - public static String encode(String str) { - try { - return URLEncoder.encode(str, "UTF-8"); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - return null; - } - - public static String decode(String str) { - try { - return URLDecoder.decode(str, "UTF-8"); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - return null; - } -} diff --git a/src/com/connectsdk/service/AirPlayService.java b/src/com/connectsdk/service/AirPlayService.java deleted file mode 100644 index e0e40c0d..00000000 --- a/src/com/connectsdk/service/AirPlayService.java +++ /dev/null @@ -1,648 +0,0 @@ -/* - * AirPlayService - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Hyun Kook Khang on 18 Apr 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 java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.StringTokenizer; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpRequestBase; -import org.apache.http.conn.ClientConnectionManager; -import org.apache.http.entity.ByteArrayEntity; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; -import org.apache.http.params.HttpParams; -import org.apache.http.util.EntityUtils; -import org.json.JSONException; -import org.json.JSONObject; - -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; - -import com.connectsdk.core.MediaInfo; -import com.connectsdk.core.Util; -import com.connectsdk.etc.helper.DeviceServiceReachability; -import com.connectsdk.etc.helper.HttpMessage; -import com.connectsdk.service.airplay.PListBuilder; -import com.connectsdk.service.capability.MediaControl; -import com.connectsdk.service.capability.MediaPlayer; -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.config.ServiceConfig; -import com.connectsdk.service.config.ServiceDescription; -import com.connectsdk.service.sessions.LaunchSession; -import com.connectsdk.service.sessions.LaunchSession.LaunchSessionType; - -public class AirPlayService extends DeviceService implements MediaPlayer, MediaControl { - - public static final String ID = "AirPlay"; - - interface PlaybackPositionListener { - void onGetPlaybackPositionSuccess(long duration, long position); - void onGetPlaybackPositionFailed(ServiceCommandError error); - } - - HttpClient httpClient; - - public AirPlayService(ServiceDescription serviceDescription, - ServiceConfig serviceConfig) { - super(serviceDescription, serviceConfig); - - httpClient = new DefaultHttpClient(); - ClientConnectionManager mgr = httpClient.getConnectionManager(); - HttpParams params = httpClient.getParams(); - httpClient = new DefaultHttpClient(new ThreadSafeClientConnManager(params, mgr.getSchemeRegistry()), params); - } - - public static JSONObject discoveryParameters() { - JSONObject params = new JSONObject(); - - try { - params.put("serviceId", ID); - params.put("filter", "_airplay._tcp.local."); - } catch (JSONException e) { - e.printStackTrace(); - } - - return params; - } - - @Override - public MediaControl getMediaControl() { - return this; - } - - @Override - public CapabilityPriorityLevel getMediaControlCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; - } - - @Override - public void play(ResponseListener listener) { - Map params = new HashMap(); - params.put("value", "1.000000"); - - String uri = getRequestURL("rate", params); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); - request.send(); - } - - @Override - public void pause(ResponseListener listener) { - Map params = new HashMap(); - params.put("value", "0.000000"); - - String uri = getRequestURL("rate", params); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); - request.send(); - } - - @Override - public void stop(ResponseListener listener) { - String uri = getRequestURL("stop"); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); - // TODO This is temp fix for issue https://github.com/ConnectSDK/Connect-SDK-Android/issues/66 - request.send(); - request.send(); - } - - @Override - public void rewind(ResponseListener listener) { - Map params = new HashMap(); - params.put("value", "-2.000000"); - - String uri = getRequestURL("rate", params); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); - request.send(); - } - - @Override - public void fastForward(ResponseListener listener) { - Map params = new HashMap(); - params.put("value", "2.000000"); - - String uri = getRequestURL("rate", params); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); - request.send(); - } - - @Override - public void seek(final long position, ResponseListener listener) { - float pos = ((float) position / 1000); - - Map params = new HashMap(); - params.put("position", String.valueOf(pos)); - - String uri = getRequestURL("scrub", params); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); - request.send(); - } - - @Override - public void getPosition(final PositionListener listener) { - getPlaybackPosition(new PlaybackPositionListener() { - - @Override - public void onGetPlaybackPositionSuccess(long duration, long position) { - Util.postSuccess(listener, position); - } - - @Override - public void onGetPlaybackPositionFailed(ServiceCommandError error) { - Util.postError(listener, new ServiceCommandError(0, "Unable to get position", null)); - } - }); - } - - @Override - public void getPlayState(final PlayStateListener listener) { - getPlaybackInfo(new ResponseListener() { - - @Override - public void onSuccess(Object object) { - // TODO need to handle play state -// Util.postSuccess(listener, object); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - } - - @Override - public void getDuration(final DurationListener listener) { - getPlaybackPosition(new PlaybackPositionListener() { - - @Override - public void onGetPlaybackPositionSuccess(long duration, long position) { - Util.postSuccess(listener, duration); - } - - @Override - public void onGetPlaybackPositionFailed(ServiceCommandError error) { - Util.postError(listener, new ServiceCommandError(0, "Unable to get duration", null)); - } - }); - } - - private void getPlaybackPosition(final PlaybackPositionListener listener) { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - String strResponse = (String) response; - - long duration = 0; - long position = 0; - - StringTokenizer st = new StringTokenizer(strResponse); - while (st.hasMoreTokens()) { - String str = st.nextToken(); - String value; - if (str.contains("duration")) { - value = st.nextToken(); - float f = Float.valueOf(value); - duration = (long) f * 1000; - } - else if (str.contains("position")) { - value = st.nextToken(); - float f = Float.valueOf(value); - position = (long) f * 1000; - } - } - - if (listener != null) { - listener.onGetPlaybackPositionSuccess(duration, position); - } - } - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) - listener.onGetPlaybackPositionFailed(error); - } - }; - - String uri = getRequestURL("scrub"); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, responseListener); - request.setHttpMethod(ServiceCommand.TYPE_GET); - request.send(); - } - - private void getPlaybackInfo(ResponseListener listener) { - String uri = getRequestURL("playback-info"); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, listener); - request.setHttpMethod(ServiceCommand.TYPE_GET); - request.send(); - } - - @Override - public ServiceSubscription subscribePlayState( - PlayStateListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - - return null; - } - - @Override - public MediaPlayer getMediaPlayer() { - return this; - } - - @Override - public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; - } - - @Override - public void displayImage(final String url, String mimeType, String title, - String description, String iconSrc, final LaunchListener listener) { - Util.runInBackground(new Runnable() { - - @Override - public void run() { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - LaunchSession launchSession = new LaunchSession(); - launchSession.setService(AirPlayService.this); - launchSession.setSessionType(LaunchSessionType.Media); - - Util.postSuccess(listener, new MediaLaunchObject(launchSession, AirPlayService.this)); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - String uri = getRequestURL("photo"); - HttpEntity entity = null; - - try { - URL imagePath = new URL(url); - HttpURLConnection connection = (HttpURLConnection) imagePath.openConnection(); - connection.setDoInput(true); - connection.connect(); - InputStream input = connection.getInputStream(); - Bitmap myBitmap = BitmapFactory.decodeStream(input); - - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - myBitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream); - - entity = new ByteArrayEntity(stream.toByteArray()); - } catch (MalformedURLException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - - ServiceCommand> request = new ServiceCommand>(AirPlayService.this, uri, entity, responseListener); - request.send(); - } - }); - } - @Override - public void displayImage(final MediaInfo mediaInfo, final LaunchListener listener) { - Util.runInBackground(new Runnable() { - - @Override - public void run() { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - LaunchSession launchSession = new LaunchSession(); - launchSession.setService(AirPlayService.this); - launchSession.setSessionType(LaunchSessionType.Media); - - Util.postSuccess(listener, new MediaLaunchObject(launchSession, AirPlayService.this)); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - String uri = getRequestURL("photo"); - HttpEntity entity = null; - - try { - URL imagePath = new URL(mediaInfo.getUrl()); - HttpURLConnection connection = (HttpURLConnection) imagePath.openConnection(); - connection.setDoInput(true); - connection.connect(); - InputStream input = connection.getInputStream(); - Bitmap myBitmap = BitmapFactory.decodeStream(input); - - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - myBitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream); - - entity = new ByteArrayEntity(stream.toByteArray()); - } catch (MalformedURLException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - - ServiceCommand> request = new ServiceCommand>(AirPlayService.this, uri, entity, responseListener); - request.send(); - } - }); - } - public void playVideo(final String url, String mimeType, String title, - String description, String iconSrc, boolean shouldLoop, - final LaunchListener listener) { - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - LaunchSession launchSession = new LaunchSession(); - launchSession.setService(AirPlayService.this); - launchSession.setSessionType(LaunchSessionType.Media); - - Util.postSuccess(listener, new MediaLaunchObject(launchSession, AirPlayService.this)); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - String uri = getRequestURL("play"); - HttpEntity entity = null; - - PListBuilder builder = new PListBuilder(); - builder.putString("Content-Location", url); - builder.putReal("Start-Position", 0); - - try { - entity = new StringEntity(builder.toString()); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - - ServiceCommand> request = new ServiceCommand>(this, uri, entity, responseListener); - request.send(); - } - - public void playVideo(final MediaInfo mediaInfo, boolean shouldLoop, - final LaunchListener listener) { - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - LaunchSession launchSession = new LaunchSession(); - launchSession.setService(AirPlayService.this); - launchSession.setSessionType(LaunchSessionType.Media); - - Util.postSuccess(listener, new MediaLaunchObject(launchSession, AirPlayService.this)); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - String uri = getRequestURL("play"); - HttpEntity entity = null; - - PListBuilder builder = new PListBuilder(); - builder.putString("Content-Location", mediaInfo.getUrl()); - builder.putReal("Start-Position", 0); - - try { - entity = new StringEntity(builder.toString()); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - - ServiceCommand> request = new ServiceCommand>(this, uri, entity, responseListener); - request.send(); - } - - @Override - public void playMedia(String url, String mimeType, String title, - String description, String iconSrc, boolean shouldLoop, - LaunchListener listener) { - - if ( mimeType.contains("image") ) { - displayImage(url, mimeType, title, description, iconSrc, listener); - } - else { - playVideo(url, mimeType, title, description, iconSrc, shouldLoop, listener); - } - } - - @Override - public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, - LaunchListener listener) { - - if ( mediaInfo.getMimeType().contains("image") ) { - displayImage(mediaInfo, listener); - } - else { - playVideo(mediaInfo, shouldLoop, listener); - } - } - - @Override - public void closeMedia(LaunchSession launchSession, - ResponseListener listener) { - - stop(listener); - } - - @Override - public void sendCommand(final ServiceCommand mCommand) { - Util.runInBackground(new Runnable() { - - @SuppressWarnings("unchecked") - @Override - public void run() { - final ServiceCommand> command = (ServiceCommand>) mCommand; - - Object payload = command.getPayload(); - - HttpRequestBase request = command.getRequest(); - request.addHeader(HttpMessage.CONTENT_TYPE_HEADER, HttpMessage.CONTENT_TYPE_APPLICATION_PLIST); - HttpResponse response = null; - - if (payload != null && command.getHttpMethod().equalsIgnoreCase(ServiceCommand.TYPE_POST)) { - HttpEntity entity = null; - - try { - if (payload instanceof String) { - entity = new StringEntity((String) payload); - } else if (payload instanceof JSONObject) { - entity = new StringEntity(((JSONObject) payload).toString()); - } else if (payload instanceof HttpEntity) { - entity = (HttpEntity)payload; - } - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - - ((HttpPost) request).setEntity(entity); - } - - try { - response = httpClient.execute(request); - - int code = response.getStatusLine().getStatusCode(); - - if (code == 200) { - HttpEntity entity = response.getEntity(); - String message = EntityUtils.toString(entity, "UTF-8"); - - Util.postSuccess(command.getResponseListener(), message); - } - else { - Util.postError(command.getResponseListener(), ServiceCommandError.getError(code)); - } - - response.getEntity().consumeContent(); - } catch (ClientProtocolException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } - }); - } - - @Override - protected void updateCapabilities() { - List capabilities = new ArrayList(); - - for (String capability : MediaPlayer.Capabilities) { capabilities.add(capability); } - - capabilities.add(Play); - capabilities.add(Pause); - capabilities.add(Stop); - capabilities.add(Position); - capabilities.add(Duration); - capabilities.add(PlayState); - capabilities.add(Seek); - capabilities.add(Rewind); - capabilities.add(FastForward); - - setCapabilities(capabilities); - } - - private String getRequestURL(String command) { - return getRequestURL(command, null); - } - - private String getRequestURL(String command, Map params) { - StringBuilder sb = new StringBuilder(); - sb.append("http://").append(serviceDescription.getIpAddress()); - sb.append(":").append(serviceDescription.getPort()); - sb.append("/").append(command); - - if (params != null) { - for (Map.Entry entry : params.entrySet()) { - String param = String.format("?%s=%s", entry.getKey(), entry.getValue()); - sb.append(param); - } - } - - return sb.toString(); - } - - @Override - public boolean isConnectable() { - return true; - } - - @Override - public boolean isConnected() { - return connected; - } - - @Override - public void connect() { - connected = true; - - reportConnected(true); - } - - @Override - public void disconnect() { - connected = false; - - if (mServiceReachability != null) - mServiceReachability.stop(); - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - if (listener != null) - listener.onDisconnect(AirPlayService.this, null); - } - }); - } - - @Override - public void onLoseReachability(DeviceServiceReachability reachability) { - if (connected) { - disconnect(); - } else { - mServiceReachability.stop(); - } - } - -} diff --git a/src/com/connectsdk/service/DIALService.java b/src/com/connectsdk/service/DIALService.java deleted file mode 100644 index ed47d40f..00000000 --- a/src/com/connectsdk/service/DIALService.java +++ /dev/null @@ -1,561 +0,0 @@ -/* - * DIALService - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Hyun Kook Khang on 24 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 java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpRequestBase; -import org.apache.http.conn.ClientConnectionManager; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; -import org.apache.http.params.HttpParams; -import org.apache.http.util.EntityUtils; -import org.json.JSONException; -import org.json.JSONObject; - -import android.util.Log; - -import com.connectsdk.core.AppInfo; -import com.connectsdk.core.Util; -import com.connectsdk.etc.helper.DeviceServiceReachability; -import com.connectsdk.etc.helper.HttpMessage; -import com.connectsdk.service.capability.Launcher; -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.command.NotSupportedServiceSubscription; -import com.connectsdk.service.command.ServiceCommand; -import com.connectsdk.service.command.ServiceCommandError; -import com.connectsdk.service.command.ServiceSubscription; -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; - -public class DIALService extends DeviceService implements Launcher { - - public static final String ID = "DIAL"; - - private static List registeredApps = new ArrayList(); - - static { - registeredApps.add("YouTube"); - registeredApps.add("Netflix"); - registeredApps.add("Amazon"); - } - - public static void registerApp(String appId) { - if (!registeredApps.contains(appId)) - registeredApps.add(appId); - } - - HttpClient httpClient; - - public DIALService(ServiceDescription serviceDescription, ServiceConfig serviceConfig) { - super(serviceDescription, serviceConfig); - - httpClient = new DefaultHttpClient(); - ClientConnectionManager mgr = httpClient.getConnectionManager(); - HttpParams params = httpClient.getParams(); - httpClient = new DefaultHttpClient(new ThreadSafeClientConnManager(params, mgr.getSchemeRegistry()), params); - } - - public static JSONObject discoveryParameters() { - JSONObject params = new JSONObject(); - - try { - params.put("serviceId", ID); - params.put("filter", "urn:dial-multiscreen-org:service:dial:1"); - } catch (JSONException e) { - e.printStackTrace(); - } - - return params; - } - - @Override - public void setServiceDescription(ServiceDescription serviceDescription) { - super.setServiceDescription(serviceDescription); - - Map> responseHeaders = this.getServiceDescription().getResponseHeaders(); - - if (responseHeaders != null) { - String commandPath; - List commandPaths = responseHeaders.get("Application-URL"); - - if (commandPaths != null && commandPaths.size() > 0) { - commandPath = commandPaths.get(0); - this.getServiceDescription().setApplicationURL(commandPath); - } - } - - probeForAppSupport(); - } - - @Override - public Launcher getLauncher() { - return this; - } - - @Override - public CapabilityPriorityLevel getLauncherCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; - } - - @Override - public void launchApp(String appId, AppLaunchListener listener) { - launchApp(appId, null, listener); - } - - private void launchApp(String appId, JSONObject params, AppLaunchListener listener) { - if (appId == null || appId.length() == 0) { - Util.postError(listener, new ServiceCommandError(0, "Must pass a valid appId", null)); - return; - } - - AppInfo appInfo = new AppInfo(); - appInfo.setName(appId); - appInfo.setId(appId); - - launchAppWithInfo(appInfo, listener); - } - -// private void launchApplication(final String appName, String contentId, final AppLaunchListener listener) { -// ResponseListener responseListener = new ResponseListener() { -// -// @Override -// public void onSuccess(Object response) { -// LaunchSession launchSession = new LaunchSession(); -// launchSession.setService(DIALService.this); -// launchSession.setAppName(appName); -// -// Util.postSuccess(listener, launchSession); -// } -// -// @Override -// public void onError(ServiceCommandError error) { -// Util.postError(listener, error); -// } -// }; -// -// String uri = requestURL(appName); -// -// String payload = null; -// if ( contentId != null ) { -// StringBuilder sb = new StringBuilder(); -// sb.append("v"); -// sb.append("="); -// sb.append(contentId); -// -// payload = sb.toString(); -// } -// -// ServiceCommand> request = new ServiceCommand>(this, uri, payload, responseListener); -// request.send(); -// } - - @Override - public void launchAppWithInfo(AppInfo appInfo, AppLaunchListener listener) { - launchAppWithInfo(appInfo, null, listener); - } - - @Override - public void launchAppWithInfo(final AppInfo appInfo, Object params, final AppLaunchListener listener) { - ServiceCommand> command = new ServiceCommand>(this, requestURL(appInfo.getName()), params, new ResponseListener() { - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, new ServiceCommandError(0, "Problem Launching app", null)); - } - - @Override - public void onSuccess(Object object) { - LaunchSession launchSession = LaunchSession.launchSessionForAppId(appInfo.getId()); - launchSession.setAppName(appInfo.getName()); - launchSession.setSessionId((String)object); - launchSession.setService(DIALService.this); - launchSession.setSessionType(LaunchSessionType.App); - - Util.postSuccess(listener, launchSession); - } - }); - - command.send(); - } - - @Override - public void launchBrowser(String url, AppLaunchListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void closeApp(final LaunchSession launchSession, final ResponseListener listener) { - getAppState(launchSession.getAppName(), new AppStateListener() { - - @Override - public void onSuccess(AppState state) { - String uri = requestURL(launchSession.getAppName()); - - if (launchSession.getSessionId().contains("http://") || launchSession.getSessionId().contains("https://")) - uri = launchSession.getSessionId(); - else if (launchSession.getSessionId().endsWith("run") || launchSession.getSessionId().endsWith("run/")) - uri = requestURL(launchSession.getAppId() + "/run"); - else - uri = requestURL(launchSession.getSessionId()); - - ServiceCommand> command = new ServiceCommand>(launchSession.getService(), uri, null, listener); - command.setHttpMethod(ServiceCommand.TYPE_DEL); - command.send(); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - } - - @Override - public void launchYouTube(String contentId, AppLaunchListener listener) { - launchYouTube(contentId, (float)0.0, listener); - } - - @Override - public void launchYouTube(String contentId, float startTime, AppLaunchListener listener) { - String params = null; - AppInfo appInfo = new AppInfo("YouTube"); - appInfo.setName(appInfo.getId()); - - if (contentId != null && contentId.length() > 0) { - if (startTime < 0.0) { - if (listener != null) { - listener.onError(new ServiceCommandError(0, "Start time may not be negative", null)); - } - - return; - } - - String pairingCode = java.util.UUID.randomUUID().toString(); - params = String.format("pairingCode=%s&v=%s&t=%.1f", pairingCode, contentId, startTime); - } - - launchAppWithInfo(appInfo, params, listener); - } - - @Override - public void launchHulu(String contentId, AppLaunchListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void launchNetflix(final String contentId, AppLaunchListener listener) { - JSONObject params = null; - - if (contentId != null && contentId.length() > 0) { - try { - new JSONObject() {{ - put("v", contentId); - }}; - } catch (JSONException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - AppInfo appInfo = new AppInfo("Netflix"); - appInfo.setName(appInfo.getId()); - - launchAppWithInfo(appInfo, params, listener); - } - - @Override - public void launchAppStore(String appId, AppLaunchListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - private void getAppState(String appName, final AppStateListener listener) { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - String str = (String)response; - String[] stateTAG = new String[2]; - stateTAG[0] = ""; - stateTAG[1] = ""; - - - int start = str.indexOf(stateTAG[0]); - int end = str.indexOf(stateTAG[1]); - - if ( start != -1 && end != -1 ) { - start += stateTAG[0].length(); - - String state = str.substring(start, end); - AppState appState = new AppState("running".equals(state), "running".equals(state)); - - Util.postSuccess(listener, appState); - // TODO: This isn't actually reporting anything. -// if ( listener != null ) -// listener.onAppStateSuccess(state); - } else { - Util.postError(listener, new ServiceCommandError(0, "Malformed response for app state", null)); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - String uri = requestURL(appName); - - ServiceCommand> request = new ServiceCommand>(this, uri, null, responseListener); - request.setHttpMethod(ServiceCommand.TYPE_GET); - - request.send(); - } - - @Override - public void getAppList(AppListListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void getRunningApp(AppInfoListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public ServiceSubscription subscribeRunningApp(AppInfoListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - - return new NotSupportedServiceSubscription(); - } - - @Override - public void getAppState(LaunchSession launchSession, AppStateListener listener) { - // TODO Auto-generated method stub - - } - - @Override - public ServiceSubscription subscribeAppState( - LaunchSession launchSession, - com.connectsdk.service.capability.Launcher.AppStateListener listener) { - // TODO Auto-generated method stub - return null; - } - - @Override - public void closeLaunchSession(LaunchSession launchSession, ResponseListener listener) { - if (launchSession.getSessionType() == LaunchSessionType.App) { - this.getLauncher().closeApp(launchSession, listener); - } else - { - Util.postError(listener, new ServiceCommandError(-1, "Could not find a launcher associated with this LaunchSession", launchSession)); - } - } - - @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); - } - - @Override - public void disconnect() { - connected = false; - - if (mServiceReachability != null) - mServiceReachability.stop(); - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - if (listener != null) - listener.onDisconnect(DIALService.this, null); - } - }); - } - - @Override - public void onLoseReachability(DeviceServiceReachability reachability) { - if (connected) { - disconnect(); - } else { - mServiceReachability.stop(); - } - } - - @Override - public void sendCommand(final ServiceCommand mCommand) { - Util.runInBackground(new Runnable() { - - @SuppressWarnings("unchecked") - @Override - public void run() { - ServiceCommand> command = (ServiceCommand>) mCommand; - Object payload = command.getPayload(); - - HttpRequestBase request = command.getRequest(); - HttpResponse response = null; - int code = -1; - - if (payload != null && command.getHttpMethod().equalsIgnoreCase(ServiceCommand.TYPE_POST)) { - request.setHeader(HttpMessage.CONTENT_TYPE_HEADER, "text/plain; charset=\"utf-8\""); - HttpPost post = (HttpPost) request; - HttpEntity entity = null; - try { - if (payload instanceof String) { - entity = new StringEntity((String) payload); - - } else if (payload instanceof JSONObject) { - entity = new StringEntity((String) payload); - } - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - // Error is handled below if entity is null; - } - - if (entity == null) { - Util.postError(command.getResponseListener(), new ServiceCommandError(0, "Unknown Error while preparing to send message", null)); - - return; - } - - post.setEntity(entity); - } - - try { - response = httpClient.execute(request); - - code = response.getStatusLine().getStatusCode(); - - if ( code == 200) { - HttpEntity entity = response.getEntity(); - String message = EntityUtils.toString(entity, "UTF-8"); - - Util.postSuccess(command.getResponseListener(), message); - } else if (code == 201) { - String locationPath = response.getHeaders("Location")[0].getValue(); - - Util.postSuccess(command.getResponseListener(), locationPath); - } - else { - Util.postError(command.getResponseListener(), ServiceCommandError.getError(code)); - } - } catch (IllegalStateException e) { - // TODO: Find out why this is needed. - e.printStackTrace(); - } catch (ClientProtocolException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - }); - } - - private String requestURL(String appName) { - String applicationURL = serviceDescription != null ? serviceDescription.getApplicationURL() : null; - - if (applicationURL == null) { - throw new IllegalStateException("DIAL service application URL not available"); - } - - StringBuilder sb = new StringBuilder(); - - sb.append(applicationURL); - - if (!applicationURL.endsWith("/")) - sb.append("/"); - - sb.append(appName); - - return sb.toString(); - } - - @Override - protected void updateCapabilities() { - List capabilities = new ArrayList(); - - capabilities.add(Application); - capabilities.add(Application_Params); - capabilities.add(Application_Close); - capabilities.add(AppState); - - setCapabilities(capabilities); - } - - private void hasApplication(String appID, ResponseListener listener) { - String uri = requestURL(appID); - - ServiceCommand> command = new ServiceCommand>(this, uri, null, listener); - command.setHttpMethod(ServiceCommand.TYPE_GET); - command.send(); - } - - private void probeForAppSupport() { - if (serviceDescription.getApplicationURL() == null) { - Log.d("Connect SDK", "unable to check for installed app; no service application url"); - return; - } - - for (final String appID : registeredApps) { - hasApplication(appID, new ResponseListener() { - - @Override public void onError(ServiceCommandError error) { } - - @Override - public void onSuccess(Object object) { - addCapability("Launcher." + appID); - addCapability("Launcher." + appID + ".Params"); - } - }); - } - } -} diff --git a/src/com/connectsdk/service/DLNAService.java b/src/com/connectsdk/service/DLNAService.java deleted file mode 100644 index 8840372f..00000000 --- a/src/com/connectsdk/service/DLNAService.java +++ /dev/null @@ -1,777 +0,0 @@ -/* - * 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 java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.conn.ClientConnectionManager; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; -import org.apache.http.params.HttpParams; -import org.apache.http.util.EntityUtils; -import org.json.JSONException; -import org.json.JSONObject; - -import com.connectsdk.core.MediaInfo; -import com.connectsdk.core.Util; -import com.connectsdk.core.upnp.service.Service; -import com.connectsdk.etc.helper.DeviceServiceReachability; -import com.connectsdk.etc.helper.HttpMessage; -import com.connectsdk.service.capability.MediaControl; -import com.connectsdk.service.capability.MediaPlayer; -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.config.ServiceConfig; -import com.connectsdk.service.config.ServiceDescription; -import com.connectsdk.service.sessions.LaunchSession; -import com.connectsdk.service.sessions.LaunchSession.LaunchSessionType; - -public class DLNAService extends DeviceService implements MediaControl, MediaPlayer { - - public static final String ID = "DLNA"; - - private static final String DATA = "XMLData"; - private static final String ACTION = "SOAPAction"; - private static final String ACTION_CONTENT = "\"urn:schemas-upnp-org:service:AVTransport:1#%s\""; - - String controlURL; - HttpClient httpClient; - - interface PositionInfoListener { - public void onGetPositionInfoSuccess(String positionInfoXml); - public void onGetPositionInfoFailed(ServiceCommandError error); - } - - public DLNAService(ServiceDescription serviceDescription, ServiceConfig serviceConfig) { - super(serviceDescription, serviceConfig); - - httpClient = new DefaultHttpClient(); - ClientConnectionManager mgr = httpClient.getConnectionManager(); - HttpParams params = httpClient.getParams(); - httpClient = new DefaultHttpClient(new ThreadSafeClientConnManager(params, mgr.getSchemeRegistry()), params); - - updateControlURL(); - } - - public static JSONObject discoveryParameters() { - JSONObject params = new JSONObject(); - - try { - params.put("serviceId", ID); - params.put("filter", "urn:schemas-upnp-org:device:MediaRenderer:1"); - } catch (JSONException e) { - e.printStackTrace(); - } - - return params; - } - - @Override - public void setServiceDescription(ServiceDescription serviceDescription) { - super.setServiceDescription(serviceDescription); - - updateControlURL(); - } - - private void updateControlURL() { - StringBuilder sb = new StringBuilder(); - List serviceList = serviceDescription.getServiceList(); - - if ( serviceList != null ) { - for ( int i = 0; i < serviceList.size(); i++) { - if ( serviceList.get(i).serviceType.contains("AVTransport") ) { - sb.append(serviceList.get(i).baseURL); - sb.append(serviceList.get(i).controlURL); - break; - } - } - controlURL = sb.toString(); - } - } - - /****************** - MEDIA PLAYER - *****************/ - @Override - public MediaPlayer getMediaPlayer() { - return this; - }; - - @Override - public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; - } - - public void displayMedia(final String url, final String mimeType, final String title, final String description, final 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 responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - String method = "Play"; - - Map parameters = new HashMap(); - parameters.put("Speed", "1"); - - JSONObject payload = getMethodBody(instanceId, method, parameters); - - ResponseListener playResponseListener = new ResponseListener () { - @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)); - } - - @Override - public void onError(ServiceCommandError error) { - if ( listener != null ) { - listener.onError(error); - } - } - }; - - ServiceCommand> request = new ServiceCommand>(DLNAService.this, method, payload, playResponseListener); - request.send(); - } - - @Override - public void onError(ServiceCommandError error) { - if ( listener != null ) { - listener.onError(error); - } - } - }; - - String method = "SetAVTransportURI"; - JSONObject httpMessage = getSetAVTransportURIBody(method, instanceId, url, mMimeType, title); - - ServiceCommand> request = new ServiceCommand>(DLNAService.this, method, httpMessage, responseListener); - request.send(); - } - - public void displayMedia(final MediaInfo mediaInfo, final LaunchListener listener) { - final String instanceId = "0"; - String[] mediaElements = mediaInfo.getMimeType().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 responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - String method = "Play"; - - Map parameters = new HashMap(); - parameters.put("Speed", "1"); - - JSONObject payload = getMethodBody(instanceId, method, parameters); - - ResponseListener playResponseListener = new ResponseListener () { - @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)); - } - - @Override - public void onError(ServiceCommandError error) { - if ( listener != null ) { - listener.onError(error); - } - } - }; - - ServiceCommand> request = new ServiceCommand>(DLNAService.this, method, payload, playResponseListener); - request.send(); - } - - @Override - public void onError(ServiceCommandError error) { - if ( listener != null ) { - listener.onError(error); - } - } - }; - - String method = "SetAVTransportURI"; - JSONObject httpMessage = getSetAVTransportURIBody(method, instanceId, mediaInfo.getUrl(), mMimeType, mediaInfo.getTitle()); - - ServiceCommand> request = new ServiceCommand>(DLNAService.this, method, httpMessage, responseListener); - request.send(); - } - - @Override - public void displayImage(String url, String mimeType, String title, String description, String iconSrc, LaunchListener listener) { - displayMedia(url, mimeType, title, description, iconSrc, listener); - } - - @Override - public void displayImage(MediaInfo mediaInfo, LaunchListener listener) { - - displayMedia(mediaInfo, listener); - - } - - @Override - public void playMedia(final String url, final String mimeType, final String title, final String description, final String iconSrc, final boolean shouldLoop, final LaunchListener listener) { - displayMedia(url, mimeType, title, description, iconSrc, listener); -// stop(new ResponseListener() { -// -// @Override -// public void onError(ServiceCommandError error) { -// Util.postError(listener, error); -// } -// -// @Override -// public void onSuccess(Object object) { -// 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); -// -// String shareXML = String.format("" + -// "" + -// "" + -// "" + -// "0" + -// "%s" + -// "" + -// "<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:dc="http://purl.org/dc/elements/1.1/"><item id="0" parentID="0" restricted="0"><dc:title>%s</dc:title><dc:description>%s</dc:description><res protocolInfo="http-get:*:%s:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01500000000000000000000000000000">%s</res><upnp:albumArtURI>%s</upnp:albumArtURI><upnp:class>object.item.%sItem</upnp:class></item></DIDL-Lite>" + -// "" + -// "" + -// "" + -// "", -// url, title, description, mMimeType, url, iconSrc, mediaType); -// -// String method = "SetAVTransportURI"; -// JSONObject obj = new JSONObject(); -// try { -// obj.put(ACTION, String.format(ACTION_CONTENT, method)); -// obj.put(DATA, shareXML); -// } catch (JSONException e) { -// e.printStackTrace(); -// } -// -// ResponseListener playResponseListener = new ResponseListener () { -// @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)); -// } -// -// @Override -// public void onError(ServiceCommandError error) { -// if ( listener != null ) { -// listener.onError(error); -// } -// } -// }; -// -// ServiceCommand> request = new ServiceCommand>(DLNAService.this, method, obj, playResponseListener); -// request.send(); -// } -// }); - } - - @Override - public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, - LaunchListener listener) { - - } - - @Override - public void closeMedia(LaunchSession launchSession, ResponseListener 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(final ResponseListener listener) { - String method = "Play"; - String instanceId = "0"; - - Map parameters = new HashMap(); - parameters.put("Speed", "1"); - - JSONObject payload = getMethodBody(instanceId, method, parameters); - - ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); - request.send(); - } - - @Override - public void pause(final ResponseListener listener) { - String method = "Pause"; - String instanceId = "0"; - - JSONObject payload = getMethodBody(instanceId, method); - - ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); - request.send(); - } - - @Override - public void stop(final ResponseListener listener) { - String method = "Stop"; - String instanceId = "0"; - - JSONObject payload = getMethodBody(instanceId, method); - - ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); - request.send(); - } - - @Override - public void rewind(final ResponseListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void fastForward(final ResponseListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void seek(long position, ResponseListener listener) { - String method = "Seek"; - String instanceId = "0"; - - 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); - - Map parameters = new HashMap(); - parameters.put("Unit", "REL_TIME"); - parameters.put("Target", time); - - JSONObject payload = getMethodBody(instanceId, method, parameters); - - ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); - request.send(); - } - - private void getPositionInfo(final PositionInfoListener listener) { - String method = "GetPositionInfo"; - String instanceId = "0"; - - JSONObject payload = getMethodBody(instanceId, method); - - ResponseListener responseListener = new ResponseListener() { - - @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> request = new ServiceCommand>(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"); - - long milliTimes = convertStrTimeFormatToLong(strDuration) * 1000; - - if (listener != null) { - listener.onSuccess(milliTimes); - } - } - - @Override - public void onGetPositionInfoFailed(ServiceCommandError error) { - if (listener != null) { - listener.onError(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) * 1000; - - if (listener != null) { - listener.onSuccess(milliTimes); - } - } - - @Override - public void onGetPositionInfoFailed(ServiceCommandError error) { - if (listener != null) { - listener.onError(error); - } - } - }); - } - - private JSONObject getSetAVTransportURIBody(String method, String instanceId, String mediaURL, String mime, String title) { - String action = "SetAVTransportURI"; - String metadata = getMetadata(mediaURL, mime, title); - - StringBuilder sb = new StringBuilder(); - sb.append(""); - sb.append(""); - - sb.append(""); - sb.append(""); - sb.append("" + instanceId + ""); - sb.append("" + mediaURL + ""); - sb.append(""+ metadata + ""); - - sb.append(""); - sb.append(""); - sb.append(""); - - JSONObject obj = new JSONObject(); - try { - obj.put(DATA, sb.toString()); - obj.put(ACTION, String.format(ACTION_CONTENT, method)); - } catch (JSONException e) { - e.printStackTrace(); - } - - return obj; - } - - private JSONObject getMethodBody(String instanceId, String method) { - return getMethodBody(instanceId, method, null); - } - - private JSONObject getMethodBody(String instanceId, String method, Map parameters) { - StringBuilder sb = new StringBuilder(); - - sb.append(""); - sb.append(""); - - sb.append(""); - sb.append(""); - sb.append("" + instanceId + ""); - - if (parameters != null) { - for (Map.Entry entry : parameters.entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - - sb.append("<" + key + ">"); - sb.append(value); - sb.append(""); - } - } - - sb.append(""); - sb.append(""); - sb.append(""); - - JSONObject obj = new JSONObject(); - try { - obj.put(DATA, sb.toString()); - obj.put(ACTION, String.format(ACTION_CONTENT, method)); - } catch (JSONException e) { - e.printStackTrace(); - } - - return obj; - } - - private String getMetadata(String mediaURL, String mime, String title) { - String id = "1000"; - String parentID = "0"; - String restricted = "0"; - String objectClass = null; - StringBuilder sb = new StringBuilder(); - - sb.append("<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" "); - sb.append("xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" "); - sb.append("xmlns:dc="http://purl.org/dc/elements/1.1/">"); - - sb.append("<item id="" + id + "" parentID="" + parentID + "" restricted="" + restricted + "">"); - sb.append("<dc:title>" + title + "</dc:title>"); - - 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"; - } - sb.append("<res protocolInfo="http-get:*:" + mime + ":DLNA.ORG_OP=01">" + mediaURL + "</res>"); - sb.append("<upnp:class>" + objectClass + "</upnp:class>"); - - sb.append("</item>"); - sb.append("</DIDL-Lite>"); - - return sb.toString(); - } - - @Override - public void sendCommand(final ServiceCommand mCommand) { - Util.runInBackground(new Runnable() { - - @SuppressWarnings("unchecked") - @Override - public void run() { - ServiceCommand> command = (ServiceCommand>) mCommand; - - JSONObject payload = (JSONObject) command.getPayload(); - - HttpPost request = HttpMessage.getDLNAHttpPost(controlURL, command.getTarget()); - request.setHeader(ACTION, payload.optString(ACTION)); - try { - request.setEntity(new StringEntity(payload.optString(DATA).toString())); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - - HttpResponse response = null; - - try { - response = httpClient.execute(request); - - final int code = response.getStatusLine().getStatusCode(); - - if (code == 200) { - HttpEntity entity = response.getEntity(); - final String message = EntityUtils.toString(entity, "UTF-8"); - - Util.postSuccess(command.getResponseListener(), message); - } - else { - Util.postError(command.getResponseListener(), ServiceCommandError.getError(code)); - } - - response.getEntity().consumeContent(); - } catch (ClientProtocolException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } - }); - } - - @Override - protected void updateCapabilities() { - List capabilities = new ArrayList(); - - capabilities.add(Display_Image); - capabilities.add(Display_Video); - capabilities.add(Display_Audio); - capabilities.add(Close); - - capabilities.add(MetaData_Title); - capabilities.add(MetaData_MimeType); - - capabilities.add(Play); - capabilities.add(Pause); - capabilities.add(Stop); - capabilities.add(Seek); - capabilities.add(Position); - capabilities.add(Duration); - capabilities.add(PlayState); - - setCapabilities(capabilities); - } - - @Override - public LaunchSession decodeLaunchSession(String type, JSONObject sessionObj) throws JSONException { - if (type == "dlna") { - LaunchSession launchSession = LaunchSession.launchSessionFromJSONObject(sessionObj); - launchSession.setService(this); - - return launchSession; - } - return null; - } - - private String parseData(String response, String key) { - String startTag = "<" + key + ">"; - String endTag = ""; - - int start = response.indexOf(startTag); - int end = response.indexOf(endTag); - - String data = response.substring(start + startTag.length(), end); - - return data; - } - - private long convertStrTimeFormatToLong(String strTime) { - String[] tokens = strTime.split(":"); - long time = 0; - - for (int i = 0; i < tokens.length; i++) { - time *= 60; - time += Integer.parseInt(tokens[i]); - } - - return time; - } - - @Override - public void getPlayState(PlayStateListener listener) { - if (listener != null) - listener.onError(ServiceCommandError.notSupported()); - } - - @Override - public ServiceSubscription subscribePlayState(PlayStateListener listener) { - if (listener != null) - listener.onError(ServiceCommandError.notSupported()); - - return null; - } - - @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); - } - - @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); - } - }); - } - - @Override - public void onLoseReachability(DeviceServiceReachability reachability) { - if (connected) { - disconnect(); - } else { - mServiceReachability.stop(); - } - } -} diff --git a/src/com/connectsdk/service/DeviceService.java b/src/com/connectsdk/service/DeviceService.java deleted file mode 100644 index 1ecfa8c0..00000000 --- a/src/com/connectsdk/service/DeviceService.java +++ /dev/null @@ -1,663 +0,0 @@ -/* - * DeviceService - * 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 java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.regex.Matcher; - -import org.json.JSONException; -import org.json.JSONObject; - -import android.util.SparseArray; - -import com.connectsdk.core.Util; -import com.connectsdk.device.ConnectableDevice; -import com.connectsdk.etc.helper.DeviceServiceReachability; -import com.connectsdk.etc.helper.DeviceServiceReachability.DeviceServiceReachabilityListener; -import com.connectsdk.service.capability.CapabilityMethods; -import com.connectsdk.service.capability.ExternalInputControl; -import com.connectsdk.service.capability.Launcher; -import com.connectsdk.service.capability.MediaPlayer; -import com.connectsdk.service.capability.WebAppLauncher; -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.command.ServiceCommand; -import com.connectsdk.service.command.ServiceCommandError; -import com.connectsdk.service.command.URLServiceSubscription; -import com.connectsdk.service.command.ServiceCommand.ServiceCommandProcessor; -import com.connectsdk.service.config.ServiceConfig; -import com.connectsdk.service.config.ServiceDescription; -import com.connectsdk.service.sessions.LaunchSession; - -/** - * ###Overview - * From a high-level perspective, DeviceService completely abstracts the functionality of a particular service/protocol (webOS TV, Netcast TV, Chromecast, Roku, DIAL, etc). - * - * ###In Depth - * DeviceService is an abstract class that is meant to be extended. You shouldn't ever use DeviceService directly, unless extending it to provide support for an additional service/protocol. - * - * Immediately after discovery of a DeviceService, DiscoveryManager will set the DeviceService's Listener to the ConnectableDevice that owns the DeviceService. You should not change the Listener unless you intend to manage the lifecycle of that service. The DeviceService will proxy all of its Listener method calls through the ConnectableDevice's ConnectableDeviceListener. - * - * ####Connection & Pairing - * Your ConnectableDevice object will let you know if you need to connect or pair to any services. - * - * ####Capabilities - * All DeviceService objects have a group of capabilities. These capabilities can be implemented by any object, and that object will be returned when you call the DeviceService's capability methods (launcher, mediaPlayer, volumeControl, etc). - */ -public class DeviceService implements DeviceServiceReachabilityListener, ServiceCommandProcessor { - public enum PairingType { - NONE, - FIRST_SCREEN, - PIN_CODE - } - - // @cond INTERNAL - public static final String KEY_CLASS = "class"; - public static final String KEY_CONFIG = "config"; - public static final String KEY_DESC = "description"; - - ServiceDescription serviceDescription; - ServiceConfig serviceConfig; - - protected DeviceServiceReachability mServiceReachability; - protected boolean connected = false; - // @endcond - - /** - * An array of capabilities supported by the DeviceService. This array may change based off a number of factors. - * - DiscoveryManager's pairingLevel value - * - Connect SDK framework version - * - First screen device OS version - * - First screen device configuration (apps installed, settings, etc) - * - Physical region - */ - List mCapabilities; - - // @cond INTERNAL - DeviceServiceListener listener; - - public SparseArray> requests = new SparseArray>(); - - public DeviceService(ServiceDescription serviceDescription, ServiceConfig serviceConfig) { - this.serviceDescription = serviceDescription; - this.serviceConfig = serviceConfig; - - mCapabilities = new ArrayList(); - - updateCapabilities(); - } - - public DeviceService(ServiceConfig serviceConfig) { - this.serviceConfig = serviceConfig; - - mCapabilities = new ArrayList(); - - updateCapabilities(); - } - - @SuppressWarnings("unchecked") - public static DeviceService getService(JSONObject json) { - Class newServiceClass; - - try { - String className = json.optString(KEY_CLASS); - - if (className.equalsIgnoreCase("DLNAService")) - return null; - - if (className.equalsIgnoreCase("Chromecast")) - return null; - - newServiceClass = (Class) Class.forName(DeviceService.class.getPackage().getName() + "." + className); - Constructor constructor = newServiceClass.getConstructor(ServiceDescription.class, ServiceConfig.class); - - JSONObject jsonConfig = json.optJSONObject(KEY_CONFIG); - ServiceConfig serviceConfig = null; - if (jsonConfig != null) - serviceConfig = ServiceConfig.getConfig(jsonConfig); - - JSONObject jsonDescription = json.optJSONObject(KEY_DESC); - ServiceDescription serviceDescription = null; - if (jsonDescription != null) - serviceDescription = ServiceDescription.getDescription(jsonDescription); - - if (serviceConfig == null || serviceDescription == null) - return null; - - return constructor.newInstance(serviceDescription, serviceConfig); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (InstantiationException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } - - return null; - } - - public static DeviceService getService(Class clazz, ServiceConfig serviceConfig) { - try { - Constructor constructor = clazz.getConstructor(ServiceConfig.class); - - return constructor.newInstance(serviceConfig); - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (InstantiationException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } - - return null; - } - - public static DeviceService getService(Class clazz, ServiceDescription serviceDescription, ServiceConfig serviceConfig) { - try { - Constructor constructor = clazz.getConstructor(ServiceDescription.class, ServiceConfig.class); - - return constructor.newInstance(serviceDescription, serviceConfig); - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (InstantiationException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } - - return null; - } - - @SuppressWarnings("unchecked") - public T getAPI(Class clazz) { - if ( clazz.isAssignableFrom(this.getClass()) ) { - return (T) this; - } - else - return null; - } - - public static JSONObject discoveryParameters() { - return null; - } - // @endcond - - /** - * Will attempt to connect to the DeviceService. The failure/success will be reported back to the DeviceServiceListener. If the connection attempt reveals that pairing is required, the DeviceServiceListener will also be notified in that event. - */ - public void connect() { - - } - - /** - * Will attempt to disconnect from the DeviceService. The failure/success will be reported back to the DeviceServiceListener. - */ - public void disconnect() { - - } - - /** Whether the DeviceService is currently connected */ - public boolean isConnected() { - return true; - } - - public boolean isConnectable() { - return false; - } - - /** Explicitly cancels pairing in services that require pairing. In some services, this will hide a prompt that is displaying on the device. */ - public void cancelPairing() { - - } - - protected void reportConnected(boolean ready) { - if (listener == null) - return; - - // only run callback on main thread if the callback is leaving the SDK - if (listener instanceof ConnectableDevice) - listener.onConnectionSuccess(this); - else { - Util.runOnUI(new Runnable() { - @Override - public void run() { - if (listener != null) - listener.onConnectionSuccess(DeviceService.this); - } - }); - } - } - - /** - * Will attempt to pair with the DeviceService with the provided pairingData. The failure/success will be reported back to the DeviceServiceListener. - * - * @param pairingKey Data to be used for pairing. The type of this parameter will vary depending on what type of pairing is required, but is likely to be a string (pin code, pairing key, etc). - */ - public void sendPairingKey(String pairingKey) { - - } - - // @cond INTERNAL - - public void unsubscribe(URLServiceSubscription subscription) { - - } - - public void sendCommand(ServiceCommand command) { - - } - - // @endcond - - public List getCapabilities() { - return mCapabilities; - } - - protected void updateCapabilities() { } - - protected void setCapabilities(List newCapabilities) { - List oldCapabilities = mCapabilities; - - mCapabilities = newCapabilities; - - List _lostCapabilities = new ArrayList(); - - for (String capability : oldCapabilities) { - if (!newCapabilities.contains(capability)) - _lostCapabilities.add(capability); - } - - List _addedCapabilities = new ArrayList(); - - for (String capability : newCapabilities) { - if (!oldCapabilities.contains(capability)) - _addedCapabilities.add(capability); - } - - final List lostCapabilities = _lostCapabilities; - final List addedCapabilities = _addedCapabilities; - - if (this.listener != null) { - Util.runOnUI(new Runnable() { - - @Override - public void run() { - listener.onCapabilitiesUpdated(DeviceService.this, addedCapabilities, lostCapabilities); - } - }); - } - } - - /** - * Test to see if the capabilities array contains a given capability. See the individual Capability classes for acceptable capability values. - * - * It is possible to append a wildcard search term `.Any` to the end of the search term. This method will return true for capabilities that match the term up to the wildcard. - * - * Example: `Launcher.App.Any` - * - * @param capability Capability to test against - */ - public boolean hasCapability(String capability) { - Matcher m = CapabilityMethods.ANY_PATTERN.matcher(capability); - - if (m.find()) { - String match = m.group(); - for (String item : this.mCapabilities) { - if (item.indexOf(match) != -1) { - return true; - } - } - - return false; - } - - return mCapabilities.contains(capability); - } - - /** - * Test to see if the capabilities array contains at least one capability in a given set of capabilities. See the individual Capability classes for acceptable capability values. - * - * See hasCapability: for a description of the wildcard feature provided by this method. - * - * @param capabilities Set of capabilities to test against - */ - public boolean hasAnyCapability(String... capabilities) { - for (String capability : capabilities) { - if (hasCapability(capability)) - return true; - } - - return false; - } - - /** - * Test to see if the capabilities array contains a given set of capabilities. See the individual Capability classes for acceptable capability values. - * - * See hasCapability: for a description of the wildcard feature provided by this method. - * - * @param capabilities List of capabilities to test against - */ - public boolean hasCapabilities(List capabilities) { - String[] arr = new String[capabilities.size()]; - capabilities.toArray(arr); - return hasCapabilities(arr); - } - - /** - * Test to see if the capabilities array contains a given set of capabilities. See the individual Capability classes for acceptable capability values. - * - * See hasCapability: for a description of the wildcard feature provided by this method. - * - * @param capabilities Set of capabilities to test against - */ - public boolean hasCapabilities(String... capabilities) { - boolean hasCaps = true; - - for (String capability: capabilities) { - if (!hasCapability(capability)) { - hasCaps = false; - break; - } - } - - return hasCaps; - } - - // @cond INTERNAL - public void setServiceDescription(ServiceDescription serviceDescription) { - this.serviceDescription = serviceDescription; - } - // @endcond - - public ServiceDescription getServiceDescription() { - return serviceDescription; - } - - // @cond INTERNAL - public void setServiceConfig(ServiceConfig serviceConfig) { - this.serviceConfig = serviceConfig; - } - // @endcond - - public ServiceConfig getServiceConfig() { - return serviceConfig; - } - - public JSONObject toJSONObject() { - JSONObject jsonObj = new JSONObject(); - - try { - jsonObj.put(KEY_CLASS, getClass().getSimpleName()); - jsonObj.put("description", serviceDescription.toJSONObject()); - jsonObj.put("config", serviceConfig.toJSONObject()); - } catch (JSONException e) { - e.printStackTrace(); - } - - return jsonObj; - } - - /** Name of the DeviceService (webOS, Chromecast, etc) */ - public String getServiceName() { - return serviceDescription.getServiceID(); - } - - // @cond INTERNAL - /** - * Create a LaunchSession from a serialized JSON object. - * May return null if the session was not the one that created the session. - * - * Intended for internal use. - */ - public LaunchSession decodeLaunchSession(String type, JSONObject sessionObj) throws JSONException { - return null; - } - - public DeviceServiceListener getListener() { - return listener; - } - - public void setListener(DeviceServiceListener listener) { - this.listener = listener; - } - // @endcond - - /** - * Closes the session on the first screen device. Depending on the sessionType, the associated service will have different ways of handling the close functionality. - * - * @param launchSession LaunchSession to close - * @param success (optional) listener to be called on success/failure - */ - public void closeLaunchSession(LaunchSession launchSession, ResponseListener listener) { - if (launchSession == null) { - Util.postError(listener, new ServiceCommandError(0, "You must provide a valid LaunchSession", null)); - return; - } - - DeviceService service = launchSession.getService(); - if (service == null) { - Util.postError(listener, new ServiceCommandError(0, "There is no service attached to this launch session", null)); - return; - } - - switch (launchSession.getSessionType()) { - case App: - if (service instanceof Launcher) - ((Launcher) service).closeApp(launchSession, listener); - break; - case Media: - if (service instanceof MediaPlayer) - ((MediaPlayer) service).closeMedia(launchSession, listener); - break; - case ExternalInputPicker: - if (service instanceof ExternalInputControl) - ((ExternalInputControl) service).closeInputPicker(launchSession, listener); - break; - case WebApp: - if (service instanceof WebAppLauncher) - ((WebAppLauncher) service).closeWebApp(launchSession, listener); - break; - case Unknown: - default: - Util.postError(listener, new ServiceCommandError(0, "This DeviceService does not know ho to close this LaunchSession", null)); - break; - } - } - - // @cond INTERNAL - public void addCapability(final String capability) { - if (capability == null || capability.length() == 0 || this.mCapabilities.contains(capability)) - return; - - this.mCapabilities.add(capability); - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - List added = new ArrayList(); - added.add(capability); - - if (listener != null) - listener.onCapabilitiesUpdated(DeviceService.this, added, new ArrayList()); - } - }); - } - - public void addCapabilities(final List capabilities) { - if (capabilities == null) - return; - - for (String capability : capabilities) { - if (capability == null || capability.length() == 0 || mCapabilities.contains(capabilities)) - continue; - - mCapabilities.add(capability); - } - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - - if (listener != null) - listener.onCapabilitiesUpdated(DeviceService.this, capabilities, new ArrayList()); - } - }); - } - - public void addCapabilities(String... capabilities) { - addCapabilities(Arrays.asList(capabilities)); - } - - public void removeCapability(final String capability) { - if (capability == null) - return; - - this.mCapabilities.remove(capability); - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - List removed = new ArrayList(); - removed.add(capability); - - if (listener != null) - listener.onCapabilitiesUpdated(DeviceService.this, new ArrayList(), removed); - } - }); - } - - public void removeCapabilities(final List capabilities) { - if (capabilities == null) - return; - - for (String capability : capabilities) { - mCapabilities.remove(capability); - } - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - - if (listener != null) - listener.onCapabilitiesUpdated(DeviceService.this, new ArrayList(), capabilities); - } - }); - } - - public void removeCapabilities(String... capabilities) { - removeCapabilities(Arrays.asList(capabilities)); - } - - // Unused by default. - @Override public void onLoseReachability(DeviceServiceReachability reachability) { } - // @endcond - - public interface DeviceServiceListener { - - /*! - * If the DeviceService requires an active connection (websocket, pairing, etc) this method will be called. - * - * @param service DeviceService that requires connection - */ - public void onConnectionRequired(DeviceService service); - - /*! - * After the connection has been successfully established, and after pairing (if applicable), this method will be called. - * - * @param service DeviceService that was successfully connected - */ - public void onConnectionSuccess(DeviceService service); - - /*! - * There are situations in which a DeviceService will update the capabilities it supports and propagate these changes to the DeviceService. Such situations include: - * - on discovery, DIALService will reach out to detect if certain apps are installed - * - on discovery, certain DeviceServices need to reach out for version & region information - * - * For more information on this particular method, see ConnectableDeviceDelegate's connectableDevice:capabilitiesAdded:removed: method. - * - * @param service DeviceService that has experienced a change in capabilities - * @param added List of capabilities that are new to the DeviceService - * @param removed List of capabilities that the DeviceService has lost - */ - public void onCapabilitiesUpdated(DeviceService service, List added, List removed); - - /*! - * This method will be called on any disconnection. If error is nil, then the connection was clean and likely triggered by the responsible DiscoveryProvider or by the user. - * - * @param service DeviceService that disconnected - * @param error Error with a description of any errors causing the disconnect. If this value is nil, then the disconnect was clean/expected. - */ - public void onDisconnect(DeviceService service, Error error); - - /*! - * Will be called if the DeviceService fails to establish a connection. - * - * @param service DeviceService which has failed to connect - * @param error Error with a description of the failure - */ - public void onConnectionFailure(DeviceService service, Error error); - - /*! - * If the DeviceService requires pairing, valuable data will be passed to the delegate via this method. - * - * @param service DeviceService that requires pairing - * @param pairingType PairingType that the DeviceService requires - * @param pairingData Any data that might be required for the pairing process, will usually be nil - */ - public void onPairingRequired(DeviceService service, PairingType pairingType, Object pairingData); - - /*! - * This method will be called upon pairing success. On pairing success, a connection to the DeviceService will be attempted. - * - * @property service DeviceService that has successfully completed pairing - */ - public void onPairingSuccess(DeviceService service); - - /*! - * If there is any error in pairing, this method will be called. - * - * @param service DeviceService that has failed to complete pairing - * @param error Error with a description of the failure - */ - public void onPairingFailed(DeviceService service, Error error); - } -} diff --git a/src/com/connectsdk/service/NetcastTVService.java b/src/com/connectsdk/service/NetcastTVService.java deleted file mode 100644 index de2a8bc7..00000000 --- a/src/com/connectsdk/service/NetcastTVService.java +++ /dev/null @@ -1,2404 +0,0 @@ -/* - * NetcastTVService - * 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 java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpRequestBase; -import org.apache.http.conn.ClientConnectionManager; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; -import org.apache.http.params.HttpParams; -import org.apache.http.util.EntityUtils; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.xml.sax.SAXException; - -import android.graphics.PointF; -import android.util.Log; - -import com.connectsdk.core.AppInfo; -import com.connectsdk.core.ChannelInfo; -import com.connectsdk.core.ExternalInputInfo; -import com.connectsdk.core.MediaInfo; -import com.connectsdk.core.Util; -import com.connectsdk.device.ConnectableDevice; -import com.connectsdk.device.netcast.NetcastAppNumberParser; -import com.connectsdk.device.netcast.NetcastApplicationsParser; -import com.connectsdk.device.netcast.NetcastChannelParser; -import com.connectsdk.device.netcast.NetcastHttpServer; -import com.connectsdk.device.netcast.NetcastVolumeParser; -import com.connectsdk.device.netcast.VirtualKeycodes; -import com.connectsdk.discovery.DiscoveryManager; -import com.connectsdk.discovery.DiscoveryManager.PairingLevel; -import com.connectsdk.etc.helper.DeviceServiceReachability; -import com.connectsdk.etc.helper.HttpMessage; -import com.connectsdk.service.capability.ExternalInputControl; -import com.connectsdk.service.capability.KeyControl; -import com.connectsdk.service.capability.Launcher; -import com.connectsdk.service.capability.MediaControl; -import com.connectsdk.service.capability.MediaPlayer; -import com.connectsdk.service.capability.MouseControl; -import com.connectsdk.service.capability.PowerControl; -import com.connectsdk.service.capability.TVControl; -import com.connectsdk.service.capability.TextInputControl; -import com.connectsdk.service.capability.VolumeControl; -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.command.NotSupportedServiceSubscription; -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.NetcastTVServiceConfig; -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; - -public class NetcastTVService extends DeviceService implements Launcher, MediaControl, MediaPlayer, TVControl, VolumeControl, ExternalInputControl, MouseControl, TextInputControl, PowerControl, KeyControl { - - public static final String ID = "Netcast TV"; - - public static final String UDAP_PATH_PAIRING = "/udap/api/pairing"; - public static final String UDAP_PATH_DATA = "/udap/api/data"; - public static final String UDAP_PATH_COMMAND = "/udap/api/command"; - public static final String UDAP_PATH_EVENT = "/udap/api/event"; - - public static final String UDAP_PATH_APPTOAPP_DATA = "/udap/api/apptoapp/data/"; - public static final String UDAP_PATH_APPTOAPP_COMMAND = "/udap/api/apptoapp/command/"; - public static final String ROAP_PATH_APP_STORE = "/roap/api/command/"; - - public static final String UDAP_API_PAIRING = "pairing"; - public static final String UDAP_API_COMMAND = "command"; - public static final String UDAP_API_EVENT = "event"; - - public final static String TARGET_CHANNEL_LIST = "channel_list"; - public final static String TARGET_CURRENT_CHANNEL = "cur_channel"; - public final static String TARGET_VOLUME_INFO = "volume_info"; - public final static String TARGET_APPLIST_GET = "applist_get"; - public final static String TARGET_APPNUM_GET = "appnum_get"; - public final static String TARGET_3D_MODE = "3DMode"; - public final static String TARGET_IS_3D = "is_3D"; - - enum State { - NONE, - INITIAL, - CONNECTING, - PAIRING, - PAIRED, - DISCONNECTING - }; - - HttpClient httpClient; - NetcastHttpServer httpServer; - - DLNAService dlnaService; - DIALService dialService; - - LaunchSession inputPickerSession; - - List applications; - List> subscriptions; - StringBuilder keyboardString; - - State state = State.INITIAL; - - PointF mMouseDistance; - Boolean mMouseIsMoving; - - public NetcastTVService(ServiceDescription serviceDescription, ServiceConfig serviceConfig) { - super(serviceDescription, serviceConfig); - - if (serviceDescription.getPort() != 8080) - serviceDescription.setPort(8080); - - applications = new ArrayList(); - subscriptions = new ArrayList>(); - - keyboardString = new StringBuilder(); - - httpClient = new DefaultHttpClient(); - ClientConnectionManager mgr = httpClient.getConnectionManager(); - HttpParams params = httpClient.getParams(); - httpClient = new DefaultHttpClient(new ThreadSafeClientConnManager(params, mgr.getSchemeRegistry()), params); - - state = State.INITIAL; - - inputPickerSession = null; - } - - public static JSONObject discoveryParameters() { - JSONObject params = new JSONObject(); - - try { - params.put("serviceId", ID); -// params.put("filter", "udap:rootservice"); - params.put("filter", "urn:schemas-upnp-org:device:MediaRenderer:1"); - } catch (JSONException e) { - e.printStackTrace(); - } - - return params; - } - - @Override - public void setServiceDescription(ServiceDescription serviceDescription) { - super.setServiceDescription(serviceDescription); - - if (serviceDescription.getPort() != 8080) - serviceDescription.setPort(8080); - } - - @Override - public void connect() { - if (state != State.INITIAL) { - Log.w("Connect SDK", "already connecting; not trying to connect again: " + state); - return; // don't try to connect again while connected - } - - if ( !(serviceConfig instanceof NetcastTVServiceConfig) ) { - serviceConfig = new NetcastTVServiceConfig(serviceConfig.getServiceUUID()); - } - - if ( DiscoveryManager.getInstance().getPairingLevel() == PairingLevel.ON ) { - if ( ((NetcastTVServiceConfig) serviceConfig).getPairingKey() != null - && ((NetcastTVServiceConfig)serviceConfig).getPairingKey().length() != 0) { - - sendPairingKey(((NetcastTVServiceConfig) serviceConfig).getPairingKey()); - } - else { - showPairingKeyOnTV(); - } - - Util.runInBackground(new Runnable() { - - @Override - public void run() { - httpServer = new NetcastHttpServer(NetcastTVService.this, getServiceDescription().getPort(), mTextChangedListener); - httpServer.setSubscriptions(subscriptions); - httpServer.start(); - } - }); - } else { - hConnectSuccess(); - } - } - - @Override - public void disconnect() { - endPairing(null); - - connected = false; - - if (mServiceReachability != null) - mServiceReachability.stop(); - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - if (listener != null) - listener.onDisconnect(NetcastTVService.this, null); - } - }); - - if ( httpServer != null ) { - httpServer.stop(); - httpServer = null; - } - - state = State.INITIAL; - } - - @Override - public boolean isConnectable() { - return true; - } - - @Override - public boolean isConnected() { - return connected; - } - - private void hConnectSuccess() { - // TODO: Fix this for Netcast. 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; - - // Pairing was successful, so report connected and ready - reportConnected(true); - } - - @Override - public void onLoseReachability(DeviceServiceReachability reachability) { - if (connected) { - disconnect(); - } else { - if (mServiceReachability != null) - mServiceReachability.stop(); - } - } - - public void hostByeBye () { - disconnect(); - } - - //============= Auth ============================== - public void showPairingKeyOnTV() { - state = State.CONNECTING; - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - if (listener != null) - listener.onPairingRequired(NetcastTVService.this, PairingType.PIN_CODE, null); - } - - @Override - public void onError(ServiceCommandError error) { - state = State.INITIAL; - - if (listener != null) - listener.onConnectionFailure(NetcastTVService.this, error); - } - }; - - String requestURL = getUDAPRequestURL(UDAP_PATH_PAIRING); - - Map params = new HashMap(); - params.put("name", "showKey"); - - String httpMessage = getUDAPMessageBody(UDAP_API_PAIRING, params); - - ServiceCommand> command = new ServiceCommand>(this, requestURL, httpMessage, responseListener); - command.send(); - } - - @Override - public void cancelPairing() { - removePairingKeyOnTV(); - state = State.INITIAL; - } - - public void removePairingKeyOnTV() { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - } - - @Override - public void onError(ServiceCommandError error) { - } - }; - - String requestURL = getUDAPRequestURL(UDAP_PATH_PAIRING); - - Map params = new HashMap(); - params.put("name", "CancelAuthKeyReq"); - - String httpMessage = getUDAPMessageBody(UDAP_API_PAIRING, params); - - ServiceCommand> command = new ServiceCommand>(this, requestURL, httpMessage, responseListener); - command.send(); - } - - @Override - public void sendPairingKey(final String pairingKey) { - state = State.PAIRING; - - if ( !(serviceConfig instanceof NetcastTVServiceConfig) ) { - serviceConfig = new NetcastTVServiceConfig(serviceConfig.getServiceUUID()); - } - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - state = State.PAIRED; - - ((NetcastTVServiceConfig)serviceConfig).setPairingKey(pairingKey); - - hConnectSuccess(); - } - - @Override - public void onError(ServiceCommandError error) { - state = State.INITIAL; - - if (listener != null) - listener.onConnectionFailure(NetcastTVService.this, error); - } - }; - - String requestURL = getUDAPRequestURL(UDAP_PATH_PAIRING); - - Map params = new HashMap(); - params.put("name", "hello"); - params.put("value", pairingKey); - params.put("port", String.valueOf(serviceDescription.getPort())); - - String httpMessage = getUDAPMessageBody(UDAP_API_PAIRING, params); - - ServiceCommand> command = new ServiceCommand>(this, requestURL, httpMessage, responseListener); - command.send(); - } - - private void endPairing(ResponseListener listener) { - String requestURL = getUDAPRequestURL(UDAP_PATH_PAIRING); - - Map params = new HashMap(); - params.put("name", "byebye"); - params.put("port", String.valueOf(serviceDescription.getPort())); - - String httpMessage = getUDAPMessageBody(UDAP_API_PAIRING, params); - - ServiceCommand> command = new ServiceCommand>(this, requestURL, httpMessage, listener); - command.send(); - } - - - /****************** - LAUNCHER - *****************/ - public Launcher getLauncher() { - return this; - }; - - @Override - public CapabilityPriorityLevel getLauncherCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - class NetcastTVLaunchSessionR extends LaunchSession { - String appName; - NetcastTVService service; - - NetcastTVLaunchSessionR (NetcastTVService service, String auid, String appName) { - this.service = service; - appId = auid; - } - - NetcastTVLaunchSessionR (NetcastTVService service, JSONObject obj) throws JSONException { - this.service = service; - fromJSONObject(obj); - } - - public void close(ResponseListener responseListener) { - } - - @Override - public JSONObject toJSONObject() throws JSONException { - JSONObject obj = super.toJSONObject(); - obj.put("type", "netcasttv"); - obj.put("appName", appName); - return obj; - } - - @Override - public void fromJSONObject(JSONObject obj) throws JSONException { - super.fromJSONObject(obj); - appName = obj.optString("appName"); - } - } - - public void getApplication(final String appName, final AppInfoListener listener) { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - final String strObj = (String) response; - - AppInfo appId = new AppInfo() {{ - setId(decToHex(strObj)); - }}; - - if ( appId != null ) { - Util.postSuccess(listener, appId); - } - } - - @Override - public void onError(ServiceCommandError error) { - if ( listener != null ) - Util.postError(listener, error); - } - }; - - String uri = UDAP_PATH_APPTOAPP_DATA + appName; - String requestURL = getUDAPRequestURL(uri); - - ServiceCommand> command = new ServiceCommand>(this, requestURL, null, responseListener); - command.setHttpMethod(ServiceCommand.TYPE_GET); - command.send(); - } - - @Override - public void launchApp(final String appId, final AppLaunchListener listener) { - getAppInfoForId(appId, new AppInfoListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(AppInfo appInfo) { - launchAppWithInfo(appInfo, listener); - } - }); - } - - private void getAppInfoForId(final String appId, final AppInfoListener listener) { - getAppList(new AppListListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(List object) { - for (AppInfo info : object) { - if (info.getName().equalsIgnoreCase(appId)) { - Util.postSuccess(listener, info); - return; - } - } - Util.postError(listener, new ServiceCommandError(0, "Unable to find the App with id", null)); - } - }); - } - - private void launchApplication(final String appName, final String auid, final String contentId, final Launcher.AppLaunchListener listener) { - JSONObject jsonObj = new JSONObject(); - - try { - jsonObj.put("id", auid); - jsonObj.put("title", appName); - } catch (JSONException e) { - e.printStackTrace(); - } - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - LaunchSession launchSession = LaunchSession.launchSessionForAppId(auid); - launchSession.setAppName(appName); - launchSession.setService(NetcastTVService.this); - launchSession.setSessionType(LaunchSessionType.App); - - Util.postSuccess(listener, launchSession); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - String requestURL = getUDAPRequestURL(UDAP_PATH_APPTOAPP_COMMAND); - - Map params = new HashMap(); - params.put("name", "AppExecute"); - params.put("auid", auid); - if ( appName != null ) { - params.put("appname", appName); - } - if ( contentId != null ) { - params.put("contentid", contentId); - } - - String httpMessage = getUDAPMessageBody(UDAP_API_COMMAND, params); - - ServiceCommand> request = new ServiceCommand>(this, requestURL, httpMessage, responseListener); - request.send(); - } - - @Override - public void launchAppWithInfo(AppInfo appInfo, Launcher.AppLaunchListener listener) { - launchAppWithInfo(appInfo, null, listener); - } - - @Override - public void launchAppWithInfo(AppInfo appInfo, Object params, Launcher.AppLaunchListener listener) { - String appName = HttpMessage.encode(appInfo.getName()); - String appId = appInfo.getId(); - String contentId = null; - JSONObject mParams = null; - if (params instanceof JSONObject) - mParams = (JSONObject) params; - - if (mParams != null) { - try { - contentId = (String) mParams.get("contentId"); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - launchApplication(appName, appId, contentId, listener); - } - - @Override - public void launchBrowser(String url, final Launcher.AppLaunchListener listener) { - if ( !(url == null || url.length() == 0) ) - Log.w("Connect SDK", "Netcast TV does not support deeplink for Browser"); - - final String appName = "Internet"; - - getApplication(appName, new AppInfoListener() { - - @Override - public void onSuccess(AppInfo appInfo) { - String contentId = null; - launchApplication(appName, appInfo.getId(), contentId, listener); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - } - - @Override - public void launchYouTube(String contentId, Launcher.AppLaunchListener listener) { - launchYouTube(contentId, (float)0.0, listener); - } - - @Override - public void launchYouTube(final String contentId, float startTime, final AppLaunchListener listener) { - if (getDIALService() != null) { - getDIALService().getLauncher().launchYouTube(contentId, startTime, listener); - return; - } - - if (startTime <= 0.0) { - getApplication("YouTube", new AppInfoListener() { - - @Override - public void onSuccess(AppInfo appInfo) { - launchApplication(appInfo.getName(), appInfo.getId(), contentId, listener); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - } - else { - if (listener != null) { - listener.onError(new ServiceCommandError(0, "Cannot reach DIAL service for launching with provided start time", null)); - } - } - } - - @Override - public void launchHulu(final String contentId, final Launcher.AppLaunchListener listener) { - final String appName = "Hulu"; - - getApplication(appName, new AppInfoListener() { - - @Override - public void onSuccess(AppInfo appInfo) { - launchApplication(appName, appInfo.getId(), contentId, listener); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - } - - @Override - public void launchNetflix(final String contentId, final Launcher.AppLaunchListener listener) { - if (!serviceDescription.getModelNumber().equals("4.0")) { - launchApp("Netflix", listener); - return; - } - - final String appName = "Netflix"; - - getApplication(appName, new AppInfoListener() { - - @Override - public void onSuccess(final AppInfo appInfo) { - JSONObject jsonObj = new JSONObject(); - - try { - jsonObj.put("id", appInfo.getId()); - jsonObj.put("name", appName); - } catch (JSONException e) { - e.printStackTrace(); - } - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - LaunchSession launchSession = LaunchSession.launchSessionForAppId(appInfo.getId()); - launchSession.setAppName(appName); - launchSession.setService(NetcastTVService.this); - launchSession.setSessionType(LaunchSessionType.App); - - Util.postSuccess(listener, launchSession); - } - - @Override - public void onError(ServiceCommandError error) { - if ( listener != null ) - Util.postError(listener, error); - } - }; - - String requestURL = getUDAPRequestURL(UDAP_PATH_APPTOAPP_COMMAND); - - Map params = new HashMap(); - params.put("name", "SearchCMDPlaySDPContent"); - params.put("content_type", "1"); - params.put("conts_exec_type", "20"); - params.put("conts_plex_type_flag", "N"); - params.put("conts_search_id", "2023237"); - params.put("conts_age", "18"); - params.put("exec_id", "netflix"); - params.put("item_id", "-Q m=http%3A%2F%2Fapi.netflix.com%2Fcatalog%2Ftitles%2Fmovies%2F" + contentId + "&source_type=4&trackId=6054700&trackUrl=https%3A%2F%2Fapi.netflix.com%2FAPI_APP_ID_6261%3F%23Search%3F"); - params.put("app_type", ""); - - String httpMessage = getUDAPMessageBody(UDAP_API_COMMAND, params); - - ServiceCommand> request = new ServiceCommand>(NetcastTVService.this, requestURL, httpMessage, responseListener); - request.send(); - } - - @Override - public void onError(ServiceCommandError error) { - if ( listener != null ) - Util.postError(listener, error); - } - }); - } - - @Override - public void launchAppStore(final String appId, final AppLaunchListener listener) { - if (!serviceDescription.getModelNumber().equals("4.0")) { - launchApp("LG Smart World", listener); // TODO: this will not work in Korea, use Korean name instead - return; - } - - String targetPath = getUDAPRequestURL(ROAP_PATH_APP_STORE); - - Map params = new HashMap(); - params.put("name", "SearchCMDPlaySDPContent"); - params.put("content_type", "4"); - params.put("conts_exec_type", ""); - params.put("conts_plex_type_flag", ""); - params.put("conts_search_id", ""); - params.put("conts_age", "12"); - params.put("exec_id", ""); - params.put("item_id", HttpMessage.encode(appId)); - params.put("app_type", "S"); - - String httpMessage = getUDAPMessageBody(UDAP_API_COMMAND, params); - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - LaunchSession launchSession = LaunchSession.launchSessionForAppId(appId); - launchSession.setAppName("LG Smart World"); // TODO: this will not work in Korea, use Korean name instead - launchSession.setService(NetcastTVService.this); - launchSession.setSessionType(LaunchSessionType.App); - - Util.postSuccess(listener, launchSession); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - ServiceCommand> command = new ServiceCommand>(this, targetPath, httpMessage, responseListener); - command.send(); - } - - @Override - public void closeApp(LaunchSession launchSession, ResponseListener listener) { - String requestURL = getUDAPRequestURL(UDAP_PATH_APPTOAPP_COMMAND); - - Map params = new HashMap(); - params.put("name", "AppTerminate"); - params.put("auid", launchSession.getAppId()); - if (launchSession.getAppName() != null) - params.put("appname", HttpMessage.encode(launchSession.getAppName())); - - String httpMessage = getUDAPMessageBody(UDAP_API_COMMAND, params); - - ServiceCommand> command = new ServiceCommand>(launchSession.getService(), requestURL, httpMessage, listener); - command.send(); - } - - private void getTotalNumberOfApplications(final int type, final AppCountListener listener) { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - String strObj = (String) response; - - int applicationNumber = parseAppNumberXmlToJSON(strObj); - - Util.postSuccess(listener, applicationNumber); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - String requestURL = getUDAPRequestURL(UDAP_PATH_DATA, TARGET_APPNUM_GET, String.valueOf(type)); - - ServiceCommand> command = new ServiceCommand>(this, requestURL, null, responseListener); - command.setHttpMethod(ServiceCommand.TYPE_GET); - command.send(); - } - - private void getApplications(final int type, final int number, final AppListListener listener) { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - String strObj = (String) response; - - JSONArray applicationArray = parseApplicationsXmlToJSON(strObj); - List appList = new ArrayList(); - - for (int i = 0; i < applicationArray.length(); i++) - { - try { - final JSONObject appJSON = applicationArray.getJSONObject(i); - - AppInfo appInfo = new AppInfo() {{ - setId(appJSON.getString("id")); - setName(appJSON.getString("title")); - }}; - - appList.add(appInfo); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - if ( listener != null ) { - Util.postSuccess(listener, appList); - } - } - - @Override - public void onError(ServiceCommandError error) { - if ( listener != null ) { -// listener.onGetApplicationsFailed(error) - } - } - }; - - String requestURL = getUDAPRequestURL(UDAP_PATH_DATA, TARGET_APPLIST_GET, String.valueOf(type), "0", String.valueOf(number)); - - ServiceCommand> command = new ServiceCommand>(this, requestURL, null, responseListener); - command.setHttpMethod(ServiceCommand.TYPE_GET); - command.send(); - } - - @Override - public void getAppList(final AppListListener listener) { - applications.clear(); - getTotalNumberOfApplications(2, new AppCountListener() { - - @Override - public void onSuccess(final Integer count) { - getApplications(2, count, new AppListListener() { - - @Override - public void onSuccess(List apps) { - applications.addAll(apps); - - getTotalNumberOfApplications(3, new AppCountListener() { - - @Override - public void onSuccess(final Integer count) { - getApplications(3, count, new AppListListener() { - - @Override - public void onSuccess(List apps) { - applications.addAll(apps); - - Util.postSuccess(listener, applications); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - } - - @Override - public void getRunningApp(AppInfoListener listener) { - // Do nothing - Not Supported - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public ServiceSubscription subscribeRunningApp(AppInfoListener listener) { - // Do nothing - Not Supported - Util.postError(listener, ServiceCommandError.notSupported()); - - return new NotSupportedServiceSubscription(); - } - - @Override - public void getAppState(final LaunchSession launchSession, final AppStateListener listener) { - String requestURL = String.format(Locale.US, "%s%s", - getUDAPRequestURL(UDAP_PATH_APPTOAPP_DATA), - String.format(Locale.US, "/%s/status", launchSession.getAppId())); - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - String response = (String) object; - AppState appState; - if (response.equalsIgnoreCase("NONE")) - appState = new AppState(false, false); - else if (response.equalsIgnoreCase("LOAD")) - appState = new AppState(false, true); - else if (response.equalsIgnoreCase("RUN_NF")) - appState = new AppState(true, false); - else if (response.equalsIgnoreCase("TERM")) - appState = new AppState(false, true); - else - appState = new AppState(false, false); - - Util.postSuccess(listener, appState); - } - }; - - ServiceCommand> command = new ServiceCommand>(this, requestURL, null, responseListener); - command.setHttpMethod(ServiceCommand.TYPE_GET); - command.send(); - } - - @Override - public ServiceSubscription subscribeAppState(LaunchSession launchSession, AppStateListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - - return null; - } - - - /****************** - TV CONTROL - *****************/ - @Override - public TVControl getTVControl() { - return this; - } - - @Override - public CapabilityPriorityLevel getTVControlCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void getChannelList(final ChannelListListener listener) { - String requestURL = getUDAPRequestURL(UDAP_PATH_DATA, TARGET_CHANNEL_LIST); - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - String strObj = (String)response; - - try { - SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); - InputStream stream = new ByteArrayInputStream(strObj.getBytes("UTF-8")); - SAXParser saxParser = saxParserFactory.newSAXParser(); - - NetcastChannelParser parser = new NetcastChannelParser(); - saxParser.parse(stream, parser); - - JSONArray channelArray = parser.getJSONChannelArray(); - ArrayList channelList = new ArrayList(); - - for (int i = 0; i < channelArray.length(); i++) { - JSONObject rawData; - try { - rawData = (JSONObject) channelArray.get(i); - - ChannelInfo channel = NetcastChannelParser.parseRawChannelData(rawData); - channelList.add(channel); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - Util.postSuccess(listener, channelList); - } catch (ParserConfigurationException e) { - e.printStackTrace(); - } catch (SAXException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - ServiceCommand> request = new ServiceCommand>(this, requestURL, null, responseListener); - request.setHttpMethod(ServiceCommand.TYPE_GET); - request.send(); - } - - @Override - public void channelUp(ResponseListener listener) { - sendKeyCode(VirtualKeycodes.CHANNEL_UP.getCode(), listener); - } - - @Override - public void channelDown(ResponseListener listener) { - sendKeyCode(VirtualKeycodes.CHANNEL_DOWN.getCode(), listener); - } - - @Override - public void setChannel(final ChannelInfo channelInfo, final ResponseListener listener) { - getChannelList(new ChannelListListener() { - - @Override - public void onSuccess(List channelList) { - String requestURL = getUDAPRequestURL(UDAP_PATH_COMMAND); - - Map params = new HashMap(); - - for (int i = 0; i < channelList.size(); i++) { - ChannelInfo ch = channelList.get(i); - JSONObject rawData = ch.getRawData(); - - try { - String major = channelInfo.getNumber().split("-")[0]; - String minor = channelInfo.getNumber().split("-")[1]; - - int majorNumber = ch.getMajorNumber(); - int minorNumber = ch.getMinorNumber(); - - String sourceIndex = (String) rawData.get("sourceIndex"); - int physicalNum = (Integer) rawData.get("physicalNumber"); - - if ( Integer.valueOf(major) == majorNumber - && Integer.valueOf(minor) == minorNumber ) { - params.put("name", "HandleChannelChange"); - params.put("major", major); - params.put("minor", minor); - params.put("sourceIndex", sourceIndex); - params.put("physicalNum", String.valueOf(physicalNum)); - - break; - } - } catch (JSONException e) { - e.printStackTrace(); - } - } - - String httpMessage = getUDAPMessageBody(UDAP_API_COMMAND, params); - - ServiceCommand> request = new ServiceCommand>(NetcastTVService.this, requestURL, httpMessage, listener); - request.send(); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - } - - @Override - public void getCurrentChannel(final ChannelListener listener) { - String requestURL = getUDAPRequestURL(UDAP_PATH_DATA, TARGET_CURRENT_CHANNEL); - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - String strObj = (String)response; - - try { - SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); - InputStream stream = new ByteArrayInputStream(strObj.getBytes("UTF-8")); - SAXParser saxParser = saxParserFactory.newSAXParser(); - - NetcastChannelParser parser = new NetcastChannelParser(); - saxParser.parse(stream, parser); - - JSONArray channelArray = parser.getJSONChannelArray(); - - if ( channelArray.length() > 0 ) { - JSONObject rawData = (JSONObject) channelArray.get(0); - - ChannelInfo channel = NetcastChannelParser.parseRawChannelData(rawData); - - Util.postSuccess(listener, channel); - } - } catch (ParserConfigurationException e) { - e.printStackTrace(); - } catch (SAXException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - ServiceCommand> request = new ServiceCommand>(this, requestURL, null, responseListener); - request.send(); - } - - @Override - public ServiceSubscription subscribeCurrentChannel(final ChannelListener listener) { - getCurrentChannel(listener); // This is for the initial Current TV Channel Info. - - URLServiceSubscription request = new URLServiceSubscription(this, "ChannelChanged", null, null); - request.setHttpMethod(ServiceCommand.TYPE_GET); - request.addListener(listener); - addSubscription(request); - - return request; - } - - @Override - public void getProgramInfo(ProgramInfoListener listener) { - // Do nothing - Not Supported - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public ServiceSubscription subscribeProgramInfo(ProgramInfoListener listener) { - // Do nothing - Not Supported - Util.postError(listener, ServiceCommandError.notSupported()); - - return null; - } - - @Override - public void getProgramList(ProgramListListener listener) { - // Do nothing - Not Supported - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public ServiceSubscription subscribeProgramList(ProgramListListener listener) { - // Do nothing - Not Supported - Util.postError(listener, ServiceCommandError.notSupported()); - - return null; - } - - @Override - public void set3DEnabled(final boolean enabled, final ResponseListener listener) { - get3DEnabled(new State3DModeListener() { - - @Override - public void onSuccess(Boolean isEnabled) { - if ( enabled != isEnabled ) { - sendKeyCode(VirtualKeycodes.VIDEO_3D.getCode(), listener); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - } - - @Override - public void get3DEnabled(final State3DModeListener listener) { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - String strObj = (String) response; - String upperStr = strObj.toUpperCase(Locale.ENGLISH); - - Util.postSuccess(listener, upperStr.contains("TRUE")); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - String requestURL = getUDAPRequestURL(UDAP_PATH_DATA, TARGET_IS_3D); - - ServiceCommand> request = new ServiceCommand>(this, requestURL, null, responseListener); - request.setHttpMethod(ServiceCommand.TYPE_GET); - request.send(); - } - - @Override - public ServiceSubscription subscribe3DEnabled(final State3DModeListener listener) { - get3DEnabled(listener); - - URLServiceSubscription request = new URLServiceSubscription(this, TARGET_3D_MODE, null, null); - request.setHttpMethod(ServiceCommand.TYPE_GET); - request.addListener(listener); - - addSubscription(request); - - return request; - } - - - - /************** - VOLUME - **************/ - @Override - public VolumeControl getVolumeControl() { - return this; - }; - - @Override - public CapabilityPriorityLevel getVolumeControlCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void volumeUp(ResponseListener listener) { - sendKeyCode(VirtualKeycodes.VOLUME_UP.getCode(), listener); - } - - @Override - public void volumeDown(ResponseListener listener) { - sendKeyCode(VirtualKeycodes.VOLUME_DOWN.getCode(), listener); - } - - @Override - public void setVolume(float volume, ResponseListener listener) { - // Do nothing - not supported - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void getVolume(final VolumeListener listener) { - getVolumeStatus(new VolumeStatusListener() { - - @Override - public void onSuccess(VolumeStatus status) { - Util.postSuccess(listener, status.volume); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - } - - @Override - public void setMute(final boolean isMute, final ResponseListener listener) { - getVolumeStatus(new VolumeStatusListener() { - - @Override - public void onSuccess(VolumeStatus status) { - if ( isMute != status.isMute ) { - sendKeyCode(VirtualKeycodes.MUTE.getCode(), listener); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - } - - @Override - public void getMute(final MuteListener listener) { - getVolumeStatus(new VolumeStatusListener() { - - @Override - public void onSuccess(VolumeStatus status) { - Util.postSuccess(listener, status.isMute); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - } - - @Override - public ServiceSubscription subscribeVolume(VolumeListener listener) { - // Do nothing - not supported - Util.postError(listener, ServiceCommandError.notSupported()); - - return null; - } - - @Override - public ServiceSubscription subscribeMute(MuteListener listener) { - // Do nothing - not supported - Util.postError(listener, ServiceCommandError.notSupported()); - - return null; - } - - private void getVolumeStatus(final VolumeStatusListener listener) { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - String strObj = (String) response; - JSONObject volumeStatus = parseVolumeXmlToJSON(strObj); - try { - boolean isMute = (Boolean) volumeStatus.get("mute"); - int volume = (Integer) volumeStatus.get("level"); - - Util.postSuccess(listener, new VolumeStatus(isMute, volume)); - } catch (JSONException e) { - e.printStackTrace(); - } - - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - String requestURL = getUDAPRequestURL(UDAP_PATH_DATA, TARGET_VOLUME_INFO); - - ServiceCommand> request = new ServiceCommand>(this, requestURL, null, responseListener); - request.setHttpMethod(ServiceCommand.TYPE_GET); - request.send(); - } - - /************** - EXTERNAL INPUT - **************/ - @Override - public ExternalInputControl getExternalInput() { - return this; - }; - - @Override - public CapabilityPriorityLevel getExternalInputControlPriorityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void launchInputPicker(final AppLaunchListener listener) { - final String appName = "Input List"; - final String encodedStr = HttpMessage.encode(appName); - - getApplication(encodedStr, new AppInfoListener() { - - @Override - public void onSuccess(final AppInfo appInfo) { - Launcher.AppLaunchListener launchListener = new Launcher.AppLaunchListener() { - - @Override - public void onSuccess(LaunchSession session) { - if ( inputPickerSession == null ) { - inputPickerSession = session; - } - - Util.postSuccess(listener, session); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - launchApplication(appName, appInfo.getId(), null, launchListener); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - } - - @Override - public void closeInputPicker(LaunchSession launchSession, ResponseListener listener) { - this.getKeyControl().sendKeyCode(VirtualKeycodes.EXIT.getCode(), listener); - } - - @Override - public void getExternalInputList(ExternalInputListListener listener) { - // Do nothing - not Supported - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void setExternalInput(ExternalInputInfo input, ResponseListener listener) { - // Do nothing - not Supported - Util.postError(listener, ServiceCommandError.notSupported()); - } - - - /****************** - MEDIA PLAYER - *****************/ - @Override - public MediaPlayer getMediaPlayer() { - return this; - }; - - @Override - public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void displayImage(final String url, final String mimeType, final String title, final String description, final String iconSrc, final MediaPlayer.LaunchListener listener) { - if ( getDLNAService() != null ) { - final MediaPlayer.LaunchListener launchListener = new LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) - Util.postError(listener, error); - } - - @Override - public void onSuccess(MediaLaunchObject object) { - object.launchSession.setAppId("SmartShareª"); - object.launchSession.setAppName("SmartShareª"); - - object.mediaControl = NetcastTVService.this.getMediaControl(); - - if (listener != null) - Util.postSuccess(listener, object); - } - }; - - getDLNAService().displayImage(url, mimeType, title, description, iconSrc, launchListener); - } - else { - System.err.println("DLNA Service is not ready yet"); - } - } - - @Override - public void displayImage(final MediaInfo mediaInfo, final LaunchListener listener) { - - if ( dlnaService != null ) { - final MediaPlayer.LaunchListener launchListener = new LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) - Util.postError(listener, error); - } - - @Override - public void onSuccess(MediaLaunchObject object) { - object.launchSession.setAppId("SmartShareª"); - object.launchSession.setAppName("SmartShareª"); - - object.mediaControl = NetcastTVService.this.getMediaControl(); - - if (listener != null) - Util.postSuccess(listener, object); - } - }; - - getDLNAService().displayImage(mediaInfo, launchListener); - } - else { - System.err.println("DLNA Service is not ready yet"); - } - - } - - @Override - public void playMedia(final String url, final String mimeType, final String title, final String description, final String iconSrc, final boolean shouldLoop, final MediaPlayer.LaunchListener listener) { - if ( getDLNAService() != null ) { - final MediaPlayer.LaunchListener launchListener = new LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) - Util.postError(listener, error); - } - - @Override - public void onSuccess(MediaLaunchObject object) { - object.launchSession.setAppId("SmartShareª"); - object.launchSession.setAppName("SmartShareª"); - - object.mediaControl = NetcastTVService.this.getMediaControl(); - - if (listener != null) - Util.postSuccess(listener, object); - } - }; - - getDLNAService().playMedia(url, mimeType, title, description, iconSrc, shouldLoop, launchListener); - } - else { - System.err.println("DLNA Service is not ready yet"); - } - } - - @Override - public void playMedia(final MediaInfo mediaInfo, final boolean shouldLoop, final MediaPlayer.LaunchListener listener) { - if ( getDLNAService() != null ) { - final MediaPlayer.LaunchListener launchListener = new LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) - Util.postError(listener, error); - } - - @Override - public void onSuccess(MediaLaunchObject object) { - object.launchSession.setAppId("SmartShareª"); - object.launchSession.setAppName("SmartShareª"); - - object.mediaControl = NetcastTVService.this.getMediaControl(); - - if (listener != null) - Util.postSuccess(listener, object); - } - }; - - getDLNAService().playMedia(mediaInfo, shouldLoop, launchListener); - } - else { - System.err.println("DLNA Service is not ready yet"); - } - } - - @Override - public void closeMedia(LaunchSession launchSession, ResponseListener listener) { - if (getDLNAService() == null) { - Util.postError(listener, new ServiceCommandError(0, "Service is not connected", null)); - return; - } - - getDLNAService().closeMedia(launchSession, listener); - } - - /****************** - MEDIA CONTROL - *****************/ - @Override - public MediaControl getMediaControl() { - if (DiscoveryManager.getInstance().getPairingLevel() == PairingLevel.OFF) - return this.getDLNAService(); - else - return this; - }; - - @Override - public CapabilityPriorityLevel getMediaControlCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; - } - - @Override - public void play(ResponseListener listener) { - sendKeyCode(VirtualKeycodes.PLAY.getCode(), listener); - } - - @Override - public void pause(ResponseListener listener) { - sendKeyCode(VirtualKeycodes.PAUSE.getCode(), listener); - } - - @Override - public void stop(final ResponseListener listener) { - sendKeyCode(VirtualKeycodes.STOP.getCode(), listener); - } - - @Override - public void rewind(ResponseListener listener) { - sendKeyCode(VirtualKeycodes.REWIND.getCode(), listener); - } - - @Override - public void fastForward(ResponseListener listener) { - sendKeyCode(VirtualKeycodes.FAST_FORWARD.getCode(), listener); - } - - @Override - public void seek(long position, ResponseListener listener) { - if ( getDLNAService() != null ) { - getDLNAService().seek(position, listener); - } else { - if (listener != null) - Util.postError(listener, new ServiceCommandError(-1, "Command is not supported", null)); - } - } - - @Override - public void getDuration(DurationListener listener) { - if ( getDLNAService() != null ) { - getDLNAService().getDuration(listener); - } else { - if (listener != null) - Util.postError(listener, new ServiceCommandError(-1, "Command is not supported", null)); - } - } - - @Override - public void getPosition(PositionListener listener) { - if ( getDLNAService() != null ) { - getDLNAService().getPosition(listener); - } else { - if (listener != null) - Util.postError(listener, new ServiceCommandError(-1, "Command is not supported", null)); - } - } - - @Override - public void getPlayState(PlayStateListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public ServiceSubscription subscribePlayState(PlayStateListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - return null; - } - - /************** - MOUSE CONTROL - **************/ - @Override - public MouseControl getMouseControl() { - return this; - }; - - @Override - public CapabilityPriorityLevel getMouseControlCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - private void setMouseCursorVisible(boolean visible, ResponseListener listener) { - String requestURL = getUDAPRequestURL(UDAP_PATH_EVENT); - - Map params = new HashMap(); - params.put("name", "CursorVisible"); - params.put("value", visible? "true" : "false"); - params.put("mode", "auto"); - - String httpMessage = getUDAPMessageBody(UDAP_API_EVENT, params); - - ServiceCommand> request = new ServiceCommand>(this, requestURL, httpMessage, listener); - request.send(); - } - - @Override - public void connectMouse() { - ResponseListener listener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - Log.d("Connect SDK", "Netcast TV's mouse has been connected"); - - mMouseDistance = new PointF(0, 0); - mMouseIsMoving = false; - } - - @Override - public void onError(ServiceCommandError error) { - Log.w("Connect SDK", "Netcast TV's mouse connection has been failed"); - } - }; - - setMouseCursorVisible(true, listener); - } - - @Override - public void disconnectMouse() { - setMouseCursorVisible(false, null); - } - - @Override - public void click() { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - } - - @Override - public void onError(ServiceCommandError error) { - Log.w("Connect SDK", "Netcast TV's mouse click has been failed"); - } - }; - - String requestURL = getUDAPRequestURL(UDAP_PATH_COMMAND); - - Map params = new HashMap(); - params.put("name", "HandleTouchClick"); - - String httpMessage = getUDAPMessageBody(UDAP_API_COMMAND, params); - - ServiceCommand> request = new ServiceCommand>(this, requestURL, httpMessage, responseListener); - request.send(); - } - - @Override - public void move(double dx, double dy) { - mMouseDistance.x += dx; - mMouseDistance.y += dy; - - if (!mMouseIsMoving) - { - mMouseIsMoving = true; - this.moveMouse(); - } - } - - private void moveMouse() { - String requestURL = getUDAPRequestURL(UDAP_PATH_COMMAND); - - int x = (int)mMouseDistance.x; - int y = (int)mMouseDistance.y; - - Map params = new HashMap(); - params.put("name", "HandleTouchMove"); - params.put("x", String.valueOf(x)); - params.put("y", String.valueOf(y)); - - mMouseDistance.x = mMouseDistance.y = 0; - - final NetcastTVService mouseService = this; - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - if (mMouseDistance.x > 0 || mMouseDistance.y > 0) - mouseService.moveMouse(); - else - mMouseIsMoving = false; - } - - @Override - public void onError(ServiceCommandError error) { - Log.w("Connect SDK", "Netcast TV's mouse move has failed"); - - mMouseIsMoving = false; - } - }; - - String httpMessage = getUDAPMessageBody(UDAP_API_COMMAND, params); - - ServiceCommand> request = new ServiceCommand>(this, requestURL, httpMessage, responseListener); - request.send(); - } - - @Override - public void move(PointF diff) { - move(diff.x, diff.y); - } - - @Override - public void scroll(double dx, double dy) { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - - } - - @Override - public void onError(ServiceCommandError error) { - Log.w("Connect SDK", "Netcast TV's mouse scroll has been failed"); - } - }; - - String requestURL = getUDAPRequestURL(UDAP_PATH_COMMAND); - - Map params = new HashMap(); - params.put("name", "HandleTouchWheel"); - if ( dy > 0 ) - params.put("value", "up"); - else - params.put("value", "down"); - - String httpMessage = getUDAPMessageBody(UDAP_API_COMMAND, params); - - ServiceCommand> request = new ServiceCommand>(this, requestURL, httpMessage, responseListener); - request.send(); - } - - @Override - public void scroll(PointF diff) { - scroll(diff.x, diff.y); - } - - - /************** - KEYBOARD CONTROL - **************/ - @Override - public TextInputControl getTextInputControl() { - return this; - }; - - @Override - public CapabilityPriorityLevel getTextInputControlCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public ServiceSubscription subscribeTextInputStatus(final TextInputStatusListener listener) { - keyboardString = new StringBuilder(); - - URLServiceSubscription request = new URLServiceSubscription(this, "KeyboardVisible", null, null); - request.addListener(listener); - - addSubscription(request); - - return request; - } - - @Override - public void sendText(final String input) { - Log.d("Connect SDK", "Add to Queue: " + input); - keyboardString.append(input); - handleKeyboardInput("Editing", keyboardString.toString()); - } - - @Override - public void sendEnter() { - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - - } - - @Override - public void onError(ServiceCommandError error) { - Log.w("Connect SDK", "Netcast TV's enter key has been failed"); - } - }; - handleKeyboardInput("EditEnd", keyboardString.toString()); - sendKeyCode(VirtualKeycodes.RED.getCode(), responseListener); // Send RED Key to enter the "ENTER" button - } - - @Override - public void sendDelete() { - if ( keyboardString.length() > 1 ) { - keyboardString.deleteCharAt(keyboardString.length()-1); - } - else { - keyboardString = new StringBuilder(); - } - - handleKeyboardInput("Editing", keyboardString.toString()); - } - - private ResponseListener mTextChangedListener = new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { } - - @Override - public void onSuccess(String newValue) { - keyboardString = new StringBuilder(newValue); - } - }; - - private void handleKeyboardInput(final String state, final String buffer) { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - - } - - @Override - public void onError(ServiceCommandError error) { - Log.w("Connect SDK", "Netcast TV's keyboard input has been failed"); - } - }; - - String requestURL = getUDAPRequestURL(UDAP_PATH_EVENT); - - Map params = new HashMap(); - params.put("name", "TextEdited"); - params.put("state", state); - params.put("value", buffer); - - String httpMessage = getUDAPMessageBody(UDAP_API_EVENT, params); - - ServiceCommand> request = new ServiceCommand>(this, requestURL, httpMessage, responseListener); - request.send(); - } - - - /************** - KEY CONTROL - **************/ - @Override - public KeyControl getKeyControl() { - return this; - } - - @Override - public CapabilityPriorityLevel getKeyControlCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void up(final ResponseListener listener) { - sendKeyCode(VirtualKeycodes.KEY_UP.getCode(), listener); - } - - @Override - public void down(final ResponseListener listener) { - sendKeyCode(VirtualKeycodes.KEY_DOWN.getCode(), listener); - } - - @Override - public void left(final ResponseListener listener) { - sendKeyCode(VirtualKeycodes.KEY_LEFT.getCode(), listener); - } - - @Override - public void right(final ResponseListener listener) { - sendKeyCode(VirtualKeycodes.KEY_RIGHT.getCode(), listener); - } - - @Override - public void ok(final ResponseListener listener) { - sendKeyCode(VirtualKeycodes.OK.getCode(), listener); - } - - @Override - public void back(final ResponseListener listener) { - sendKeyCode(VirtualKeycodes.BACK.getCode(), listener); - } - - @Override - public void home(final ResponseListener listener) { - sendKeyCode(VirtualKeycodes.HOME.getCode(), listener); - } - - - /************** - POWER CONTROL - **************/ - @Override - public PowerControl getPowerControl() { - return this; - }; - - @Override - public CapabilityPriorityLevel getPowerControlCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void powerOff(ResponseListener listener) { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - - } - - @Override - public void onError(ServiceCommandError error) { - Log.w("Connect SDK", "Netcast TV's power off has been failed"); - } - }; - - sendKeyCode(VirtualKeycodes.POWER.getCode(), responseListener); - } - - @Override - public void powerOn(ResponseListener listener) { - if (listener != null) - listener.onError(ServiceCommandError.notSupported()); - } - - private JSONObject parseVolumeXmlToJSON(String data) { - SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); - try { - InputStream stream = new ByteArrayInputStream(data.getBytes("UTF-8")); - - SAXParser saxParser = saxParserFactory.newSAXParser(); - NetcastVolumeParser handler = new NetcastVolumeParser(); - saxParser.parse(stream, handler); - - return handler.getVolumeStatus(); - } catch (ParserConfigurationException e) { - e.printStackTrace(); - } catch (SAXException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - return null; - } - - private int parseAppNumberXmlToJSON(String data) { - SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); - try { - InputStream stream = new ByteArrayInputStream(data.getBytes("UTF-8")); - - SAXParser saxParser = saxParserFactory.newSAXParser(); - NetcastAppNumberParser handler = new NetcastAppNumberParser(); - saxParser.parse(stream, handler); - - return handler.getApplicationNumber(); - } catch (ParserConfigurationException e) { - e.printStackTrace(); - } catch (SAXException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - return 0; - } - - private JSONArray parseApplicationsXmlToJSON(String data) { - SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); - try { - InputStream stream = new ByteArrayInputStream(data.getBytes("UTF-8")); - - SAXParser saxParser = saxParserFactory.newSAXParser(); - NetcastApplicationsParser handler = new NetcastApplicationsParser(); - saxParser.parse(stream, handler); - - return handler.getApplications(); - } catch (ParserConfigurationException e) { - e.printStackTrace(); - } catch (SAXException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - return null; - } - - public String getHttpMessageForHandleKeyInput(final int keycode) { - String strKeycode = String.valueOf(keycode); - - Map params = new HashMap(); - params.put("name", "HandleKeyInput"); - params.put("value", strKeycode); - - return getUDAPMessageBody(UDAP_API_COMMAND, params); - } - - @Override - public void sendKeyCode(final int keycode, final ResponseListener listener) { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - try { - Thread.sleep(150); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - String requestURL = getUDAPRequestURL(UDAP_PATH_COMMAND); - String httpMessage = getHttpMessageForHandleKeyInput(keycode); - - ServiceCommand> request = new ServiceCommand>(NetcastTVService.this, requestURL, httpMessage, listener); - request.send(); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - setMouseCursorVisible(false, responseListener); - } - - private String getUDAPRequestURL(String path) { - return getUDAPRequestURL(path, null); - } - - private String getUDAPRequestURL(String path, String target) { - return getUDAPRequestURL(path, target, null); - } - - private String getUDAPRequestURL(String path, String target, String type) { - return getUDAPRequestURL(path, target, type, null, null); - } - - private String getUDAPRequestURL(String path, String target, String type, String index, String number) { - // Type Values - // 1: List of all apps - // 2: List of apps in the Premium category - // 3: List of apps in the My Apps category - - StringBuilder sb = new StringBuilder(); - sb.append("http://"); - sb.append(serviceDescription.getIpAddress()); - sb.append(":"); - sb.append(serviceDescription.getPort()); - sb.append(path); - - if ( target != null ) { - sb.append("?target="); - sb.append(target); - - if ( type != null ) { - sb.append("&type="); - sb.append(type); - } - - if ( index != null ) { - sb.append("&index="); - sb.append(index); - } - - if ( number != null ) { - sb.append("&number="); - sb.append(number); - } - } - - return sb.toString(); - } - - private String getUDAPMessageBody(String api, Map params) { - StringBuilder sb = new StringBuilder(); - sb.append(""); - sb.append(""); - sb.append(""); - - for (Map.Entry entry : params.entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - - sb.append(createNode(key, value)); - } - - sb.append(""); - sb.append(""); - - return sb.toString(); - } - - private String createNode(String tag, String value) { - StringBuilder sb = new StringBuilder(); - - sb.append("<" + tag + ">"); - sb.append(value); - sb.append(""); - - return sb.toString(); - } - - public String decToHex(String dec) { - if ( dec != null && dec.length() > 0 ) - return decToHex(Long.parseLong(dec)); - return null; - } - - public String decToHex(long dec) { - return String.format("%016x",dec); - } - - @Override - public void sendCommand(final ServiceCommand mCommand) { - Thread thread = new Thread(new Runnable() { - - @SuppressWarnings("unchecked") - @Override - public void run() { - final ServiceCommand> command = (ServiceCommand>) mCommand; - - Object payload = command.getPayload(); - - HttpRequestBase request = command.getRequest(); - request.addHeader(HttpMessage.USER_AGENT, HttpMessage.UDAP_USER_AGENT); - request.addHeader(HttpMessage.CONTENT_TYPE_HEADER, HttpMessage.CONTENT_TYPE_TEXT_XML); - HttpResponse response = null; - - if (payload != null && command.getHttpMethod().equalsIgnoreCase(ServiceCommand.TYPE_POST)) { - HttpEntity entity = null; - - try { - if (payload instanceof String) { - entity = new StringEntity((String) payload); - } else if (payload instanceof JSONObject) { - entity = new StringEntity(((JSONObject) payload).toString()); - } - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - - ((HttpPost) request).setEntity(entity); - } - - try { - response = httpClient.execute(request); - - final int code = response.getStatusLine().getStatusCode(); - - if ( code == 200 ) { - HttpEntity entity = response.getEntity(); - final String message = EntityUtils.toString(entity, "UTF-8"); - - Util.postSuccess(command.getResponseListener(), message); - } - else { - Util.postError(command.getResponseListener(), ServiceCommandError.getError(code)); - } - - response.getEntity().consumeContent(); - } catch (ClientProtocolException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } - }); - - thread.start(); - } - - private void addSubscription(URLServiceSubscription subscription) { - subscriptions.add(subscription); - - if (httpServer != null) - httpServer.setSubscriptions(subscriptions); - } - - @Override - public void unsubscribe(URLServiceSubscription subscription) { - subscriptions.remove(subscription); - - if (httpServer != null) - httpServer.setSubscriptions(subscriptions); - } - -// @Override -// public LaunchSession decodeLaunchSession(String type, JSONObject obj) throws JSONException { -// if ("netcasttv".equals(type)) { -// LaunchSession launchSession = LaunchSession.launchSessionFromJSONObject(obj); -// launchSession.setService(this); -// -// return launchSession; -// } -// return null; -// } - - @Override - protected void updateCapabilities() { - List capabilities = new ArrayList(); - - if (DiscoveryManager.getInstance().getPairingLevel() == PairingLevel.ON) { - for (String capability : TextInputControl.Capabilities) { capabilities.add(capability); } - for (String capability : MouseControl.Capabilities) { capabilities.add(capability); } - for (String capability : KeyControl.Capabilities) { capabilities.add(capability); } - for (String capability : MediaPlayer.Capabilities) { capabilities.add(capability); } - - capabilities.add(PowerControl.Off); - - capabilities.add(Play); - capabilities.add(Pause); - capabilities.add(Stop); - capabilities.add(Rewind); - capabilities.add(FastForward); - capabilities.add(Duration); - capabilities.add(Position); - capabilities.add(Seek); - capabilities.add(MetaData_Title); - capabilities.add(MetaData_MimeType); - - capabilities.add(Application); - capabilities.add(Application_Close); - capabilities.add(Application_List); - capabilities.add(Browser); - capabilities.add(Hulu); - capabilities.add(Netflix); - capabilities.add(Netflix_Params); - capabilities.add(YouTube); - capabilities.add(YouTube_Params); - capabilities.add(AppStore); - - capabilities.add(Channel_Up); - capabilities.add(Channel_Down); - capabilities.add(Channel_Get); - capabilities.add(Channel_List); - capabilities.add(Channel_Subscribe); - capabilities.add(Get_3D); - capabilities.add(Set_3D); - capabilities.add(Subscribe_3D); - - capabilities.add(Picker_Launch); - capabilities.add(Picker_Close); - - capabilities.add(Volume_Get); - capabilities.add(Volume_Up_Down); - capabilities.add(Mute_Get); - capabilities.add(Mute_Set); - - if (serviceDescription.getModelNumber().equals("4.0")) { - capabilities.add(AppStore_Params); - } - } else { - for (String capability : MediaPlayer.Capabilities) { capabilities.add(capability); } - - capabilities.add(Play); - capabilities.add(Pause); - capabilities.add(Stop); - capabilities.add(Rewind); - capabilities.add(FastForward); - - capabilities.add(YouTube); - capabilities.add(YouTube_Params); - } - - setCapabilities(capabilities); - } - - public DLNAService getDLNAService() { - if (dlnaService == null) { - DiscoveryManager discoveryManager = DiscoveryManager.getInstance(); - ConnectableDevice device = discoveryManager.getAllDevices().get(serviceDescription.getIpAddress()); - - if (device != null) { - DLNAService foundService = null; - - for (DeviceService service: device.getServices()) { - if (DLNAService.class.isAssignableFrom(service.getClass())) { - foundService = (DLNAService)service; - break; - } - } - - dlnaService = foundService; - } - } - - return dlnaService; - } - - public DIALService getDIALService() { - if (dialService == null) { - DiscoveryManager discoveryManager = DiscoveryManager.getInstance(); - ConnectableDevice device = discoveryManager.getAllDevices().get(serviceDescription.getIpAddress()); - - if (device != null) { - DIALService foundService = null; - - for (DeviceService service: device.getServices()) { - if (DIALService.class.isAssignableFrom(service.getClass())) { - foundService = (DIALService)service; - break; - } - } - - dialService = foundService; - } - } - - return dialService; - } -} diff --git a/src/com/connectsdk/service/RokuService.java b/src/com/connectsdk/service/RokuService.java deleted file mode 100644 index 0099c56b..00000000 --- a/src/com/connectsdk/service/RokuService.java +++ /dev/null @@ -1,1152 +0,0 @@ -/* - * RokuService - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Hyun Kook Khang on 26 Feb 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 java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpRequestBase; -import org.apache.http.conn.ClientConnectionManager; -import org.apache.http.entity.AbstractHttpEntity; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; -import org.apache.http.params.HttpParams; -import org.apache.http.util.EntityUtils; -import org.json.JSONException; -import org.json.JSONObject; -import org.xml.sax.SAXException; - -import android.util.Log; - -import com.connectsdk.core.AppInfo; -import com.connectsdk.core.MediaInfo; -import com.connectsdk.core.Util; -import com.connectsdk.device.ConnectableDevice; -import com.connectsdk.device.roku.RokuApplicationListParser; -import com.connectsdk.discovery.DiscoveryManager; -import com.connectsdk.etc.helper.DeviceServiceReachability; -import com.connectsdk.etc.helper.HttpMessage; -import com.connectsdk.service.capability.KeyControl; -import com.connectsdk.service.capability.Launcher; -import com.connectsdk.service.capability.MediaControl; -import com.connectsdk.service.capability.MediaPlayer; -import com.connectsdk.service.capability.TextInputControl; -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.command.NotSupportedServiceSubscription; -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; - -public class RokuService extends DeviceService implements Launcher, - MediaPlayer, MediaControl, KeyControl, TextInputControl { - - public static final String ID = "Roku"; - - private static List registeredApps = new ArrayList(); - - DIALService dialService; - - static { - registeredApps.add("YouTube"); - registeredApps.add("Netflix"); - registeredApps.add("Amazon"); - } - - public static void registerApp(String appId) { - if (!registeredApps.contains(appId)) - registeredApps.add(appId); - } - - HttpClient httpClient; - - public RokuService(ServiceDescription serviceDescription, - ServiceConfig serviceConfig) { - super(serviceDescription, serviceConfig); - - httpClient = new DefaultHttpClient(); - ClientConnectionManager mgr = httpClient.getConnectionManager(); - HttpParams params = httpClient.getParams(); - httpClient = new DefaultHttpClient(new ThreadSafeClientConnManager( - params, mgr.getSchemeRegistry()), params); - } - - @Override - public void setServiceDescription(ServiceDescription serviceDescription) { - super.setServiceDescription(serviceDescription); - - if (this.serviceDescription != null) - this.serviceDescription.setPort(8060); - - probeForAppSupport(); - } - - public static JSONObject discoveryParameters() { - JSONObject params = new JSONObject(); - - try { - params.put("serviceId", ID); - params.put("filter", "roku:ecp"); - } catch (JSONException e) { - e.printStackTrace(); - } - - return params; - } - - @Override - public Launcher getLauncher() { - return this; - } - - @Override - public CapabilityPriorityLevel getLauncherCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - class RokuLaunchSession extends LaunchSession { - String appName; - RokuService service; - - RokuLaunchSession(RokuService service) { - this.service = service; - } - - RokuLaunchSession(RokuService service, String appId, String appName) { - this.service = service; - this.appId = appId; - this.appName = appName; - } - - RokuLaunchSession(RokuService service, JSONObject obj) - throws JSONException { - this.service = service; - fromJSONObject(obj); - } - - public void close(ResponseListener responseListener) { - home(responseListener); - } - - @Override - public JSONObject toJSONObject() throws JSONException { - JSONObject obj = super.toJSONObject(); - obj.put("type", "roku"); - obj.put("appName", appName); - return obj; - } - - @Override - public void fromJSONObject(JSONObject obj) throws JSONException { - super.fromJSONObject(obj); - appName = obj.optString("appName"); - } - } - - @Override - public void launchApp(String appId, AppLaunchListener listener) { - if (appId == null) { - Util.postError(listener, new ServiceCommandError(0, - "Must supply a valid app id", null)); - return; - } - - AppInfo appInfo = new AppInfo(); - appInfo.setId(appId); - - launchAppWithInfo(appInfo, listener); - } - - @Override - public void launchAppWithInfo(AppInfo appInfo, - Launcher.AppLaunchListener listener) { - launchAppWithInfo(appInfo, null, listener); - } - - @Override - public void launchAppWithInfo(final AppInfo appInfo, Object params, - final Launcher.AppLaunchListener listener) { - if (appInfo == null || appInfo.getId() == null) { - if (listener != null) - Util.postError(listener, new ServiceCommandError(-1, - "Cannot launch app without valid AppInfo object", - appInfo)); - - return; - } - - String baseTargetURL = requestURL("launch", appInfo.getId()); - String queryParams = ""; - - if (params != null && params instanceof JSONObject) { - JSONObject jsonParams = (JSONObject) params; - - int count = 0; - Iterator jsonIterator = jsonParams.keys(); - - while (jsonIterator.hasNext()) { - String key = (String) jsonIterator.next(); - String value = null; - - try { - value = jsonParams.getString(key); - } catch (JSONException ex) { - } - - if (value == null) - continue; - - String urlSafeKey = null; - String urlSafeValue = null; - String prefix = (count == 0) ? "?" : "&"; - - try { - urlSafeKey = URLEncoder.encode(key, "UTF-8"); - urlSafeValue = URLEncoder.encode(value, "UTF-8"); - } catch (UnsupportedEncodingException ex) { - - } - - if (urlSafeKey == null || urlSafeValue == null) - continue; - - String appendString = prefix + urlSafeKey + "=" + urlSafeValue; - queryParams = queryParams + appendString; - - count++; - } - } - - String targetURL = null; - - if (queryParams.length() > 0) - targetURL = baseTargetURL + queryParams; - else - targetURL = baseTargetURL; - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - Util.postSuccess(listener, new RokuLaunchSession( - RokuService.this, appInfo.getId(), appInfo.getName())); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - ServiceCommand> request = new ServiceCommand>( - this, targetURL, null, responseListener); - request.send(); - } - - @Override - public void closeApp(LaunchSession launchSession, - ResponseListener listener) { - home(listener); - } - - @Override - public void getAppList(final AppListListener listener) { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - String msg = (String) response; - - SAXParserFactory saxParserFactory = SAXParserFactory - .newInstance(); - InputStream stream; - try { - stream = new ByteArrayInputStream(msg.getBytes("UTF-8")); - SAXParser saxParser = saxParserFactory.newSAXParser(); - - RokuApplicationListParser parser = new RokuApplicationListParser(); - saxParser.parse(stream, parser); - - List appList = parser.getApplicationList(); - - Util.postSuccess(listener, appList); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } catch (ParserConfigurationException e) { - e.printStackTrace(); - } catch (SAXException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - String action = "query"; - String param = "apps"; - - String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, responseListener); - request.setHttpMethod(ServiceCommand.TYPE_GET); - request.send(); - } - - @Override - public void getRunningApp(AppInfoListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public ServiceSubscription subscribeRunningApp( - AppInfoListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - - return new NotSupportedServiceSubscription(); - } - - @Override - public void getAppState(LaunchSession launchSession, - AppStateListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public ServiceSubscription subscribeAppState( - LaunchSession launchSession, AppStateListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - - return null; - } - - @Override - public void launchBrowser(String url, Launcher.AppLaunchListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void launchYouTube(String contentId, - Launcher.AppLaunchListener listener) { - launchYouTube(contentId, (float) 0.0, listener); - } - - @Override - public void launchYouTube(String contentId, float startTime, - AppLaunchListener listener) { - if (getDIALService() != null) { - getDIALService().getLauncher().launchYouTube(contentId, startTime, - listener); - } else { - if (listener != null) { - listener.onError(new ServiceCommandError( - 0, - "Cannot reach DIAL service for launching with provided start time", - null)); - } - } - } - - @Override - public void launchNetflix(final String contentId, - final Launcher.AppLaunchListener listener) { - getAppList(new AppListListener() { - - @Override - public void onSuccess(List appList) { - for (AppInfo appInfo : appList) { - if (appInfo.getName().equalsIgnoreCase("Netflix")) { - JSONObject payload = new JSONObject(); - try { - payload.put("mediaType", "movie"); - - if (contentId != null && contentId.length() > 0) - payload.put("contentId", contentId); - } catch (JSONException e) { - e.printStackTrace(); - } - launchAppWithInfo(appInfo, payload, listener); - break; - } - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - } - - @Override - public void launchHulu(final String contentId, - final Launcher.AppLaunchListener listener) { - getAppList(new AppListListener() { - - @Override - public void onSuccess(List appList) { - for (AppInfo appInfo : appList) { - if (appInfo.getName().contains("Hulu")) { - JSONObject payload = new JSONObject(); - try { - payload.put("contentId", contentId); - } catch (JSONException e) { - e.printStackTrace(); - } - launchAppWithInfo(appInfo, payload, listener); - break; - } - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - } - - @Override - public void launchAppStore(final String appId, AppLaunchListener listener) { - AppInfo appInfo = new AppInfo("11"); - appInfo.setName("Channel Store"); - - JSONObject params = null; - try { - params = new JSONObject() { - { - put("contentId", appId); - } - }; - } catch (JSONException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - launchAppWithInfo(appInfo, params, listener); - } - - @Override - public KeyControl getKeyControl() { - return this; - } - - @Override - public CapabilityPriorityLevel getKeyControlCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; - } - - @Override - public void up(ResponseListener listener) { - String action = "keypress"; - String param = "Up"; - - String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, listener); - request.send(); - } - - @Override - public void down(final ResponseListener listener) { - String action = "keypress"; - String param = "Down"; - - String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, listener); - request.send(); - } - - @Override - public void left(ResponseListener listener) { - String action = "keypress"; - String param = "Left"; - - String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, listener); - request.send(); - } - - @Override - public void right(ResponseListener listener) { - String action = "keypress"; - String param = "Right"; - - String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, listener); - request.send(); - } - - @Override - public void ok(final ResponseListener listener) { - String action = "keypress"; - String param = "Select"; - - String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, listener); - request.send(); - } - - @Override - public void back(ResponseListener listener) { - String action = "keypress"; - String param = "Back"; - - String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, listener); - request.send(); - } - - @Override - public void home(ResponseListener listener) { - String action = "keypress"; - String param = "Home"; - - String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, listener); - request.send(); - } - - @Override - public MediaControl getMediaControl() { - return this; - } - - @Override - public CapabilityPriorityLevel getMediaControlCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; - } - - @Override - public void play(ResponseListener listener) { - String action = "keypress"; - String param = "Play"; - - String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, listener); - request.send(); - } - - @Override - public void pause(ResponseListener listener) { - String action = "keypress"; - String param = "Play"; - - String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, listener); - request.send(); - } - - @Override - public void stop(ResponseListener listener) { - String action = null; - String param = "input?a=sto"; - - String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, listener); - request.send(); - } - - @Override - public void rewind(ResponseListener listener) { - String action = "keypress"; - String param = "Rev"; - - String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, listener); - request.send(); - } - - @Override - public void fastForward(ResponseListener listener) { - String action = "keypress"; - String param = "Fwd"; - - String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, listener); - request.send(); - } - - @Override - public void getDuration(DurationListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void getPosition(PositionListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void seek(long position, ResponseListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public MediaPlayer getMediaPlayer() { - return this; - } - - @Override - public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; - } - - private void displayMedia(String url, String mimeType, String title, - String description, String iconSrc, - final MediaPlayer.LaunchListener listener) { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - Util.postSuccess(listener, new MediaLaunchObject( - new RokuLaunchSession(RokuService.this), - RokuService.this)); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - String host = String.format("%s:%s", serviceDescription.getIpAddress(), - serviceDescription.getPort()); - - String action = "input"; - String mediaFormat = mimeType; - if (mimeType.contains("/")) { - int index = mimeType.indexOf("/") + 1; - mediaFormat = mimeType.substring(index); - } - - String param; - if (mimeType.contains("image")) { - param = String.format("15985?t=p&u=%s&h=%s&tr=crossfade", - HttpMessage.encode(url), HttpMessage.encode(host)); - } else if (mimeType.contains("video")) { - param = String.format( - "15985?t=v&u=%s&k=(null)&h=%s&videoName=%s&videoFormat=%s", - HttpMessage.encode(url), HttpMessage.encode(host), - HttpMessage.encode(title), HttpMessage.encode(mediaFormat)); - } else { // if (mimeType.contains("audio")) { - param = String - .format("15985?t=a&u=%s&k=(null)&h=%s&songname=%s&artistname=%s&songformat=%s&albumarturl=%s", - HttpMessage.encode(url), HttpMessage.encode(host), - HttpMessage.encode(title), - HttpMessage.encode(description), - HttpMessage.encode(mediaFormat), - HttpMessage.encode(iconSrc)); - } - - String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, responseListener); - request.send(); - } - - private void displayMedia(MediaInfo mediaInfo, - final MediaPlayer.LaunchListener listener) { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - Util.postSuccess(listener, new MediaLaunchObject( - new RokuLaunchSession(RokuService.this), - RokuService.this)); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - String host = String.format("%s:%s", serviceDescription.getIpAddress(), - serviceDescription.getPort()); - - String action = "input"; - String mediaFormat = mediaInfo.getMimeType(); - if (mediaInfo.getMimeType().contains("/")) { - int index = mediaInfo.getMimeType().indexOf("/") + 1; - mediaFormat = mediaInfo.getMimeType().substring(index); - } - - String param; - if (mediaInfo.getMimeType().contains("image")) { - param = String.format("15985?t=p&u=%s&h=%s&tr=crossfade", - HttpMessage.encode(mediaInfo.getUrl()), HttpMessage.encode(host)); - } else if (mediaInfo.getMimeType().contains("video")) { - param = String.format( - "15985?t=v&u=%s&k=(null)&h=%s&videoName=%s&videoFormat=%s", - HttpMessage.encode(mediaInfo.getUrl()), HttpMessage.encode(host), - HttpMessage.encode(mediaInfo.getTitle()), HttpMessage.encode(mediaFormat)); - } else { // if (mimeType.contains("audio")) { - param = String - .format("15985?t=a&u=%s&k=(null)&h=%s&songname=%s&artistname=%s&songformat=%s&albumarturl=%s", - HttpMessage.encode(mediaInfo.getUrl()), HttpMessage.encode(host), - HttpMessage.encode(mediaInfo.getTitle()), - HttpMessage.encode(mediaInfo.getDescription()), - HttpMessage.encode(mediaFormat), - HttpMessage.encode(mediaInfo.getImages().get(0).getUrl())); - } - - String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, responseListener); - request.send(); - } - - @Override - public void displayImage(String url, String mimeType, String title, - String description, String iconSrc, - MediaPlayer.LaunchListener listener) { - displayMedia(url, mimeType, title, description, iconSrc, listener); - } - - @Override - public void displayImage(MediaInfo mediaInfo, - MediaPlayer.LaunchListener listener) { - displayMedia(mediaInfo, listener); - } - - @Override - public void playMedia(String url, String mimeType, String title, - String description, String iconSrc, boolean shouldLoop, - MediaPlayer.LaunchListener listener) { - displayMedia(url, mimeType, title, description, iconSrc, listener); - } - - @Override - public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, - MediaPlayer.LaunchListener listener) { - displayMedia(mediaInfo, listener); - } - - @Override - public void closeMedia(LaunchSession launchSession, - ResponseListener listener) { - home(listener); - } - - @Override - public TextInputControl getTextInputControl() { - return this; - } - - @Override - public CapabilityPriorityLevel getTextInputControlCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; - } - - @Override - public ServiceSubscription subscribeTextInputStatus( - TextInputStatusListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - - return new NotSupportedServiceSubscription(); - } - - @Override - public void sendText(String input) { - if (input == null || input.length() == 0) { - return; - } - - ResponseListener listener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - // TODO Auto-generated method stub - - } - - @Override - public void onError(ServiceCommandError error) { - // TODO Auto-generated method stub - - } - }; - - String action = "keypress"; - String param = null; - try { - param = "Lit_" + URLEncoder.encode(input, "UTF-8"); - } catch (UnsupportedEncodingException e) { - // This can be safetly ignored since it isn't a dynamic encoding. - e.printStackTrace(); - } - - String uri = requestURL(action, param); - - Log.d("Connect SDK", "RokuService::send() | uri = " + uri); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, listener); - request.send(); - } - - @Override - public void sendKeyCode(int keyCode, ResponseListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void sendEnter() { - ResponseListener listener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - // TODO Auto-generated method stub - - } - - @Override - public void onError(ServiceCommandError error) { - // TODO Auto-generated method stub - - } - }; - - String action = "keypress"; - String param = "Enter"; - - String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, listener); - request.send(); - } - - @Override - public void sendDelete() { - ResponseListener listener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - // TODO Auto-generated method stub - - } - - @Override - public void onError(ServiceCommandError error) { - // TODO Auto-generated method stub - - } - }; - - String action = "keypress"; - String param = "Backspace"; - - String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, listener); - request.send(); - } - - @Override - public void unsubscribe(URLServiceSubscription subscription) { - } - - @Override - public void sendCommand(final ServiceCommand mCommand) { - Thread thread = new Thread(new Runnable() { - - @SuppressWarnings("unchecked") - @Override - public void run() { - ServiceCommand> command = (ServiceCommand>) mCommand; - Object payload = command.getPayload(); - - HttpRequestBase request = command.getRequest(); - HttpResponse response = null; - int code = -1; - - if (command.getHttpMethod().equalsIgnoreCase( - ServiceCommand.TYPE_POST)) { - HttpPost post = (HttpPost) request; - AbstractHttpEntity entity = null; - - if (payload != null) { - try { - if (payload instanceof JSONObject) { - entity = new StringEntity((String) payload); - } - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - // Error is handled below if entity is null; - } - - if (entity == null) { - Util.postError( - command.getResponseListener(), - new ServiceCommandError( - 0, - "Unknown Error while preparing to send message", - null)); - - return; - } - - post.setEntity(entity); - } - } - - try { - if (httpClient != null) { - response = httpClient.execute(request); - - code = response.getStatusLine().getStatusCode(); - - if (code == 200 || code == 201) { - HttpEntity entity = response.getEntity(); - String message = EntityUtils.toString(entity, - "UTF-8"); - - Util.postSuccess(command.getResponseListener(), - message); - } else { - Util.postError(command.getResponseListener(), - ServiceCommandError.getError(code)); - } - } - } catch (ClientProtocolException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } - }); - thread.start(); - } - - private String requestURL(String action, String parameter) { - StringBuilder sb = new StringBuilder(); - - sb.append("http://"); - sb.append(serviceDescription.getIpAddress()).append(":"); - sb.append(serviceDescription.getPort()).append("/"); - - if (action != null) - sb.append(action); - - if (parameter != null) - sb.append("/").append(parameter); - - return sb.toString(); - } - - private void probeForAppSupport() { - getAppList(new AppListListener() { - - @Override - public void onError(ServiceCommandError error) { - } - - @Override - public void onSuccess(List object) { - List appsToAdd = new ArrayList(); - - for (String probe : registeredApps) { - for (AppInfo app : object) { - if (app.getName().contains(probe)) { - appsToAdd.add("Launcher." + probe); - appsToAdd.add("Launcher." + probe + ".Params"); - } - } - } - - addCapabilities(appsToAdd); - } - }); - } - - @Override - protected void updateCapabilities() { - List capabilities = new ArrayList(); - - for (String capability : KeyControl.Capabilities) { - capabilities.add(capability); - } - - capabilities.add(Application); - - capabilities.add(Application_Params); - capabilities.add(Application_List); - capabilities.add(AppStore); - capabilities.add(AppStore_Params); - capabilities.add(Application_Close); - - capabilities.add(Display_Image); - capabilities.add(Display_Video); - capabilities.add(Display_Audio); - capabilities.add(Close); - capabilities.add(MetaData_Title); - - capabilities.add(FastForward); - capabilities.add(Rewind); - capabilities.add(Play); - capabilities.add(Pause); - - capabilities.add(Send); - capabilities.add(Send_Delete); - capabilities.add(Send_Enter); - - setCapabilities(capabilities); - } - - @Override - public void getPlayState(PlayStateListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public ServiceSubscription subscribePlayState( - PlayStateListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - - return null; - } - - @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); - } - - @Override - public void disconnect() { - connected = false; - - if (mServiceReachability != null) - mServiceReachability.stop(); - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - if (listener != null) - listener.onDisconnect(RokuService.this, null); - } - }); - } - - @Override - public void onLoseReachability(DeviceServiceReachability reachability) { - if (connected) { - disconnect(); - } else { - if (mServiceReachability != null) - mServiceReachability.stop(); - } - } - - public DIALService getDIALService() { - if (dialService == null) { - DiscoveryManager discoveryManager = DiscoveryManager.getInstance(); - ConnectableDevice device = discoveryManager.getAllDevices().get( - serviceDescription.getIpAddress()); - - if (device != null) { - DIALService foundService = null; - - for (DeviceService service : device.getServices()) { - if (DIALService.class.isAssignableFrom(service.getClass())) { - foundService = (DIALService) service; - break; - } - } - - dialService = foundService; - } - } - - return dialService; - } -} diff --git a/src/com/connectsdk/service/WebOSTVService.java b/src/com/connectsdk/service/WebOSTVService.java deleted file mode 100644 index 13895b0b..00000000 --- a/src/com/connectsdk/service/WebOSTVService.java +++ /dev/null @@ -1,2925 +0,0 @@ -/* - * WebOSTVService - * 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 java.io.ByteArrayOutputStream; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -import org.java_websocket.client.WebSocketClient; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.pm.PackageManager.NameNotFoundException; -import android.graphics.Bitmap; -import android.graphics.PointF; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.util.Base64; -import android.util.Log; - -import com.connectsdk.core.AppInfo; -import com.connectsdk.core.ChannelInfo; -import com.connectsdk.core.ExternalInputInfo; -import com.connectsdk.core.MediaInfo; -import com.connectsdk.core.ProgramList; -import com.connectsdk.core.Util; -import com.connectsdk.core.upnp.Device; -import com.connectsdk.device.ConnectableDevice; -import com.connectsdk.discovery.DiscoveryManager; -import com.connectsdk.discovery.DiscoveryManager.PairingLevel; -import com.connectsdk.service.capability.ExternalInputControl; -import com.connectsdk.service.capability.KeyControl; -import com.connectsdk.service.capability.Launcher; -import com.connectsdk.service.capability.MediaControl; -import com.connectsdk.service.capability.MediaPlayer; -import com.connectsdk.service.capability.MouseControl; -import com.connectsdk.service.capability.PowerControl; -import com.connectsdk.service.capability.TVControl; -import com.connectsdk.service.capability.TextInputControl; -import com.connectsdk.service.capability.ToastControl; -import com.connectsdk.service.capability.VolumeControl; -import com.connectsdk.service.capability.WebAppLauncher; -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.command.NotSupportedServiceSubscription; -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.config.WebOSTVServiceConfig; -import com.connectsdk.service.sessions.LaunchSession; -import com.connectsdk.service.sessions.LaunchSession.LaunchSessionType; -import com.connectsdk.service.sessions.WebAppSession; -import com.connectsdk.service.sessions.WebOSWebAppSession; -import com.connectsdk.service.webos.WebOSTVKeyboardInput; -import com.connectsdk.service.webos.WebOSTVMouseSocketConnection; -import com.connectsdk.service.webos.WebOSTVServiceSocketClient; -import com.connectsdk.service.webos.WebOSTVServiceSocketClient.WebOSTVServiceSocketClientListener; - -@SuppressLint("DefaultLocale") -public class WebOSTVService extends DeviceService implements Launcher, MediaControl, MediaPlayer, VolumeControl, TVControl, ToastControl, ExternalInputControl, MouseControl, TextInputControl, PowerControl, KeyControl, WebAppLauncher { - - public static final String ID = "webOS TV"; - - public interface WebOSTVServicePermission { - public enum Open implements WebOSTVServicePermission { - LAUNCH, - LAUNCH_WEB, - APP_TO_APP, - CONTROL_AUDIO, - CONTROL_INPUT_MEDIA_PLAYBACK; - } - - public static final WebOSTVServicePermission[] OPEN = { - Open.LAUNCH, - Open.LAUNCH_WEB, - Open.APP_TO_APP, - Open.CONTROL_AUDIO, - Open.CONTROL_INPUT_MEDIA_PLAYBACK - }; - - public enum Protected implements WebOSTVServicePermission { - CONTROL_POWER, - READ_INSTALLED_APPS, - CONTROL_DISPLAY, - CONTROL_INPUT_JOYSTICK, - CONTROL_INPUT_MEDIA_RECORDING, - CONTROL_INPUT_TV, - READ_INPUT_DEVICE_LIST, - READ_NETWORK_STATE, - READ_TV_CHANNEL_LIST, - WRITE_NOTIFICATION_TOAST - } - - public static final WebOSTVServicePermission[] PROTECTED = { - Protected.CONTROL_POWER, - Protected.READ_INSTALLED_APPS, - Protected.CONTROL_DISPLAY, - Protected.CONTROL_INPUT_JOYSTICK, - Protected.CONTROL_INPUT_MEDIA_RECORDING, - Protected.CONTROL_INPUT_TV, - Protected.READ_INPUT_DEVICE_LIST, - Protected.READ_NETWORK_STATE, - Protected.READ_TV_CHANNEL_LIST, - Protected.WRITE_NOTIFICATION_TOAST - }; - - public enum PersonalActivity implements WebOSTVServicePermission { - CONTROL_INPUT_TEXT, - CONTROL_MOUSE_AND_KEYBOARD, - READ_CURRENT_CHANNEL, - READ_RUNNING_APPS; - } - - public static final WebOSTVServicePermission[] PERSONAL_ACTIVITY = { - PersonalActivity.CONTROL_INPUT_TEXT, - PersonalActivity.CONTROL_MOUSE_AND_KEYBOARD, - PersonalActivity.READ_CURRENT_CHANNEL, - PersonalActivity.READ_RUNNING_APPS - }; - } - - public final static String[] kWebOSTVServiceOpenPermissions = { - "LAUNCH", - "LAUNCH_WEBAPP", - "APP_TO_APP", - "CONTROL_AUDIO", - "CONTROL_INPUT_MEDIA_PLAYBACK" - }; - - public final static String[] kWebOSTVServiceProtectedPermissions = { - "CONTROL_POWER", - "READ_INSTALLED_APPS", - "CONTROL_DISPLAY", - "CONTROL_INPUT_JOYSTICK", - "CONTROL_INPUT_MEDIA_RECORDING", - "CONTROL_INPUT_TV", - "READ_INPUT_DEVICE_LIST", - "READ_NETWORK_STATE", - "READ_TV_CHANNEL_LIST", - "WRITE_NOTIFICATION_TOAST" - }; - - public final static String[] kWebOSTVServicePersonalActivityPermissions = { - "CONTROL_INPUT_TEXT", - "CONTROL_MOUSE_AND_KEYBOARD", - "READ_CURRENT_CHANNEL", - "READ_RUNNING_APPS" - }; - - - public interface SecureAccessTestListener extends ResponseListener { } - - public interface ACRAuthTokenListener extends ResponseListener { } - - public interface LaunchPointsListener extends ResponseListener { } - - static String FOREGROUND_APP = "ssap://com.webos.applicationManager/getForegroundAppInfo"; - static String APP_STATUS = "ssap://com.webos.service.appstatus/getAppStatus"; - static String APP_STATE = "ssap://system.launcher/getAppState"; - static String VOLUME = "ssap://audio/getVolume"; - static String MUTE = "ssap://audio/getMute"; - static String VOLUME_STATUS = "ssap://audio/getStatus"; - static String CHANNEL_LIST = "ssap://tv/getChannelList"; - static String CHANNEL = "ssap://tv/getCurrentChannel"; - static String PROGRAM = "ssap://tv/getChannelProgramInfo"; - - static final String CLOSE_APP_URI = "ssap://system.launcher/close"; - static final String CLOSE_MEDIA_URI = "ssap://media.viewer/close"; - static final String CLOSE_WEBAPP_URI = "ssap://webapp/closeWebApp"; - - WebOSTVMouseSocketConnection mouseSocket; - - WebSocketClient mouseWebSocket; - WebOSTVKeyboardInput keyboardInput; - - ConcurrentHashMap mAppToAppIdMappings; - ConcurrentHashMap mWebAppSessions; - - WebOSTVServiceSocketClient socket; - PairingType pairingType; - - List permissions; - - public WebOSTVService(ServiceDescription serviceDescription, ServiceConfig serviceConfig) { - super(serviceDescription, serviceConfig); - - setServiceDescription(serviceDescription); - - pairingType = PairingType.FIRST_SCREEN; - - mAppToAppIdMappings = new ConcurrentHashMap(); - mWebAppSessions = new ConcurrentHashMap(); - } - - @Override - public void setServiceDescription(ServiceDescription serviceDescription) { - super.setServiceDescription(serviceDescription); - - if (this.serviceDescription.getVersion() == null && this.serviceDescription.getResponseHeaders() != null) - { - String serverInfo = serviceDescription.getResponseHeaders().get(Device.HEADER_SERVER).get(0); - String systemOS = serverInfo.split(" ")[0]; - String[] versionComponents = systemOS.split("/"); - String systemVersion = versionComponents[versionComponents.length - 1]; - - this.serviceDescription.setVersion(systemVersion); - - this.updateCapabilities(); - } - } - - private DeviceService getDLNAService() { - Map allDevices = DiscoveryManager.getInstance().getAllDevices(); - ConnectableDevice device = null; - DeviceService service = null; - - if (allDevices != null && allDevices.size() > 0) - device = allDevices.get(this.serviceDescription.getIpAddress()); - - if (device != null) - service = device.getServiceByName("DLNA"); - - return service; - } - - public static JSONObject discoveryParameters() { - JSONObject params = new JSONObject(); - - try { - params.put("serviceId", ID); - params.put("filter", "urn:lge-com:service:webos-second-screen:1"); - } catch (JSONException e) { - e.printStackTrace(); - } - - return params; - } - - @Override - public boolean isConnected() { - if (DiscoveryManager.getInstance().getPairingLevel() == PairingLevel.ON) { - return this.socket != null && this.socket.isConnected() && (((WebOSTVServiceConfig)serviceConfig).getClientKey() != null); - } else { - return this.socket != null && this.socket.isConnected(); - } - } - - @Override - public void connect() { - if (this.socket == null) { - this.socket = new WebOSTVServiceSocketClient(this, WebOSTVServiceSocketClient.getURI(this)); - this.socket.setListener(mSocketListener); - } - - if (!this.isConnected()) - this.socket.connect(); - } - - @Override - public void disconnect() { - Log.d("Connect SDK", "attempting to disconnect to " + serviceDescription.getIpAddress()); - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - if (listener != null) - listener.onDisconnect(WebOSTVService.this, null); - } - }); - - if (socket != null) { - socket.setListener(null); - socket.disconnect(); - socket = null; - } - - if (mAppToAppIdMappings != null) - mAppToAppIdMappings.clear(); - - if (mWebAppSessions != null) { - Enumeration iterator = mWebAppSessions.elements(); - - while (iterator.hasMoreElements()) { - WebOSWebAppSession session = iterator.nextElement(); - session.disconnectFromWebApp(); - } - - mWebAppSessions.clear(); - } - } - - @Override - public void cancelPairing() { - if (this.socket != null) { - this.socket.disconnect(); - } - } - - private WebOSTVServiceSocketClientListener mSocketListener = new WebOSTVServiceSocketClientListener() { - - @Override - public void onRegistrationFailed(final ServiceCommandError error) { - disconnect(); - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - if (listener != null) - listener.onConnectionFailure(WebOSTVService.this, error); - } - }); - } - - @Override - public Boolean onReceiveMessage(JSONObject message) { return true; } - - @Override - public void onFailWithError(final ServiceCommandError error) { - socket.setListener(null); - socket.disconnect(); - socket = null; - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - if (listener != null) - listener.onConnectionFailure(WebOSTVService.this, error); - } - }); - } - - @Override - public void onConnect() { - reportConnected(true); - } - - @Override - public void onCloseWithError(final ServiceCommandError error) { - socket.setListener(null); - socket.disconnect(); - socket = null; - - Util.runOnUI(new Runnable() { - - @Override - public void run() { - if (listener != null) - listener.onDisconnect(WebOSTVService.this, error); - } - }); - } - - @Override - public void onBeforeRegister() { - if ( DiscoveryManager.getInstance().getPairingLevel() == PairingLevel.ON ) { - Util.runOnUI(new Runnable() { - - @Override - public void run() { - if (listener != null) - listener.onPairingRequired(WebOSTVService.this, pairingType, null); - } - }); - } - } - }; - - // @cond INTERNAL - - public ConcurrentHashMap getWebAppIdMappings() { - return mAppToAppIdMappings; - } - - // @endcond - - @Override - public Launcher getLauncher() { - return this; - }; - - @Override - public CapabilityPriorityLevel getLauncherCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void launchApp(String appId, AppLaunchListener listener) { - AppInfo appInfo = new AppInfo(); - appInfo.setId(appId); - - launchAppWithInfo(appInfo, listener); - } - - @Override - public void launchAppWithInfo(AppInfo appInfo, Launcher.AppLaunchListener listener) { - launchAppWithInfo(appInfo, null, listener); - } - - @Override - public void launchAppWithInfo(final AppInfo appInfo, Object params, final Launcher.AppLaunchListener listener) { - String uri = "ssap://system.launcher/launch"; - JSONObject payload = new JSONObject(); - - final String appId = appInfo.getId(); - - String contentId = null; - - if (params != null) { - try { - contentId = (String) ((JSONObject) params).get("contentId"); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - try { - payload.put("id", appId); - - if (contentId != null) - payload.put("contentId", contentId); - - if (params != null) - payload.put("params", params); - } catch (JSONException e) { - e.printStackTrace(); - } - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - JSONObject obj = (JSONObject) response; - LaunchSession launchSession = new LaunchSession(); - - launchSession.setService(WebOSTVService.this); - launchSession.setAppId(appId); // note that response uses id to mean appId - launchSession.setSessionId(obj.optString("sessionId")); - launchSession.setSessionType(LaunchSessionType.App); - - Util.postSuccess(listener, launchSession); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - ServiceCommand> request = new ServiceCommand>(this, uri, payload, true, responseListener); - request.send(); - } - - - @Override - public void launchBrowser(String url, final Launcher.AppLaunchListener listener) { - String uri = "ssap://system.launcher/open"; - JSONObject payload = new JSONObject(); - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - JSONObject obj = (JSONObject) response; - LaunchSession launchSession = new LaunchSession(); - - launchSession.setService(WebOSTVService.this); - launchSession.setAppId(obj.optString("id")); // note that response uses id to mean appId - launchSession.setSessionId(obj.optString("sessionId")); - launchSession.setSessionType(LaunchSessionType.App); - launchSession.setRawData(obj); - - Util.postSuccess(listener, launchSession); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - try { - payload.put("target", url); - } catch (JSONException e) { - e.printStackTrace(); - } - - ServiceCommand> request = new ServiceCommand>(this, uri, payload, true, responseListener); - request.send(); - } - - @Override - public void launchYouTube(String contentId, Launcher.AppLaunchListener listener) { - launchYouTube(contentId, (float)0.0, listener); - } - - @Override - public void launchYouTube(final String contentId, float startTime, final AppLaunchListener listener) { - JSONObject params = new JSONObject(); - - if (contentId != null && contentId.length() > 0) { - if (startTime < 0.0) { - if (listener != null) { - listener.onError(new ServiceCommandError(0, "Start time may not be negative", null)); - } - - return; - } - - try { - params.put("contentId", String.format("%s&pairingCode=%s&t=%.1f", contentId, UUID.randomUUID().toString(), startTime)); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - AppInfo appInfo = new AppInfo() {{ - setId("youtube.leanback.v4"); - setName("YouTube"); - }}; - - launchAppWithInfo(appInfo, params, listener); - } - - @Override - public void launchHulu(String contentId, Launcher.AppLaunchListener listener) { - JSONObject params = new JSONObject(); - - try { - params.put("contentId", contentId); - } catch (JSONException e) { - e.printStackTrace(); - } - - AppInfo appInfo = new AppInfo() {{ - setId("hulu"); - setName("Hulu"); - }}; - - launchAppWithInfo(appInfo, params, listener); - } - - @Override - public void launchNetflix(String contentId, Launcher.AppLaunchListener listener) { - JSONObject params = new JSONObject(); - String netflixContentId = "m=http%3A%2F%2Fapi.netflix.com%2Fcatalog%2Ftitles%2Fmovies%2F" + contentId + "&source_type=4"; - - try { - params.put("contentId", netflixContentId); - } catch (JSONException e) { - e.printStackTrace(); - } - - AppInfo appInfo = new AppInfo() {{ - setId("netflix"); - setName("Netflix"); - }}; - - launchAppWithInfo(appInfo, params, listener); - } - - @Override - public void launchAppStore(String appId, AppLaunchListener listener) { - AppInfo appInfo = new AppInfo("com.webos.app.discovery"); - appInfo.setName("LG Store"); - - JSONObject params = new JSONObject(); - - if (appId != null && appId.length() > 0) { - String query = String.format("category/GAME_APPS/%s", appId); - try { - params.put("query", query); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - launchAppWithInfo(appInfo, params, listener); - } - - @Override - public void closeApp(LaunchSession launchSession, ResponseListener listener) { - String uri = "ssap://system.launcher/close"; - String appId = launchSession.getAppId(); - String sessionId = launchSession.getSessionId(); - - JSONObject payload = new JSONObject(); - - try { - payload.put("id", appId); - payload.put("sessionId", sessionId); - } catch (JSONException e) { - e.printStackTrace(); - } - - ServiceCommand> request = new ServiceCommand>(launchSession.getService(), uri, payload, true, listener); - request.send(); - } - - @Override - public void getAppList(final AppListListener listener) { - String uri = "ssap://com.webos.applicationManager/listApps"; - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - - try { - JSONObject jsonObj = (JSONObject) response; - - JSONArray apps = (JSONArray) jsonObj.get("apps"); - List appList = new ArrayList(); - - for (int i = 0; i < apps.length(); i++) - { - final JSONObject appObj = apps.getJSONObject(i); - - AppInfo appInfo = new AppInfo() {{ - setId(appObj.getString("id")); - setName(appObj.getString("title")); - setRawData(appObj); - }}; - - appList.add(appInfo); - } - - Util.postSuccess(listener, appList); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, responseListener); - request.send(); - } - - private ServiceCommand getRunningApp(boolean isSubscription, final AppInfoListener listener) { - ServiceCommand request; - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - final JSONObject jsonObj = (JSONObject)response; - AppInfo app = new AppInfo() {{ - setId(jsonObj.optString("appId")); - setName(jsonObj.optString("appName")); - setRawData(jsonObj); - }}; - - Util.postSuccess(listener, app); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - if (isSubscription) - request = new URLServiceSubscription(this, FOREGROUND_APP, null, true, responseListener); - else - request = new ServiceCommand(this, FOREGROUND_APP, null, true, responseListener); - - request.send(); - - return request; - } - - @Override - public void getRunningApp(AppInfoListener listener) { - getRunningApp(false, listener); - } - - @Override - public ServiceSubscription subscribeRunningApp(AppInfoListener listener) { - return (URLServiceSubscription) getRunningApp(true, listener); - } - - private ServiceCommand getAppState(boolean subscription, LaunchSession launchSession, final AppStateListener listener) { - ServiceCommand request; - JSONObject params = new JSONObject(); - - try { - params.put("appId", launchSession.getAppId()); - params.put("sessionId", launchSession.getSessionId()); - } catch (JSONException e) { - e.printStackTrace(); - } - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - JSONObject json = (JSONObject) object; - try { - Util.postSuccess(listener, new AppState(json.getBoolean("running"), json.getBoolean("visible"))); - } catch (JSONException e) { - Util.postError(listener, new ServiceCommandError(0, "Malformed JSONObject", null)); - e.printStackTrace(); - } - } - }; - - if (subscription) { - request = new URLServiceSubscription(this, APP_STATE, params, true, responseListener); - } else { - request = new ServiceCommand(this, APP_STATE, params, true, responseListener); - } - - request.send(); - - return request; - } - - @Override - public void getAppState(LaunchSession launchSession, AppStateListener listener) { - getAppState(false, launchSession, listener); - } - - @Override - public ServiceSubscription subscribeAppState(LaunchSession launchSession, AppStateListener listener) { - return (URLServiceSubscription) getAppState(true, launchSession, listener); - } - - - /****************** - TOAST CONTROL - *****************/ - @Override - public ToastControl getToastControl() { - return this; - }; - - @Override - public CapabilityPriorityLevel getToastControlCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void showToast(String message, ResponseListener listener) { - showToast(message, null, null, listener); - } - - @Override - public void showToast(String message, String iconData, String iconExtension, ResponseListener listener) - { - JSONObject payload = new JSONObject(); - - try { - payload.put("message", message); - - if (iconData != null) - { - payload.put("iconData", iconData); - payload.put("iconExtension", iconExtension); - } - } catch (JSONException e) { - e.printStackTrace(); - } - - sendToast(payload, listener); - } - - @Override - public void showClickableToastForApp(String message, AppInfo appInfo, JSONObject params, ResponseListener listener) { - showClickableToastForApp(message, appInfo, params, null, null, listener); - } - - @Override - public void showClickableToastForApp(String message, AppInfo appInfo, JSONObject params, String iconData, String iconExtension, ResponseListener listener) { - JSONObject payload = new JSONObject(); - - try { - payload.put("message", message); - - if (iconData != null) { - payload.put("iconData", iconData); - payload.put("iconExtension", iconExtension); - } - - if (appInfo != null) { - JSONObject onClick = new JSONObject(); - onClick.put("appId", appInfo.getId()); - if (params != null) { - onClick.put("params", params); - } - payload.put("onClick", onClick); - } - } catch (JSONException e) { - e.printStackTrace(); - } - - sendToast(payload, listener); - } - - @Override - public void showClickableToastForURL(String message, String url, ResponseListener listener) { - showClickableToastForURL(message, url, null, null, listener); - } - - @Override - public void showClickableToastForURL(String message, String url, String iconData, String iconExtension, ResponseListener listener) { - JSONObject payload = new JSONObject(); - - try { - payload.put("message", message); - - if (iconData != null) { - payload.put("iconData", iconData); - payload.put("iconExtension", iconExtension); - } - - if (url != null) { - JSONObject onClick = new JSONObject(); - onClick.put("target", url); - payload.put("onClick", onClick); - } - } catch (JSONException e) { - e.printStackTrace(); - } - - sendToast(payload, listener); - } - - private void sendToast(JSONObject payload, ResponseListener listener) { - if (!payload.has("iconData")) - { - Context context = DiscoveryManager.getInstance().getContext(); - - try { - Drawable drawable = context.getPackageManager().getApplicationIcon(context.getPackageName()); - - if(drawable != null) { - BitmapDrawable bitDw = ((BitmapDrawable) drawable); - Bitmap bitmap = bitDw.getBitmap(); - - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); - - byte[] bitmapByte = stream.toByteArray(); - bitmapByte = Base64.encode(bitmapByte,Base64.NO_WRAP); - String bitmapData = new String(bitmapByte); - - payload.put("iconData", bitmapData); - payload.put("iconExtension", "png"); - } - } catch (NameNotFoundException e) { - e.printStackTrace(); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - String uri = "palm://system.notifications/createToast"; - ServiceCommand> request = new ServiceCommand>(this, uri, payload, true, listener); - request.send(); - } - - - /****************** - VOLUME CONTROL - *****************/ - @Override - public VolumeControl getVolumeControl() { - return this; - }; - - @Override - public CapabilityPriorityLevel getVolumeControlCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - public void volumeUp() { - volumeUp(null); - } - - @Override - public void volumeUp(ResponseListener listener) { - String uri = "ssap://audio/volumeUp"; - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, listener); - - request.send(); - } - - public void volumeDown() { - volumeDown(null); - } - - @Override - public void volumeDown(ResponseListener listener) { - String uri = "ssap://audio/volumeDown"; - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, listener); - - request.send(); - } - - public void setVolume(int volume) { - setVolume(volume, null); - } - - @Override - public void setVolume(float volume, ResponseListener listener) { - String uri = "ssap://audio/setVolume"; - JSONObject payload = new JSONObject(); - - try { - payload.put("volume", (volume * 100.0f)); - } catch (JSONException e) { - e.printStackTrace(); - } - - ServiceCommand> request = new ServiceCommand>(this, uri, payload, true, listener); - request.send(); - } - - private ServiceCommand getVolume(boolean isSubscription, final VolumeListener listener) { - ServiceCommand request; - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - - try { - JSONObject jsonObj = (JSONObject)response; - int iVolume = (Integer) jsonObj.get("volume"); - float fVolume = (float) (iVolume / 100.0); - - Util.postSuccess(listener, fVolume); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - if (isSubscription) - request = new URLServiceSubscription(this, VOLUME, null, true, responseListener); - else - request = new ServiceCommand(this, VOLUME, null, true, responseListener); - - request.send(); - - return request; - } - - @Override - public void getVolume(VolumeListener listener) { - getVolume(false, listener); - } - - @SuppressWarnings("unchecked") - @Override - public ServiceSubscription subscribeVolume(VolumeListener listener) { - return (ServiceSubscription) getVolume(true, listener); - } - - @Override - public void setMute(boolean isMute, ResponseListener listener) { - String uri = "ssap://audio/setMute"; - JSONObject payload = new JSONObject(); - - try { - payload.put("mute", isMute); - } catch (JSONException e) { - e.printStackTrace(); - } - - ServiceCommand> request = new ServiceCommand>(this, uri, payload, true, listener); - request.send(); - } - - private ServiceCommand> getMuteStatus(boolean isSubscription, final MuteListener listener) { - ServiceCommand> request; - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - try { - JSONObject jsonObj = (JSONObject)response; - boolean isMute = (Boolean) jsonObj.get("mute"); - Util.postSuccess(listener, isMute); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - if (isSubscription) - request = new URLServiceSubscription>(this, MUTE, null, true, responseListener); - else - request = new ServiceCommand>(this, MUTE, null, true, responseListener); - - request.send(); - - return request; - } - - @Override - public void getMute(MuteListener listener) { - getMuteStatus(false, listener); - } - - @SuppressWarnings("unchecked") - @Override - public ServiceSubscription subscribeMute(MuteListener listener) { - return (ServiceSubscription) getMuteStatus(true, listener); - } - - private ServiceCommand> getVolumeStatus(boolean isSubscription, final VolumeStatusListener listener) { - ServiceCommand> request; - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - try { - JSONObject jsonObj = (JSONObject) response; - boolean isMute = (Boolean) jsonObj.get("mute"); - int iVolume = jsonObj.getInt("volume"); - float fVolume = (float) (iVolume / 100.0); - - Util.postSuccess(listener, new VolumeStatus(isMute, fVolume)); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - if (isSubscription) - request = new URLServiceSubscription>(this, VOLUME_STATUS, null, true, responseListener); - else - request = new ServiceCommand>(this, VOLUME_STATUS, null, true, responseListener); - - request.send(); - - return request; - } - - public void getVolumeStatus(VolumeStatusListener listener) { - getVolumeStatus(false, listener); - } - - @SuppressWarnings("unchecked") - public ServiceSubscription subscribeVolumeStatus(VolumeStatusListener listener) { - return (ServiceSubscription) getVolumeStatus(true, listener); - } - - - /****************** - MEDIA PLAYER - *****************/ - @Override - public MediaPlayer getMediaPlayer() { - return this; - }; - - @Override - public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - private void displayMedia(JSONObject params, final MediaPlayer.LaunchListener listener) { - String uri = "ssap://media.viewer/open"; - - ResponseListener responseListener = new ResponseListener() { - @Override - public void onSuccess(Object response) { - JSONObject obj = (JSONObject) response; - - LaunchSession launchSession = LaunchSession.launchSessionForAppId(obj.optString("id")); - launchSession.setService(WebOSTVService.this); - launchSession.setSessionId(obj.optString("sessionId")); - launchSession.setSessionType(LaunchSessionType.Media); - - Util.postSuccess(listener, new MediaLaunchObject(launchSession, WebOSTVService.this)); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - ServiceCommand> request = new ServiceCommand>(this, uri, params, true, responseListener); - request.send(); - } - - @Override - public void displayImage(final String url, final String mimeType, final String title, final String description, final String iconSrc, final MediaPlayer.LaunchListener listener) { - if ("4.0.0".equalsIgnoreCase(this.serviceDescription.getVersion())) { - DeviceService dlnaService = this.getDLNAService(); - - if (dlnaService != null) { - MediaPlayer mediaPlayer = dlnaService.getAPI(MediaPlayer.class); - - if (mediaPlayer != null) { - mediaPlayer.displayImage(url, mimeType, title, description, iconSrc, listener); - return; - } - } - - JSONObject params = null; - - try { - params = new JSONObject() {{ - put("target", url); - put("title", title == null ? NULL : title); - put("description", description == null ? NULL : description); - put("mimeType", mimeType == null ? NULL : mimeType); - put("iconSrc", iconSrc == null ? NULL : iconSrc); - }}; - } catch (JSONException ex) { - ex.printStackTrace(); - Util.postError(listener, new ServiceCommandError(-1, ex.getLocalizedMessage(), ex)); - } - - if (params != null) - this.displayMedia(params, listener); - } else { - final String webAppId = "MediaPlayer"; - - final WebAppSession.LaunchListener webAppLaunchListener = new WebAppSession.LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - listener.onError(error); - } - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.displayImage(url, mimeType, title, description, iconSrc, listener); - } - }; - - this.getWebAppLauncher().joinWebApp(webAppId, new WebAppSession.LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - getWebAppLauncher().launchWebApp(webAppId, webAppLaunchListener); - } - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.displayImage(url, mimeType, title, description, iconSrc, listener); - } - }); - } - } - - @Override - public void displayImage(final MediaInfo mediaInfo, - final MediaPlayer.LaunchListener listener) { - if ("4.0.0".equalsIgnoreCase(this.serviceDescription.getVersion())) { - DeviceService dlnaService = this.getDLNAService(); - - if (dlnaService != null) { - MediaPlayer mediaPlayer = dlnaService.getAPI(MediaPlayer.class); - - if (mediaPlayer != null) { - mediaPlayer.displayImage(mediaInfo, listener); - return; - } - } - - JSONObject params = null; - - - try { - params = new JSONObject() { - { - put("target", mediaInfo.getUrl()); - put("title", mediaInfo.getTitle() == null ? NULL : mediaInfo.getTitle()); - put("description", mediaInfo.getDescription() == null ? NULL : mediaInfo.getDescription()); - put("mimeType", mediaInfo.getMimeType() == null ? NULL : mediaInfo.getMimeType()); - put("iconSrc", mediaInfo.getImages().get(0).getUrl() == null ? NULL : mediaInfo.getImages().get(0).getUrl()); - - }}; - } catch (JSONException ex) { - ex.printStackTrace(); - Util.postError(listener, new ServiceCommandError(-1, ex.getLocalizedMessage(), ex)); - } - - if (params != null) - this.displayMedia(params, listener); - } else { - final String webAppId = "MediaPlayer"; - - final WebAppSession.LaunchListener webAppLaunchListener = new WebAppSession.LaunchListener() { - - - - @Override - public void onError(ServiceCommandError error) { - listener.onError(error); - } - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.displayImage(mediaInfo, listener); - } - }; - - this.getWebAppLauncher().joinWebApp(webAppId, new WebAppSession.LaunchListener() { - - - - @Override - public void onError(ServiceCommandError error) { - getWebAppLauncher().launchWebApp(webAppId, webAppLaunchListener); - } - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.displayImage(mediaInfo, listener); - } - }); - } - } - - @Override - public void playMedia(final String url, final String mimeType, final String title, final String description, final String iconSrc, final boolean shouldLoop, final MediaPlayer.LaunchListener listener) { - if ("4.0.0".equalsIgnoreCase(this.serviceDescription.getVersion())) { - DeviceService dlnaService = this.getDLNAService(); - - if (dlnaService != null) { - MediaPlayer mediaPlayer = dlnaService.getAPI(MediaPlayer.class); - - if (mediaPlayer != null) { - mediaPlayer.playMedia(url, mimeType, title, description, iconSrc, shouldLoop, listener); - return; - } - } - - JSONObject params = null; - - try { - params = new JSONObject() {{ - put("target", url); - put("title", title == null ? NULL : title); - put("description", description == null ? NULL : description); - put("mimeType", mimeType == null ? NULL : mimeType); - put("iconSrc", iconSrc == null ? NULL : iconSrc); - put("loop", shouldLoop); - }}; - } catch (JSONException ex) { - ex.printStackTrace(); - Util.postError(listener, new ServiceCommandError(-1, ex.getLocalizedMessage(), ex)); - } - - if (params != null) - this.displayMedia(params, listener); - } else { - final String webAppId = "MediaPlayer"; - - final WebAppSession.LaunchListener webAppLaunchListener = new WebAppSession.LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - listener.onError(error); - } - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.playMedia(url, mimeType, title, description, iconSrc, shouldLoop, listener); - } - }; - - this.getWebAppLauncher().joinWebApp(webAppId, new WebAppSession.LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - getWebAppLauncher().launchWebApp(webAppId, webAppLaunchListener); - } - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.playMedia(url, mimeType, title, description, iconSrc, shouldLoop, listener); - } - }); - } - } - - @Override - public void playMedia(final MediaInfo mediaInfo, - final boolean shouldLoop, final MediaPlayer.LaunchListener listener) { - if ("4.0.0".equalsIgnoreCase(this.serviceDescription.getVersion())) { - DeviceService dlnaService = this.getDLNAService(); - - if (dlnaService != null) { - MediaPlayer mediaPlayer = dlnaService.getAPI(MediaPlayer.class); - - if (mediaPlayer != null) { - mediaPlayer.playMedia(mediaInfo, shouldLoop, listener); - return; - } - } - - JSONObject params = null; - - try { - params = new JSONObject() { - { - put("target", mediaInfo.getUrl()); - put("title", mediaInfo.getTitle() == null ? NULL : mediaInfo.getTitle()); - put("description", mediaInfo.getDescription() == null ? NULL - : mediaInfo.getDescription()); - put("mimeType", mediaInfo.getMimeType() == null ? NULL : mediaInfo.getMimeType()); - put("iconSrc", mediaInfo.getImages().get(0).getUrl() == null ? NULL : mediaInfo.getImages().get(0).getUrl()); - put("posterSrc", mediaInfo.getImages().get(1).getUrl() == null ? NULL : mediaInfo.getImages().get(1).getUrl()); - put("loop", shouldLoop); - } - }; - } catch (JSONException ex) { - ex.printStackTrace(); - Util.postError(listener, - new ServiceCommandError(-1, ex.getLocalizedMessage(), - ex)); - } - - if (params != null) - this.displayMedia(params, listener); - } else { - final String webAppId = "MediaPlayer"; - - final WebAppSession.LaunchListener webAppLaunchListener = new WebAppSession.LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - listener.onError(error); - } - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.playMedia(mediaInfo, shouldLoop, listener); - } - }; - - this.getWebAppLauncher().joinWebApp(webAppId, - new WebAppSession.LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - getWebAppLauncher().launchWebApp(webAppId, - webAppLaunchListener); - } - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.playMedia(mediaInfo, shouldLoop, listener); - } - }); - } - } - - @Override - public void closeMedia(LaunchSession launchSession, ResponseListener listener) { - JSONObject payload = new JSONObject(); - - try { - if (launchSession.getAppId() != null && launchSession.getAppId().length() > 0) - payload.put("id", launchSession.getAppId()); - - if (launchSession.getSessionId() != null && launchSession.getSessionId().length() > 0) - payload.put("sessionId", launchSession.getSessionId()); - } catch (JSONException e) { - e.printStackTrace(); - } - - ServiceCommand> request = new ServiceCommand>(launchSession.getService(), CLOSE_MEDIA_URI, payload, true, listener); - request.send(); - } - - /****************** - MEDIA CONTROL - *****************/ - @Override - public MediaControl getMediaControl() { - return this; - }; - - @Override - public CapabilityPriorityLevel getMediaControlCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; - } - - @Override - public void play(ResponseListener listener) { - String uri = "ssap://media.controls/play"; - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, listener); - - request.send(); - } - - @Override - public void pause(ResponseListener listener) { - String uri = "ssap://media.controls/pause"; - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, listener); - - request.send(); - } - - @Override - public void stop(ResponseListener listener) { - String uri = "ssap://media.controls/stop"; - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, listener); - - request.send(); - } - - @Override - public void rewind(ResponseListener listener) { - String uri = "ssap://media.controls/rewind"; - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, listener); - - request.send(); - } - - @Override - public void fastForward(ResponseListener listener) { - String uri = "ssap://media.controls/fastForward"; - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, listener); - - request.send(); - } - - @Override - public void seek(long position, ResponseListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void getDuration(DurationListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void getPosition(PositionListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - - /****************** - TV CONTROL - *****************/ - @Override - public TVControl getTVControl() { - return this; - }; - - @Override - public CapabilityPriorityLevel getTVControlCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - public void channelUp() { - channelDown(null); - } - - @Override - public void channelUp(ResponseListener listener) { - String uri = "ssap://tv/channelUp"; - - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, listener); - request.send(); - } - - public void channelDown() { - channelDown(null); - } - - @Override - public void channelDown(ResponseListener listener) { - String uri = "ssap://tv/channelDown"; - - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, listener); - request.send(); - } - - @Override - public void setChannel(ChannelInfo channelInfo, ResponseListener listener) { - String uri = "ssap://tv/openChannel"; - JSONObject payload = new JSONObject(); - - try { - payload.put("channelNumber", channelInfo.getNumber()); - } catch (JSONException e) { - e.printStackTrace(); - } - - ServiceCommand> request = new ServiceCommand>(this, uri, payload, true, listener); - request.send(); - } - - public void setChannelById(String channelId) { - setChannelById(channelId, null); - } - - public void setChannelById(String channelId, ResponseListener listener) { - String uri = "ssap://tv/openChannel"; - JSONObject payload = new JSONObject(); - - try { - payload.put("channelId", channelId); - } catch (JSONException e) { - e.printStackTrace(); - } - - ServiceCommand> request = new ServiceCommand>(this, uri, payload, true, listener); - request.send(); - } - - private ServiceCommand> getCurrentChannel(boolean isSubscription, final ChannelListener listener) { - ServiceCommand> request; - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - JSONObject jsonObj = (JSONObject) response; - ChannelInfo channel = parseRawChannelData(jsonObj); - - Util.postSuccess(listener, channel); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - if (isSubscription) { - request = new URLServiceSubscription>(this, CHANNEL, null, true, responseListener); - } - else - request = new ServiceCommand>(this, CHANNEL, null, true, responseListener); - - request.send(); - - return request; - } - - @Override - public void getCurrentChannel(ChannelListener listener) { - getCurrentChannel(false, listener); - } - - @SuppressWarnings("unchecked") - @Override - public ServiceSubscription subscribeCurrentChannel(ChannelListener listener) { - return (ServiceSubscription) getCurrentChannel(true, listener); - } - - private ServiceCommand> getChannelList(boolean isSubscription, final ChannelListListener listener) { - ServiceCommand> request; - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - try { - JSONObject jsonObj = (JSONObject)response; - ArrayList list = new ArrayList(); - - JSONArray array = (JSONArray) jsonObj.get("channelList"); - for (int i = 0; i < array.length(); i++) { - JSONObject object = (JSONObject) array.get(i); - - ChannelInfo channel = parseRawChannelData(object); - list.add(channel); - } - - Util.postSuccess(listener, list); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - if (isSubscription) - request = new URLServiceSubscription>(this, CHANNEL_LIST, null, true, responseListener); - else - request = new ServiceCommand>(this, CHANNEL_LIST, null, true, responseListener); - - request.send(); - - return request; - } - - @Override - public void getChannelList(ChannelListListener listener) { - getChannelList(false, listener); - } - - @SuppressWarnings("unchecked") - public ServiceSubscription subscribeChannelList(final ChannelListListener listener) { - return (ServiceSubscription) getChannelList(true, listener); - } - - private ServiceCommand> getProgramList(boolean isSubscription, final ProgramListListener listener) { - ServiceCommand> request; - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - try { - JSONObject jsonObj = (JSONObject)response; - JSONObject jsonChannel = (JSONObject) jsonObj.get("channel"); - ChannelInfo channel = parseRawChannelData(jsonChannel); - JSONArray programList = (JSONArray) jsonObj.get("programList"); - - Util.postSuccess(listener, new ProgramList(channel, programList)); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - if (isSubscription) - request = new URLServiceSubscription>(this, PROGRAM, null, true, responseListener); - else - request = new ServiceCommand>(this, PROGRAM, null, true, responseListener); - - request.send(); - - return request; - } - - @Override - public void getProgramInfo(ProgramInfoListener listener) { - // TODO need to parse current program when program id is correct - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public ServiceSubscription subscribeProgramInfo(ProgramInfoListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - - return new NotSupportedServiceSubscription(); - } - - @Override - public void getProgramList(ProgramListListener listener) { - getProgramList(false, listener); - } - - @SuppressWarnings("unchecked") - @Override - public ServiceSubscription subscribeProgramList(ProgramListListener listener) { - return (ServiceSubscription) getProgramList(true, listener); - } - - @Override - public void set3DEnabled(final boolean enabled, final ResponseListener listener) { - String uri; - if (enabled == true) - uri = "ssap://com.webos.service.tv.display/set3DOn"; - else - uri = "ssap://com.webos.service.tv.display/set3DOff"; - - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, listener); - - request.send(); - } - - private ServiceCommand get3DEnabled(boolean isSubscription, final State3DModeListener listener) { - String uri = "ssap://com.webos.service.tv.display/get3DStatus"; - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - JSONObject jsonObj = (JSONObject)response; - - JSONObject status; - try { - status = jsonObj.getJSONObject("status3D"); - boolean isEnabled = status.getBoolean("status"); - - Util.postSuccess(listener, isEnabled); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - ServiceCommand request; - if (isSubscription == true) - request = new URLServiceSubscription(this, uri, null, true, responseListener); - else - request = new ServiceCommand(this, uri, null, true, responseListener); - - request.send(); - - return request; - } - - @Override - public void get3DEnabled(final State3DModeListener listener) { - get3DEnabled(false, listener); - } - - @SuppressWarnings("unchecked") - @Override - public ServiceSubscription subscribe3DEnabled(final State3DModeListener listener) { - return (ServiceSubscription) get3DEnabled(true, listener); - } - - - /************** - EXTERNAL INPUT - **************/ - @Override - public ExternalInputControl getExternalInput() { - return this; - }; - - @Override - public CapabilityPriorityLevel getExternalInputControlPriorityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void launchInputPicker(final AppLaunchListener listener) { - AppInfo appInfo = new AppInfo() {{ - setId("com.webos.app.inputpicker"); - setName("InputPicker"); - }}; - - launchAppWithInfo(appInfo, null, listener); - } - - @Override - public void closeInputPicker(LaunchSession launchSession, ResponseListener listener) { - closeApp(launchSession, listener); - } - - @Override - public void getExternalInputList(final ExternalInputListListener listener) { - String uri = "ssap://tv/getExternalInputList"; - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - try { - JSONObject jsonObj = (JSONObject)response; - JSONArray devices = (JSONArray) jsonObj.get("devices"); - Util.postSuccess(listener, externalnputInfoFromJSONArray(devices)); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, responseListener); - request.send(); - } - - @Override - public void setExternalInput(ExternalInputInfo externalInputInfo , final ResponseListener listener) { - String uri = "ssap://tv/switchInput"; - - JSONObject payload = new JSONObject(); - - try { - if (externalInputInfo != null && externalInputInfo .getId() != null) { - payload.put("inputId", externalInputInfo.getId()); - } - else { - Log.w("Connect SDK", "ExternalInputInfo has no id"); - } - } catch (JSONException e) { - e.printStackTrace(); - } - - ServiceCommand> request = new ServiceCommand>(this, uri, payload, true, listener); - request.send(); - } - - - /************** - MOUSE CONTROL - **************/ - @Override - public MouseControl getMouseControl() { - return this; - }; - - @Override - public CapabilityPriorityLevel getMouseControlCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void connectMouse() { - if (mouseSocket != null) - return; - - ResponseListener listener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - try { - JSONObject jsonObj = (JSONObject)response; - String socketPath = (String) jsonObj.get("socketPath"); - mouseSocket = new WebOSTVMouseSocketConnection(socketPath); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - } - }; - - connectMouse(listener); - } - - @Override - public void disconnectMouse() { - if (mouseSocket == null) - return; - - mouseSocket.disconnect(); - mouseSocket = null; - } - - private void connectMouse(ResponseListener listener) { - String uri = "ssap://com.webos.service.networkinput/getPointerInputSocket"; - - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, listener); - request.send(); - } - - @Override - public void click() { - if (mouseSocket != null) - mouseSocket.click(); - else { - connectMouse(); - } - } - - @Override - public void move(double dx, double dy) { - if (mouseSocket != null) - mouseSocket.move(dx, dy); - else - Log.w("Connect SDK", "Mouse Socket is not ready yet"); - } - - @Override - public void move(PointF diff) { - move(diff.x, diff.y); - } - - @Override - public void scroll(double dx, double dy) { - if (mouseSocket != null) - mouseSocket.scroll(dx, dy); - else - Log.w("Connect SDK", "Mouse Socket is not ready yet"); - } - - @Override - public void scroll(PointF diff) { - scroll(diff.x, diff.y); - } - - - /************** - KEYBOARD CONTROL - **************/ - @Override - public TextInputControl getTextInputControl() { - return this; - }; - - @Override - public CapabilityPriorityLevel getTextInputControlCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public ServiceSubscription subscribeTextInputStatus(TextInputStatusListener listener) { - keyboardInput = new WebOSTVKeyboardInput(this); - return keyboardInput.connect(listener); - } - - @Override - public void sendText(String input) { - if (keyboardInput != null) { - keyboardInput.addToQueue(input); - } - } - - @Override - public void sendKeyCode(int keyCode, ResponseListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void sendEnter() { - if (keyboardInput != null) { - keyboardInput.sendEnter(); - } - } - - @Override - public void sendDelete() { - if (keyboardInput != null) { - keyboardInput.sendDel(); - } - } - - - /************** - POWER CONTROL - **************/ - @Override - public PowerControl getPowerControl() { - return this; - }; - - @Override - public CapabilityPriorityLevel getPowerControlCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void powerOff(ResponseListener listener) { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - - } - - @Override - public void onError(ServiceCommandError error) { - - } - }; - - String uri = "ssap://system/turnOff"; - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, responseListener); - - request.send(); - } - - @Override - public void powerOn(ResponseListener listener) { - if (listener != null) - listener.onError(ServiceCommandError.notSupported()); - } - - - /************** - KEY CONTROL - **************/ - @Override - public KeyControl getKeyControl() { - return this; - } - - @Override - public CapabilityPriorityLevel getKeyControlCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - private void sendSpecialKey(final String key) { - if (mouseSocket != null) { - mouseSocket.button(key); - } - else { - ResponseListener listener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - try { - JSONObject jsonObj = (JSONObject)response; - String socketPath = (String) jsonObj.get("socketPath"); - mouseSocket = new WebOSTVMouseSocketConnection(socketPath); - - mouseSocket.button(key); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - } - }; - - connectMouse(listener); - } - } - - @Override - public void up(ResponseListener listener) { - sendSpecialKey("UP"); - } - - @Override - public void down(ResponseListener listener) { - sendSpecialKey("DOWN"); - } - - @Override - public void left(ResponseListener listener) { - sendSpecialKey("LEFT"); - } - - @Override - public void right(ResponseListener listener) { - sendSpecialKey("RIGHT"); - } - - @Override - public void ok(ResponseListener listener) { - if (mouseSocket != null) { - mouseSocket.click(); - } - else { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - try { - JSONObject jsonObj = (JSONObject)response; - String socketPath = (String) jsonObj.get("socketPath"); - mouseSocket = new WebOSTVMouseSocketConnection(socketPath); - - mouseSocket.click(); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - } - }; - - connectMouse(responseListener); - } - } - - @Override - public void back(ResponseListener listener) { - sendSpecialKey("BACK"); - } - - @Override - public void home(ResponseListener listener) { - sendSpecialKey("HOME"); - } - - - /************** - Web App Launcher - **************/ - - @Override - public WebAppLauncher getWebAppLauncher() { - return this; - } - - @Override - public CapabilityPriorityLevel getWebAppLauncherCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void launchWebApp(final String webAppId, final WebAppSession.LaunchListener listener) { - this.launchWebApp(webAppId, null, true, listener); - } - - @Override - public void launchWebApp(String webAppId, boolean relaunchIfRunning, WebAppSession.LaunchListener listener) { - launchWebApp(webAppId, null, relaunchIfRunning, listener); - } - - public void launchWebApp(final String webAppId, final JSONObject params, final WebAppSession.LaunchListener listener) { - if (webAppId == null || webAppId.length() == 0) { - Util.postError(listener, new ServiceCommandError(-1, "You need to provide a valid webAppId.", null)); - - return; - } - - final WebOSWebAppSession _webAppSession = mWebAppSessions.get(webAppId); - - String uri = "ssap://webapp/launchWebApp"; - JSONObject payload = new JSONObject(); - - try { - payload.put("webAppId", webAppId); - - if (params != null) - payload.put("urlParams", params); - } catch (JSONException e) { - e.printStackTrace(); - } - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(final Object response) { - JSONObject obj = (JSONObject) response; - - LaunchSession launchSession = null; - WebOSWebAppSession webAppSession = _webAppSession; - - if (webAppSession != null) - launchSession = webAppSession.launchSession; - else { - launchSession = LaunchSession.launchSessionForAppId(webAppId); - webAppSession = new WebOSWebAppSession(launchSession, WebOSTVService.this); - mWebAppSessions.put(webAppId, webAppSession); - } - - launchSession.setService(WebOSTVService.this); - launchSession.setSessionId(obj.optString("sessionId")); - launchSession.setSessionType(LaunchSessionType.WebApp); - launchSession.setRawData(obj); - - Util.postSuccess(listener, webAppSession); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - ServiceCommand> request = new ServiceCommand>(this, uri, payload, true, responseListener); - request.send(); - } - - @Override - public void launchWebApp(final String webAppId, final JSONObject params, boolean relaunchIfRunning, final WebAppSession.LaunchListener listener) { - if (webAppId == null) { - Util.postError(listener, new ServiceCommandError(0, "Must pass a web App id", null)); - return; - } - - if (relaunchIfRunning) { - launchWebApp(webAppId, params, listener); - } else { - getLauncher().getRunningApp(new AppInfoListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(AppInfo appInfo) { - // TODO: this will only work on pinned apps, currently - if (appInfo.getId().indexOf(webAppId) != -1) { - LaunchSession launchSession = LaunchSession.launchSessionForAppId(webAppId); - launchSession.setSessionType(LaunchSessionType.WebApp); - launchSession.setService(WebOSTVService.this); - launchSession.setRawData(appInfo.getRawData()); - - WebOSWebAppSession webAppSession = webAppSessionForLaunchSession(launchSession); - - Util.postSuccess(listener, webAppSession); - } else { - launchWebApp(webAppId, params, listener); - } - } - }); - } - } - - @Override - public void closeWebApp(LaunchSession launchSession, final ResponseListener listener) { - if (launchSession == null || launchSession.getAppId() == null || launchSession.getAppId().length() == 0) { - Util.postError(listener, new ServiceCommandError(0, "Must provide a valid launch session", null)); - return; - } - - final WebOSWebAppSession webAppSession = mWebAppSessions.get(launchSession.getAppId()); - - if (webAppSession != null && webAppSession.isConnected()) { - JSONObject serviceCommand = new JSONObject(); - JSONObject closeCommand = new JSONObject(); - - try { - serviceCommand.put("type", "close"); - - closeCommand.put("contentType", "connectsdk.serviceCommand"); - closeCommand.put("serviceCommand", serviceCommand); - } catch (JSONException ex) { - ex.printStackTrace(); - } - - if (closeCommand != null && serviceCommand != null) { - webAppSession.sendMessage(closeCommand, new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - webAppSession.disconnectFromWebApp(); - - if (listener != null) - listener.onError(error); - } - - @Override - public void onSuccess(Object object) { - webAppSession.disconnectFromWebApp(); - - if (listener != null) - listener.onSuccess(object); - } - }); - } - } else - { - if (webAppSession != null) - webAppSession.disconnectFromWebApp(); - - String uri = "ssap://webapp/closeWebApp"; - JSONObject payload = new JSONObject(); - - try { - if (launchSession.getAppId() != null) payload.put("webAppId", launchSession.getAppId()); - if (launchSession.getSessionId() != null) payload.put("sessionId", launchSession.getSessionId()); - } catch (JSONException e) { - e.printStackTrace(); - } - - ServiceCommand> request = new ServiceCommand>(this, uri, payload, true, listener); - request.send(); - } - } - - public void connectToWebApp(final WebOSWebAppSession webAppSession, final boolean joinOnly, final ResponseListener connectionListener) { - if (mWebAppSessions == null) - mWebAppSessions = new ConcurrentHashMap(); - - if (mAppToAppIdMappings == null) - mAppToAppIdMappings = new ConcurrentHashMap(); - - if (webAppSession == null || webAppSession.launchSession == null) { - Util.postError(connectionListener, new ServiceCommandError(0, "You must provide a valid LaunchSession object", null)); - - return; - } - - String _appId = webAppSession.launchSession.getAppId(); - String _idKey = null; - - if (webAppSession.launchSession.getSessionType() == LaunchSession.LaunchSessionType.WebApp) - _idKey = "webAppId"; - else - _idKey = "appId"; - - if (_appId == null || _appId.length() == 0) { - Util.postError(connectionListener, new ServiceCommandError(-1, "You must provide a valid web app session", null)); - - return; - } - - final String appId = _appId; - final String idKey = _idKey; - - String uri = "ssap://webapp/connectToApp"; - JSONObject payload = new JSONObject(); - - try { - payload.put(idKey, appId); - } catch (JSONException e) { - e.printStackTrace(); - } - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(final Object response) { - JSONObject jsonObj = (JSONObject)response; - - String state = jsonObj.optString("state"); - - if (!state.equalsIgnoreCase("CONNECTED")) { - if (joinOnly && state.equalsIgnoreCase("WAITING_FOR_APP")) { - Util.postError(connectionListener, new ServiceCommandError(0, "Web app is not currently running", null)); - } - - return; - } - - String fullAppId = jsonObj.optString("appId"); - - if (fullAppId != null && fullAppId.length() != 0) { - if (webAppSession.launchSession.getSessionType() == LaunchSessionType.WebApp) - mAppToAppIdMappings.put(fullAppId, appId); - - webAppSession.setFullAppId(fullAppId); - } - - if (connectionListener != null) { - Util.runOnUI(new Runnable() { - - @Override - public void run() { - connectionListener.onSuccess(response); - } - }); - } - } - - @Override - public void onError(ServiceCommandError error) { - webAppSession.disconnectFromWebApp(); - - boolean appChannelDidClose = false; - - if (error != null && error.getPayload() != null) - appChannelDidClose = error.getPayload().toString().contains("app channel closed"); - - if (appChannelDidClose) { - if (webAppSession != null && webAppSession.getWebAppSessionListener() != null) { - Util.runOnUI(new Runnable() { - - @Override - public void run() { - webAppSession.getWebAppSessionListener().onWebAppSessionDisconnect(webAppSession); - } - }); - } - } else { - Util.postError(connectionListener, error); - } - } - }; - - webAppSession.appToAppSubscription = new URLServiceSubscription>(webAppSession.socket, uri, payload, true, responseListener); - webAppSession.appToAppSubscription.subscribe(); - } - - /* Join a native/installed webOS app */ - public void joinApp(String appId, WebAppSession.LaunchListener listener) { - LaunchSession launchSession = LaunchSession.launchSessionForAppId(appId); - launchSession.setSessionType(LaunchSessionType.App); - launchSession.setService(this); - - joinWebApp(launchSession, listener); - } - - /* Connect to a native/installed webOS app */ - public void connectToApp(String appId, final WebAppSession.LaunchListener listener) { - LaunchSession launchSession = LaunchSession.launchSessionForAppId(appId); - launchSession.setSessionType(LaunchSessionType.App); - launchSession.setService(this); - - final WebOSWebAppSession webAppSession = webAppSessionForLaunchSession(launchSession); - - connectToWebApp(webAppSession, false, new ResponseListener () { - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - Util.postSuccess(listener, webAppSession); - } - }); - } - - @Override - public void joinWebApp(final LaunchSession webAppLaunchSession, final WebAppSession.LaunchListener listener) { - final WebOSWebAppSession webAppSession = this.webAppSessionForLaunchSession(webAppLaunchSession); - - webAppSession.join(new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - Util.postSuccess(listener, webAppSession); - } - }); - } - - @Override - public void joinWebApp(String webAppId, WebAppSession.LaunchListener listener) { - LaunchSession launchSession = LaunchSession.launchSessionForAppId(webAppId); - launchSession.setSessionType(LaunchSessionType.WebApp); - launchSession.setService(this); - - joinWebApp(launchSession, listener); - } - - private WebOSWebAppSession webAppSessionForLaunchSession(LaunchSession launchSession) { - if (mWebAppSessions == null) - mWebAppSessions = new ConcurrentHashMap(); - - if (launchSession.getService() == null) - launchSession.setService(this); - - WebOSWebAppSession webAppSession = mWebAppSessions.get(launchSession.getAppId()); - - if (webAppSession == null) { - webAppSession = new WebOSWebAppSession(launchSession, this); - mWebAppSessions.put(launchSession.getAppId(), webAppSession); - } - - return webAppSession; - } - - @SuppressWarnings("unused") - private void sendMessage(Object message, LaunchSession launchSession, ResponseListener listener) { - if (launchSession == null || launchSession.getAppId() == null) { - Util.postError(listener, new ServiceCommandError(0, "Must provide a valid LaunchSession object", null)); - return; - } - - if (message == null) { - Util.postError(listener, new ServiceCommandError(0, "Cannot send a null message", null)); - return; - } - - if (socket == null) { - connect(); - } - - String appId = launchSession.getAppId(); - String fullAppId = appId; - - if (launchSession.getSessionType() == LaunchSessionType.WebApp) - fullAppId = mAppToAppIdMappings.get(appId); - - if (fullAppId == null || fullAppId.length() == 0) - { - if (listener != null) - Util.postError(listener, new ServiceCommandError(-1, "You must provide a valid LaunchSession to send messages to", null)); - - return; - } - - JSONObject payload = new JSONObject(); - - try { - payload.put("type", "p2p"); - payload.put("to", fullAppId); - payload.put("payload", message); - - Object payTest = payload.get("payload"); - } catch (JSONException e) { - e.printStackTrace(); - } - - ServiceCommand> request = new ServiceCommand>(this, null, payload, true, listener); - sendCommand(request); - } - - public void sendMessage(String message, LaunchSession launchSession, ResponseListener listener) { - if (message != null && message.length() > 0) { - sendMessage((Object) message, launchSession, listener); - } - else { - Util.postError(listener, new ServiceCommandError(0, "Cannot send a null message", null)); - } - } - - public void sendMessage(JSONObject message, LaunchSession launchSession, ResponseListener listener) { - if (message != null && message.length() > 0) - sendMessage((Object) message, launchSession, listener); - else - Util.postError(listener, new ServiceCommandError(0, "Cannot send a null message", null)); - } - - - /************** - SYSTEM CONTROL - **************/ - public void getServiceInfo(final ServiceInfoListener listener) { - String uri = "ssap://api/getServiceList"; - - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, new ResponseListener() { - - @Override - public void onSuccess(Object response) { - try { - JSONObject jsonObj = (JSONObject)response; - JSONArray services = (JSONArray) jsonObj.get("services"); - Util.postSuccess(listener, services); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - - request.send(); - } - - public void getSystemInfo(final SystemInfoListener listener) { - String uri = "ssap://system/getSystemInfo"; - - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, new ResponseListener() { - - @Override - public void onSuccess(Object response) { - try { - JSONObject jsonObj = (JSONObject)response; - JSONObject features = (JSONObject) jsonObj.get("features"); - Util.postSuccess(listener, features); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }); - - request.send(); - } - - public void secureAccessTest(final SecureAccessTestListener listener) { - String uri = "ssap://com.webos.service.secondscreen.gateway/test/secure"; - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - - try { - JSONObject jsonObj = (JSONObject) response; - boolean isSecure = (Boolean) jsonObj.get("returnValue"); - Util.postSuccess(listener, isSecure); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, responseListener); - request.send(); - } - - public void getACRAuthToken(final ACRAuthTokenListener listener) { - String uri = "ssap://tv/getACRAuthToken"; - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - - try { - JSONObject jsonObj = (JSONObject) response; - String authToken = (String) jsonObj.get("token"); - Util.postSuccess(listener, authToken); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, responseListener); - request.send(); - } - - public void getLaunchPoints(final LaunchPointsListener listener) { - String uri = "ssap://com.webos.applicationManager/listLaunchPoints"; - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - - try { - JSONObject jsonObj = (JSONObject) response; - JSONArray launchPoints = (JSONArray) jsonObj.get("launchPoints"); - Util.postSuccess(listener, launchPoints); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - ServiceCommand> request = new ServiceCommand>(this, uri, null, true, responseListener); - request.send(); - } - - @Override - public void sendCommand(ServiceCommand command) { - if (socket != null) - socket.sendCommand(command); - } - - @Override - public void unsubscribe(URLServiceSubscription subscription) { - if (socket != null) - socket.unsubscribe(subscription); - } - - @Override - protected void updateCapabilities() { - List capabilities = new ArrayList(); - - if (DiscoveryManager.getInstance().getPairingLevel() == PairingLevel.ON) { - for (String capability : TextInputControl.Capabilities) { capabilities.add(capability); } - for (String capability : MouseControl.Capabilities) { capabilities.add(capability); } - for (String capability : KeyControl.Capabilities) { capabilities.add(capability); } - for (String capability : MediaPlayer.Capabilities) { capabilities.add(capability); } - for (String capability : Launcher.Capabilities) { capabilities.add(capability); } - for (String capability : TVControl.Capabilities) { capabilities.add(capability); } - for (String capability : ExternalInputControl.Capabilities) { capabilities.add(capability); } - for (String capability : VolumeControl.Capabilities) { capabilities.add(capability); } - for (String capability : ToastControl.Capabilities) { capabilities.add(capability); } - - capabilities.add(PowerControl.Off); - } else { - for (String capability : VolumeControl.Capabilities) { capabilities.add(capability); } - for (String capability : MediaPlayer.Capabilities) { capabilities.add(capability); } - - capabilities.add(Application); - capabilities.add(Application_Params); - capabilities.add(Application_Close); - capabilities.add(Browser); - capabilities.add(Browser_Params); - capabilities.add(Hulu); - capabilities.add(Netflix); - capabilities.add(Netflix_Params); - capabilities.add(YouTube); - capabilities.add(YouTube_Params); - capabilities.add(AppStore); - capabilities.add(AppStore_Params); - capabilities.add(AppState); - capabilities.add(AppState_Subscribe); - } - - if (serviceDescription != null && serviceDescription.getVersion() != null) { - if (serviceDescription.getVersion().contains("4.0.0") || serviceDescription.getVersion().contains("4.0.1")) { - capabilities.add(Launch); - capabilities.add(Launch_Params); - - capabilities.add(Play); - capabilities.add(Pause); - capabilities.add(Stop); - capabilities.add(Seek); - capabilities.add(Position); - capabilities.add(Duration); - capabilities.add(PlayState); - - capabilities.add(WebAppLauncher.Close); - } else { - for (String capability : WebAppLauncher.Capabilities) { capabilities.add(capability); } - for (String capability : MediaControl.Capabilities) { capabilities.add(capability); } - } - } - - setCapabilities(capabilities); - } - - public List getPermissions() { - if (permissions != null) - return permissions; - - List defaultPermissions = new ArrayList(); - for (String perm: kWebOSTVServiceOpenPermissions) { - defaultPermissions.add(perm); - } - - if (DiscoveryManager.getInstance().getPairingLevel() == PairingLevel.ON) { - for (String perm: kWebOSTVServiceProtectedPermissions) { - defaultPermissions.add(perm); - } - - for (String perm: kWebOSTVServicePersonalActivityPermissions) { - defaultPermissions.add(perm); - } - } - - permissions = defaultPermissions; - - return permissions; - } - - public void setPermissions(List permissions) { - this.permissions = permissions; - - WebOSTVServiceConfig config = (WebOSTVServiceConfig) serviceConfig; - - if (config.getClientKey() != null) { - config.setClientKey(null); - - if (isConnected()) { - Log.w("Connect SDK", "Permissions changed -- you will need to re-pair to the TV."); - disconnect(); - } - } - } - - private ChannelInfo parseRawChannelData(JSONObject channelRawData) { - String channelName = null; - String channelId = null; - String channelNumber = null; - int minorNumber; - int majorNumber; - - ChannelInfo channelInfo = new ChannelInfo(); - channelInfo.setRawData(channelRawData); - - try { - if (!channelRawData.isNull("channelName")) - channelName = (String) channelRawData.get("channelName"); - - if (!channelRawData.isNull("channelId")) - channelId = (String) channelRawData.get("channelId"); - - channelNumber = channelRawData.optString("channelNumber"); - - if (!channelRawData.isNull("majorNumber")) - majorNumber = (Integer) channelRawData.get("majorNumber"); - else - majorNumber = parseMajorNumber(channelNumber); - - if (!channelRawData.isNull("minorNumber")) - minorNumber = (Integer) channelRawData.get("minorNumber"); - else - minorNumber = parseMinorNumber(channelNumber); - - channelInfo.setName(channelName); - channelInfo.setId(channelId); - channelInfo.setNumber(channelNumber); - channelInfo.setMajorNumber(majorNumber); - channelInfo.setMinorNumber(minorNumber); - - } catch (JSONException e) { - e.printStackTrace(); - } - - return channelInfo; - } - - private int parseMinorNumber(String channelNumber) { - if (channelNumber != null) { - String tokens[] = channelNumber.split("-"); - return Integer.valueOf(tokens[tokens.length-1]); - } - else - return 0; - } - - private int parseMajorNumber(String channelNumber) { - if (channelNumber != null) { - String tokens[] = channelNumber.split("-"); - return Integer.valueOf(tokens[0]); - } - else - return 0; - } - - private List externalnputInfoFromJSONArray(JSONArray inputList) { - List externalInputInfoList = new ArrayList(); - - for (int i = 0; i < inputList.length(); i++) { - try { - JSONObject input = (JSONObject) inputList.get(i); - - String id = input.getString("id"); - String name = input.getString("label"); - boolean connected = input.getBoolean("connected"); - String iconURL = input.getString("icon"); - - ExternalInputInfo inputInfo = new ExternalInputInfo(); - inputInfo.setRawData(input); - inputInfo.setId(id); - inputInfo.setName(name); - inputInfo.setConnected(connected); - inputInfo.setIconURL(iconURL); - - externalInputInfoList.add(inputInfo); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - return externalInputInfoList; - } - -// @Override -// public LaunchSession decodeLaunchSession(String type, JSONObject obj) throws JSONException { -// if ("webostv".equals(type)) { -// LaunchSession launchSession = LaunchSession.launchSessionFromJSONObject(obj); -// launchSession.setService(this); -// return launchSession; -// } -// return null; -// } - - @Override - public void getPlayState(PlayStateListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public ServiceSubscription subscribePlayState(PlayStateListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - - return null; - } - - @Override - public boolean isConnectable() { - return true; - } - - @Override public void sendPairingKey(String pairingKey) { } - - public static interface ServiceInfoListener extends ResponseListener { } - - public static interface SystemInfoListener extends ResponseListener { } -} diff --git a/src/com/connectsdk/service/airplay/PListBuilder.java b/src/com/connectsdk/service/airplay/PListBuilder.java deleted file mode 100644 index ebdeec98..00000000 --- a/src/com/connectsdk/service/airplay/PListBuilder.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * PListBuilder - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Hyun Kook Khang on 18 Apr 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.airplay; - -import java.io.StringWriter; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.OutputKeys; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; - -import org.w3c.dom.DOMImplementation; -import org.w3c.dom.Document; -import org.w3c.dom.DocumentType; -import org.w3c.dom.Element; - -public class PListBuilder { - DocumentType dt; - Document doc; - - Element root; - Element rootDict; - - public PListBuilder() { - try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder; - builder = factory.newDocumentBuilder(); - DOMImplementation di = builder.getDOMImplementation(); - dt = di.createDocumentType("plist", - "-//Apple//DTD PLIST 1.0//EN", - "http://www.apple.com/DTDs/PropertyList-1.0.dtd"); - - doc = di.createDocument("", "plist", dt); - doc.setXmlStandalone(true); - - root = doc.getDocumentElement(); - root.setAttribute("version", "1.0"); - - rootDict = doc.createElement("dict"); - root.appendChild(rootDict); - } catch (ParserConfigurationException e) { - e.printStackTrace(); - } - } - - private void putKey(String key) { - Element eKey = doc.createElement("key"); - eKey.setTextContent(key); - rootDict.appendChild(eKey); - } - - public void putString(String key, String value) { - putKey(key); - - Element eValue = doc.createElement("string"); - eValue.setTextContent(value); - rootDict.appendChild(eValue); - } - - public void putReal(String key, double value) { - putKey(key); - - Element eValue = doc.createElement("real"); - eValue.setTextContent(String.valueOf(value)); - rootDict.appendChild(eValue); - } - - public void putInteger(String key, long value) { - putKey(key); - - Element eValue = doc.createElement("integer"); - eValue.setTextContent(String.valueOf(value)); - rootDict.appendChild(eValue); - } - - public void putBoolean(String key, boolean value) { - putKey(key); - - String str = value? "true" : "false"; - Element eValue = doc.createElement(str); - rootDict.appendChild(eValue); - } - - public void putData(String key, String value) { - putKey(key); - - Element eValue = doc.createElement("data"); - eValue.setTextContent(value); - rootDict.appendChild(eValue); - } - - @Override - public String toString() { - DOMSource domSource = new DOMSource(doc); - TransformerFactory tf = TransformerFactory.newInstance(); - Transformer t; - - StringWriter stringWriter = new StringWriter(); - - try { - t = tf.newTransformer(); - t.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); - t.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, dt.getPublicId()); - t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, dt.getSystemId()); - t.setOutputProperty(OutputKeys.INDENT, "yes"); - t.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); - - StreamResult streamResult = new StreamResult(stringWriter); - t.transform(domSource, streamResult); - } catch (TransformerConfigurationException e) { - e.printStackTrace(); - } catch (TransformerException e) { - e.printStackTrace(); - } - - return stringWriter.toString(); - } -} diff --git a/src/com/connectsdk/service/airplay/PListParser.java b/src/com/connectsdk/service/airplay/PListParser.java deleted file mode 100644 index 96de6d71..00000000 --- a/src/com/connectsdk/service/airplay/PListParser.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * PListParser - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Hyun Kook Khang on 18 Apr 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.airplay; - -import java.io.IOException; -import java.io.InputStream; - -import org.json.JSONException; -import org.json.JSONObject; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import android.util.Xml; - -public class PListParser { - private static final String ns = null; - - public JSONObject parse(InputStream in) throws XmlPullParserException, IOException, JSONException { - try { - XmlPullParser parser = Xml.newPullParser(); - parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); - parser.setInput(in, null); - parser.nextTag(); - return readPlist(parser); - } finally { - in.close(); - } - } - - private JSONObject readPlist(XmlPullParser parser) throws XmlPullParserException, IOException, JSONException { - JSONObject plist = null; - - parser.require(XmlPullParser.START_TAG, ns, "plist"); - while (parser.next() != XmlPullParser.END_TAG) { - if (parser.getEventType() != XmlPullParser.START_TAG) { - continue; - } - - String name = parser.getName(); - - if (name.equals("dict")) { - plist = readDict(parser); - } - } - - return plist; - } - - public JSONObject readDict(XmlPullParser parser) throws IOException, XmlPullParserException, JSONException { - JSONObject plist = new JSONObject(); - - parser.require(XmlPullParser.START_TAG, ns, "dict"); - - String key = null; - - while (parser.next() != XmlPullParser.END_TAG) { - if (parser.getEventType() != XmlPullParser.START_TAG) { - continue; - } - String name = parser.getName(); - - if (name.equals("key")) { - key = readKey(parser); - } - else if (key != null) { - if (name.equals("data")) { - plist.put(key, readData(parser)); - } - else if (name.equals("integer")) { - plist.put(key, readInteger(parser)); - } - else if (name.equals("string")) { - plist.put(key, readString(parser)); - } - else if (name.equals("real")) { - plist.put(key, readReal(parser)); - } - else if (name.equals("true") || name.equals("false")) { - plist.put(key, Boolean.valueOf(name)); - skip(parser); - } - - key = null; - } - } - - return plist; - } - - private String readKey(XmlPullParser parser) throws IOException, XmlPullParserException { - parser.require(XmlPullParser.START_TAG, ns, "key"); - String key = readText(parser); - parser.require(XmlPullParser.END_TAG, ns, "key"); - return key; - } - - private String readData(XmlPullParser parser) throws IOException, XmlPullParserException { - parser.require(XmlPullParser.START_TAG, ns, "data"); - String value = readText(parser); - parser.require(XmlPullParser.END_TAG, ns, "data"); - return value; - } - - private int readInteger(XmlPullParser parser) throws IOException, XmlPullParserException { - parser.require(XmlPullParser.START_TAG, ns, "integer"); - int value = Integer.valueOf(readText(parser)); - parser.require(XmlPullParser.END_TAG, ns, "integer"); - return value; - } - - private double readReal(XmlPullParser parser) throws IOException, XmlPullParserException { - parser.require(XmlPullParser.START_TAG, ns, "real"); - double value = Double.valueOf(readText(parser)); - parser.require(XmlPullParser.END_TAG, ns, "real"); - return value; - } - - private String readString(XmlPullParser parser) throws IOException, XmlPullParserException { - parser.require(XmlPullParser.START_TAG, ns, "string"); - String value = readText(parser); - parser.require(XmlPullParser.END_TAG, ns, "string"); - return value; - } - - private String readText(XmlPullParser parser) throws IOException, XmlPullParserException { - String result = ""; - if (parser.next() == XmlPullParser.TEXT) { - result = parser.getText(); - parser.nextTag(); - } - return result; - } - - private void skip(XmlPullParser parser) throws XmlPullParserException, IOException { - if (parser.getEventType() != XmlPullParser.START_TAG) { - throw new IllegalStateException(); - } - int depth = 1; - while (depth != 0) { - switch (parser.next()) { - case XmlPullParser.END_TAG: - depth--; - break; - case XmlPullParser.START_TAG: - depth++; - break; - } - } - } -} diff --git a/src/com/connectsdk/service/capability/CapabilityMethods.java b/src/com/connectsdk/service/capability/CapabilityMethods.java deleted file mode 100644 index 0e2bbcf3..00000000 --- a/src/com/connectsdk/service/capability/CapabilityMethods.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * CapabilityMethods - * 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.capability; - -import java.util.regex.Pattern; - -public interface CapabilityMethods { - // @cond INTERNAL - public final static Pattern ANY_PATTERN = Pattern.compile(".+\\.(?=Any)"); - // @endcond - - /** - * CapabilityPriorityLevel values are used by ConnectableDevice to find the most suitable DeviceService capability to be presented to the user. Values of VeryLow and VeryHigh are not in use internally the SDK. Connect SDK uses Low, Normal, and High internally. - * - * Default behavior: - * If you are unsatisfied with the default priority levels & behavior of Connect SDK, it is possible to subclass a particular DeviceService and provide your own value for each capability. That DeviceService subclass would need to be registered with DiscoveryManager. - */ - public enum CapabilityPriorityLevel { - VERY_LOW (1), - LOW (25), - NORMAL (50), - HIGH (75), - VERY_HIGH (100); - - private final int value; - - CapabilityPriorityLevel(int value) { - this.value = value; - } - - public int getValue() { return value; } - } -} diff --git a/src/com/connectsdk/service/capability/ExternalInputControl.java b/src/com/connectsdk/service/capability/ExternalInputControl.java deleted file mode 100644 index db3c6aaf..00000000 --- a/src/com/connectsdk/service/capability/ExternalInputControl.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * ExternalInputControl - * 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.capability; - -import java.util.List; - -import com.connectsdk.core.ExternalInputInfo; -import com.connectsdk.service.capability.Launcher.AppLaunchListener; -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.sessions.LaunchSession; - -public interface ExternalInputControl extends CapabilityMethods { - public final static String Any = "ExternalInputControl.Any"; - - public final static String Picker_Launch = "ExternalInputControl.Picker.Launch"; - public final static String Picker_Close = "ExternalInputControl.Picker.Close"; - public final static String List = "ExternalInputControl.List"; - public final static String Set = "ExternalInputControl.Set"; - - public final static String[] Capabilities = { - Picker_Launch, - Picker_Close, - List, - Set - }; - - public ExternalInputControl getExternalInput(); - public CapabilityPriorityLevel getExternalInputControlPriorityLevel(); - - public void launchInputPicker(AppLaunchListener listener); - public void closeInputPicker(LaunchSession launchSessionm, ResponseListener listener); - - public void getExternalInputList(ExternalInputListListener listener); - public void setExternalInput(ExternalInputInfo input, ResponseListener listener); - - /** - * Success block that is called upon successfully getting the external input list. - * - * Passes a list containing an ExternalInputInfo object for each available external input on the device - */ - public interface ExternalInputListListener extends ResponseListener> { } -} diff --git a/src/com/connectsdk/service/capability/KeyControl.java b/src/com/connectsdk/service/capability/KeyControl.java deleted file mode 100644 index 8499ca78..00000000 --- a/src/com/connectsdk/service/capability/KeyControl.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * KeyControl - * 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.capability; - -import com.connectsdk.service.capability.CapabilityMethods; -import com.connectsdk.service.capability.listeners.ResponseListener; - -public interface KeyControl extends CapabilityMethods { - public final static String Any = "KeyControl.Any"; - - public final static String Up = "KeyControl.Up"; - public final static String Down = "KeyControl.Down"; - public final static String Left = "KeyControl.Left"; - public final static String Right = "KeyControl.Right"; - public final static String OK = "KeyControl.OK"; - public final static String Back = "KeyControl.Back"; - public final static String Home = "KeyControl.Home"; - public final static String Send_Key = "KeyControl.SendKey"; - - public final static String[] Capabilities = { - Up, - Down, - Left, - Right, - OK, - Back, - Home - }; - - public KeyControl getKeyControl(); - public CapabilityPriorityLevel getKeyControlCapabilityLevel(); - - public void up(ResponseListener listener); - public void down(ResponseListener listener); - public void left(ResponseListener listener); - public void right(ResponseListener listener); - public void ok(ResponseListener listener); - public void back(ResponseListener listener); - public void home(ResponseListener listener); - public void sendKeyCode(int keyCode, ResponseListener listener); -} diff --git a/src/com/connectsdk/service/capability/Launcher.java b/src/com/connectsdk/service/capability/Launcher.java deleted file mode 100644 index 6826143d..00000000 --- a/src/com/connectsdk/service/capability/Launcher.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Launcher - * 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.capability; - -import java.util.List; - -import com.connectsdk.core.AppInfo; -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.command.ServiceSubscription; -import com.connectsdk.service.sessions.LaunchSession; - -public interface Launcher extends CapabilityMethods { - public final static String Any = "Launcher.Any"; - - public final static String Application = "Launcher.App"; - public final static String Application_Params = "Launcher.App.Params"; - public final static String Application_Close = "Launcher.App.Close"; - public final static String Application_List = "Launcher.App.List"; - public final static String Browser = "Launcher.Browser"; - public final static String Browser_Params = "Launcher.Browser.Params"; - public final static String Hulu = "Launcher.Hulu"; - public final static String Hulu_Params = "Launcher.Hulu.Params"; - public final static String Netflix = "Launcher.Netflix"; - public final static String Netflix_Params = "Launcher.Netflix.Params"; - public final static String YouTube = "Launcher.YouTube"; - public final static String YouTube_Params = "Launcher.YouTube.Params"; - public final static String AppStore = "Launcher.AppStore"; - public final static String AppStore_Params = "Launcher.AppStore.Params"; - public final static String AppState = "Launcher.AppState"; - public final static String AppState_Subscribe = "Launcher.AppState.Subscribe"; - public final static String RunningApp = "Launcher.RunningApp"; - public final static String RunningApp_Subscribe = "Launcher.RunningApp.Subscribe"; - - public final static String[] Capabilities = { - Application, - Application_Params, - Application_Close, - Application_List, - Browser, - Browser_Params, - Hulu, - Hulu_Params, - Netflix, - Netflix_Params, - YouTube, - YouTube_Params, - AppStore, - AppStore_Params, - AppState, - AppState_Subscribe, - RunningApp, - RunningApp_Subscribe - }; - - public Launcher getLauncher(); - public CapabilityPriorityLevel getLauncherCapabilityLevel(); - - public void launchAppWithInfo(AppInfo appInfo, AppLaunchListener listener); - public void launchAppWithInfo(AppInfo appInfo, Object params, AppLaunchListener listener); - public void launchApp(String appId, AppLaunchListener listener); - - public void closeApp(LaunchSession launchSession, ResponseListener listener); - - public void getAppList(AppListListener listener); - - public void getRunningApp(AppInfoListener listener); - public ServiceSubscription subscribeRunningApp(AppInfoListener listener); - - public void getAppState(LaunchSession launchSession, AppStateListener listener); - public ServiceSubscription subscribeAppState(LaunchSession launchSession, AppStateListener listener); - - public void launchBrowser(String url, AppLaunchListener listener); - public void launchYouTube(String contentId, AppLaunchListener listener); - public void launchYouTube(String contentId, float startTime, AppLaunchListener listener); - public void launchNetflix(String contentId, AppLaunchListener listener); - public void launchHulu(String contentId, AppLaunchListener listener); - public void launchAppStore(String appId, AppLaunchListener listener); - - /** - * Success listener that is called upon successfully launching an app. - * - * Passes a LaunchSession Object containing important information about the app's launch session - */ - public static interface AppLaunchListener extends ResponseListener { } - - /** - * Success listener that is called upon requesting info about the current running app. - * - * Passes an AppInfo object containing info about the running app - */ - public static interface AppInfoListener extends ResponseListener { } - - /** - * Success block that is called upon successfully getting the app list. - * - * Passes a List containing an AppInfo for each available app on the device - */ - public static interface AppListListener extends ResponseListener> { } - - // @cond INTERNAL - public static interface AppCountListener extends ResponseListener { } - // @endcond - - /** - * Success block that is called upon successfully getting an app's state. - * - * Passes an AppState object which contains information about the running app. - */ - public static interface AppStateListener extends ResponseListener { } - - /** - * Helper class used with the AppStateListener to return the current state of an app. - */ - public static class AppState { - /** Whether the app is currently running. */ - public boolean running; - /** Whether the app is currently visible. */ - public boolean visible; - - public AppState(boolean running, boolean visible) { - this.running = running; - this.visible = visible; - } - } -} diff --git a/src/com/connectsdk/service/capability/MediaControl.java b/src/com/connectsdk/service/capability/MediaControl.java deleted file mode 100644 index 2a671155..00000000 --- a/src/com/connectsdk/service/capability/MediaControl.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * MediaControl - * 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.capability; - -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.command.ServiceSubscription; - -public interface MediaControl extends CapabilityMethods { - public final static String Any = "MediaControl.Any"; - - public final static String Play = "MediaControl.Play"; - public final static String Pause = "MediaControl.Pause"; - public final static String Stop = "MediaControl.Stop"; - public final static String Rewind = "MediaControl.Rewind"; - public final static String FastForward = "MediaControl.FastForward"; - public final static String Seek = "MediaControl.Seek"; - public final static String Duration = "MediaControl.Duration"; - public final static String PlayState = "MediaControl.PlayState"; - public final static String PlayState_Subscribe = "MediaControl.PlayState.Subscribe"; - public final static String Position = "MediaControl.Position"; - - public final static String[] Capabilities = { - Play, - Pause, - Stop, - Rewind, - FastForward, - Seek, - Duration, - PlayState, - PlayState_Subscribe, - Position, - }; - - public enum PlayStateStatus { - Unknown, - Idle, - Playing, - Paused, - Buffering, - Finished - }; - - public MediaControl getMediaControl(); - public CapabilityPriorityLevel getMediaControlCapabilityLevel(); - - public void play(ResponseListener listener); - public void pause(ResponseListener listener); - public void stop(ResponseListener listener); - public void rewind(ResponseListener listener); - public void fastForward(ResponseListener listener); - - public void seek(long position, ResponseListener listener); - public void getDuration(DurationListener listener); - public void getPosition(PositionListener listener); - - public void getPlayState(PlayStateListener listener); - public ServiceSubscription subscribePlayState(PlayStateListener listener); - - /** - * Success block that is called upon any change in a media file's play state. - * - * Passes a PlayStateStatus enum of the current media file - */ - public static interface PlayStateListener extends ResponseListener { } - - /** - * Success block that is called upon successfully getting the media file's current playhead position. - * - * Passes the position of the current playhead position of the current media file, in seconds - */ - public static interface PositionListener extends ResponseListener { } - - /** - * Success block that is called upon successfully getting the media file's duration. - * - * Passes the duration of the current media file, in seconds - */ - public static interface DurationListener extends ResponseListener { } -} diff --git a/src/com/connectsdk/service/capability/MediaPlayer.java b/src/com/connectsdk/service/capability/MediaPlayer.java deleted file mode 100644 index bd91e496..00000000 --- a/src/com/connectsdk/service/capability/MediaPlayer.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * MediaPlayer - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Hyun Kook Khang on Jan 19 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.capability; - -import com.connectsdk.core.MediaInfo; -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.sessions.LaunchSession; - -public interface MediaPlayer extends CapabilityMethods { - public final static String Any = "MediaPlayer.Any"; - - public final static String Display_Image = "MediaPlayer.Display.Image"; - public final static String Display_Video = "MediaPlayer.Display.Video"; - public final static String Display_Audio = "MediaPlayer.Display.Audio"; - public final static String Close = "MediaPlayer.Close"; - public final static String MetaData_Title = "MediaControl.MetaData.Title"; - public final static String MetaData_Description = "MediaControl.MetaData.Description"; - public final static String MetaData_Thumbnail = "MediaControl.MetaData.Thumbnail"; - public final static String MetaData_MimeType = "MediaControl.MetaData.MimeType"; - - public final static String[] Capabilities = { - Display_Image, - Display_Video, - Display_Audio, - Close, - MetaData_Title, - MetaData_Description, - MetaData_Thumbnail, - MetaData_MimeType - }; - - public MediaPlayer getMediaPlayer(); - public CapabilityPriorityLevel getMediaPlayerCapabilityLevel(); - - public void displayImage(String url, String mimeType, String title, String description, String iconSrc, LaunchListener listener); - public void playMedia(String url, String mimeType, String title, String description, String iconSrc, boolean shouldLoop, LaunchListener listener); - - public void closeMedia(LaunchSession launchSession, ResponseListener listener); - - /** - * Success block that is called upon successfully playing/displaying a media file. - * - * Passes a MediaLaunchObject which contains the objects for controlling media playback. - */ - public static interface LaunchListener extends ResponseListener { } - - /** - * Helper class used with the MediaPlayer.LaunchListener to return the current media playback. - */ - public static class MediaLaunchObject { - /** The LaunchSession object for the media launched. */ - public LaunchSession launchSession; - /** The MediaControl object for the media launched. */ - public MediaControl mediaControl; - - public MediaLaunchObject(LaunchSession launchSession, MediaControl mediaControl) { - this.launchSession = launchSession; - this.mediaControl = mediaControl; - } - } - - - public void displayImage(MediaInfo mediaInfo, LaunchListener listener); - - public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, - LaunchListener listener); - -} diff --git a/src/com/connectsdk/service/capability/MouseControl.java b/src/com/connectsdk/service/capability/MouseControl.java deleted file mode 100644 index d7ff6348..00000000 --- a/src/com/connectsdk/service/capability/MouseControl.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * MouseControl - * 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.capability; - -import android.graphics.PointF; - - -public interface MouseControl extends CapabilityMethods { - public final static String Any = "MouseControl.Any"; - - public final static String Connect = "MouseControl.Connect"; - public final static String Disconnect = "MouseControl.Disconnect"; - public final static String Click = "MouseControl.Click"; - public final static String Move = "MouseControl.Move"; - public final static String Scroll = "MouseControl.Scroll"; - - public final static String[] Capabilities = { - Connect, - Disconnect, - Click, - Move, - Scroll - }; - - public MouseControl getMouseControl(); - public CapabilityPriorityLevel getMouseControlCapabilityLevel(); - - public void connectMouse(); - public void disconnectMouse(); - - public void click(); - public void move(double dx, double dy); - public void move(PointF distance); - public void scroll(double dx, double dy); - public void scroll(PointF distance); -} diff --git a/src/com/connectsdk/service/capability/PowerControl.java b/src/com/connectsdk/service/capability/PowerControl.java deleted file mode 100644 index 4d174c7e..00000000 --- a/src/com/connectsdk/service/capability/PowerControl.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * PowerControl - * 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.capability; - -import com.connectsdk.service.capability.listeners.ResponseListener; - -public interface PowerControl extends CapabilityMethods { - public final static String Any = "PowerControl.Any"; - - public final static String Off = "PowerControl.Off"; - public final static String On = "PowerControl.On"; - - public final static String[] Capabilities = { - Off, - On - }; - - public PowerControl getPowerControl(); - public CapabilityPriorityLevel getPowerControlCapabilityLevel(); - - public void powerOff(ResponseListener listener); - public void powerOn(ResponseListener listener); -} diff --git a/src/com/connectsdk/service/capability/TVControl.java b/src/com/connectsdk/service/capability/TVControl.java deleted file mode 100644 index 9cf60253..00000000 --- a/src/com/connectsdk/service/capability/TVControl.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * TVControl - * 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.capability; - -import java.util.List; - -import com.connectsdk.core.ChannelInfo; -import com.connectsdk.core.ProgramInfo; -import com.connectsdk.core.ProgramList; -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.command.ServiceSubscription; - -public interface TVControl extends CapabilityMethods { - public final static String Any = "TVControl.Any"; - - public final static String Channel_Get = "TVControl.Channel.Get"; - public final static String Channel_Set = "TVControl.Channel.Set"; - public final static String Channel_Up = "TVControl.Channel.Up"; - public final static String Channel_Down = "TVControl.Channel.Down"; - public final static String Channel_List = "TVControl.Channel.List"; - public final static String Channel_Subscribe = "TVControl.Channel.Subscribe"; - public final static String Program_Get = "TVControl.Program.Get"; - public final static String Program_List = "TVControl.Program.List"; - public final static String Program_Subscribe = "TVControl.Program.Subscribe"; - public final static String Program_List_Subscribe = "TVControl.Program.List.Subscribe"; - public final static String Get_3D = "TVControl.3D.Get"; - public final static String Set_3D = "TVControl.3D.Set"; - public final static String Subscribe_3D = "TVControl.3D.Subscribe"; - - public final static String[] Capabilities = { - Channel_Get, - Channel_Set, - Channel_Up, - Channel_Down, - Channel_List, - Channel_Subscribe, - Program_Get, - Program_List, - Program_Subscribe, - Program_List_Subscribe, - Get_3D, - Set_3D, - Subscribe_3D - }; - - public TVControl getTVControl(); - public CapabilityPriorityLevel getTVControlCapabilityLevel(); - - public void channelUp(ResponseListener listener); - public void channelDown(ResponseListener listener); - - public void setChannel(ChannelInfo channelNumber, ResponseListener listener); - - public void getCurrentChannel(ChannelListener listener); - public ServiceSubscription subscribeCurrentChannel(ChannelListener listener); - - public void getChannelList(ChannelListListener listener); - - public void getProgramInfo(ProgramInfoListener listener); - public ServiceSubscription subscribeProgramInfo(ProgramInfoListener listener); - - public void getProgramList(ProgramListListener listener); - public ServiceSubscription subscribeProgramList(ProgramListListener listener); - - public void get3DEnabled(State3DModeListener listener); - public void set3DEnabled(boolean enabled, ResponseListener listener); - public ServiceSubscription subscribe3DEnabled(State3DModeListener listener); - - /** - * Success block that is called upon successfully getting the TV's 3D mode - * - * Passes a Boolean to see Whether 3D mode is currently enabled on the TV - */ - public static interface State3DModeListener extends ResponseListener { } - - /** - * Success block that is called upon successfully getting the current channel's information. - * - * Passes a ChannelInfo object containing information about the current channel - */ - public static interface ChannelListener extends ResponseListener{ } - - /** - * Success block that is called upon successfully getting the channel list. - * - * Passes a List of ChannelList objects for each available channel on the TV - */ - public static interface ChannelListListener extends ResponseListener>{ } - - /** - * Success block that is called upon successfully getting the current program's information. - * - * Passes a ProgramInfo object containing information about the current program - */ - public static interface ProgramInfoListener extends ResponseListener { } - - /** - * Success block that is called upon successfully getting the program list for the current channel. - * - * Passes a ProgramList containing a ProgramInfo object for each available program on the TV's current channel - */ - public static interface ProgramListListener extends ResponseListener { } -} diff --git a/src/com/connectsdk/service/capability/TextInputControl.java b/src/com/connectsdk/service/capability/TextInputControl.java deleted file mode 100644 index be2b6712..00000000 --- a/src/com/connectsdk/service/capability/TextInputControl.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * TextInputControl - * 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.capability; - -import com.connectsdk.core.TextInputStatusInfo; -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.command.ServiceSubscription; - -public interface TextInputControl extends CapabilityMethods { - public final static String Any = "TextInputControl.Any"; - - public final static String Send = "TextInputControl.Send"; - public final static String Send_Enter = "TextInputControl.Enter"; - public final static String Send_Delete = "TextInputControl.Delete"; - public final static String Subscribe = "TextInputControl.Subscribe"; - - public final static String[] Capabilities = { - Send, - Send_Enter, - Send_Delete, - Subscribe - }; - - public TextInputControl getTextInputControl(); - public CapabilityPriorityLevel getTextInputControlCapabilityLevel(); - - public ServiceSubscription subscribeTextInputStatus(TextInputStatusListener listener); - - public void sendText(String input); - public void sendEnter(); - public void sendDelete(); - - /** - * Response block that is fired on any change of keyboard visibility. - * - * Passes TextInputStatusInfo object that provides keyboard type & visibility information - */ - public static interface TextInputStatusListener extends ResponseListener { } -} diff --git a/src/com/connectsdk/service/capability/ToastControl.java b/src/com/connectsdk/service/capability/ToastControl.java deleted file mode 100644 index 894e4595..00000000 --- a/src/com/connectsdk/service/capability/ToastControl.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * ToastControl - * 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.capability; - -import org.json.JSONObject; - -import com.connectsdk.core.AppInfo; -import com.connectsdk.service.capability.listeners.ResponseListener; - -public interface ToastControl extends CapabilityMethods { - public final static String Any = "ToastControl.Any"; - - public final static String Show_Toast = "ToastControl.Show"; - public final static String Show_Clickable_Toast_App = "ToastControl.Show.Clickable.App"; - public final static String Show_Clickable_Toast_App_Params = "ToastControl.Show.Clickable.App.Params"; - public final static String Show_Clickable_Toast_URL = "ToastControl.Show.Clickable.URL"; - - public final static String[] Capabilities = { - Show_Toast, - Show_Clickable_Toast_App, - Show_Clickable_Toast_App_Params, - Show_Clickable_Toast_URL - }; - - public ToastControl getToastControl(); - public CapabilityPriorityLevel getToastControlCapabilityLevel(); - - public void showToast(String message, ResponseListener listener); - public void showToast(String message, String iconData, String iconExtension, ResponseListener listener); - - public void showClickableToastForApp(String message, AppInfo appInfo, JSONObject params, ResponseListener listener); - public void showClickableToastForApp(String message, AppInfo appInfo, JSONObject params, String iconData, String iconExtension, ResponseListener listener); - - public void showClickableToastForURL(String message, String url, ResponseListener listener); - public void showClickableToastForURL(String message, String url, String iconData, String iconExtension, ResponseListener listener); -} \ No newline at end of file diff --git a/src/com/connectsdk/service/capability/VolumeControl.java b/src/com/connectsdk/service/capability/VolumeControl.java deleted file mode 100644 index 171d1cb5..00000000 --- a/src/com/connectsdk/service/capability/VolumeControl.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * VolumeControl - * 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.capability; - -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.command.ServiceSubscription; - -public interface VolumeControl extends CapabilityMethods { - public final static String Any = "VolumeControl.Any"; - - public final static String Volume_Get = "VolumeControl.Get"; - public final static String Volume_Set = "VolumeControl.Set"; - public final static String Volume_Up_Down = "VolumeControl.UpDown"; - public final static String Volume_Subscribe = "VolumeControl.Subscribe"; - public final static String Mute_Get = "VolumeControl.Mute.Get"; - public final static String Mute_Set = "VolumeControl.Mute.Set"; - public final static String Mute_Subscribe = "VolumeControl.Mute.Subscribe"; - - public final static String[] Capabilities = { - Volume_Get, - Volume_Set, - Volume_Up_Down, - Volume_Subscribe, - Mute_Get, - Mute_Set, - Mute_Subscribe - }; - - public VolumeControl getVolumeControl(); - public CapabilityPriorityLevel getVolumeControlCapabilityLevel(); - - public void volumeUp(ResponseListener listener); - public void volumeDown(ResponseListener listener); - - public void setVolume(float volume, ResponseListener listener); - public void getVolume(VolumeListener listener); - - public void setMute(boolean isMute, ResponseListener listener); - public void getMute(MuteListener listener); - - public ServiceSubscription subscribeVolume(VolumeListener listener); - public ServiceSubscription subscribeMute(MuteListener listener); - - /** - * Success block that is called upon successfully getting the device's system volume. - * - * Passes the current system volume, value is a float between 0.0 and 1.0 - */ - public static interface VolumeListener extends ResponseListener { } - - /** - * Success block that is called upon successfully getting the device's system mute status. - * - * Passes current system mute status - */ - public static interface MuteListener extends ResponseListener { } - - /** - * Success block that is called upon successfully getting the device's system volume status. - * - * Passes current system mute status - */ - public static interface VolumeStatusListener extends ResponseListener { } - - /** - * Helper class used with the VolumeControl.VolueStatusListener to return the current volume status. - */ - public static class VolumeStatus { - public boolean isMute; - public float volume; - - public VolumeStatus(boolean isMute, float volume) { - this.isMute = isMute; - this.volume = volume; - } - } -} diff --git a/src/com/connectsdk/service/capability/WebAppLauncher.java b/src/com/connectsdk/service/capability/WebAppLauncher.java deleted file mode 100644 index 4b635cc1..00000000 --- a/src/com/connectsdk/service/capability/WebAppLauncher.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * WebAppLauncher - * 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.capability; - -import org.json.JSONObject; - -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.sessions.LaunchSession; -import com.connectsdk.service.sessions.WebAppSession.LaunchListener; - - -public interface WebAppLauncher extends CapabilityMethods { - public final static String Any = "WebAppLauncher.Any"; - - public final static String Launch = "WebAppLauncher.Launch"; - public final static String Launch_Params = "WebAppLauncher.Launch.Params"; - public final static String Message_Send = "WebAppLauncher.Message.Send"; - public final static String Message_Receive = "WebAppLauncher.Message.Receive"; - public final static String Message_Send_JSON = "WebAppLauncher.Message.Send.JSON"; - public final static String Message_Receive_JSON = "WebAppLauncher.Message.Receive.JSON"; - public final static String Connect = "WebAppLauncher.Connect"; - public final static String Disconnect = "WebAppLauncher.Disconnect"; - public final static String Join = "WebAppLauncher.Join"; - public final static String Close = "WebAppLauncher.Close"; - - public final static String[] Capabilities = { - Launch, - Launch_Params, - Message_Send, - Message_Receive, - Message_Send_JSON, - Message_Receive_JSON, - Connect, - Disconnect, - Join, - Close - }; - - public WebAppLauncher getWebAppLauncher(); - public CapabilityPriorityLevel getWebAppLauncherCapabilityLevel(); - - public void launchWebApp(String webAppId, LaunchListener listener); - public void launchWebApp(String webAppId, boolean relaunchIfRunning, LaunchListener listener); - public void launchWebApp(String webAppId, JSONObject params, LaunchListener listener); - public void launchWebApp(String webAppId, JSONObject params, boolean relaunchIfRunning, LaunchListener listener); - public void joinWebApp(LaunchSession webAppLaunchSession, LaunchListener listener); - public void joinWebApp(String webAppId, LaunchListener listener); - public void closeWebApp(LaunchSession launchSession, ResponseListener listener); -} diff --git a/src/com/connectsdk/service/capability/listeners/ErrorListener.java b/src/com/connectsdk/service/capability/listeners/ErrorListener.java deleted file mode 100644 index 9467c046..00000000 --- a/src/com/connectsdk/service/capability/listeners/ErrorListener.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * ErrorListener - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Jeffrey Glenn on 07 Mar 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.capability.listeners; - -import com.connectsdk.service.command.ServiceCommandError; - -/** - * Generic asynchronous operation response error handler block. In all cases, you will get a valid ServiceCommandError object. Connect SDK will make all attempts to give you the lowest-level error possible. In cases where an error is generated by Connect SDK, an enumerated error code (ConnectStatusCode) will be present on the ServiceCommandError object. - * - * ###Low-level error example - * ####Situation - * Connect SDK receives invalid XML from a device, generating a parsing error - * - * ####Result - * Connect SDK will call the ErrorListener and pass off the ServiceCommandError generated during parsing of the XML. - * - * ###High-level error example - * ####Situation - * An invalid value is passed to a device capability method - * - * ####Result - * The capability method will immediately invoke the ErrorListener and pass off an ServiceCommandError object with a status code of ConnectStatusCodeArgumentError. - * - * @param error ServiceCommandError object describing the nature of the problem. Error descriptions are not localized and mostly intended for developer use. It is not recommended to display most error descriptions in UI elements. - */ -public interface ErrorListener { - - /** - * Method to return the error that was generated. Will pass an error object with a helpful status code and error message. - * - * @param error ServiceCommandError describing the error - */ - public void onError(ServiceCommandError error); -} diff --git a/src/com/connectsdk/service/capability/listeners/ResponseListener.java b/src/com/connectsdk/service/capability/listeners/ResponseListener.java deleted file mode 100644 index 9e536c96..00000000 --- a/src/com/connectsdk/service/capability/listeners/ResponseListener.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * ResponseListener - * 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.capability.listeners; - -/** - * Generic asynchronous operation response success handler block. If there is any response data to be processed, it will be provided via the responseObject parameter. - * - * @param responseObject Contains the output data as a generic object reference. This value may be any of a number of types as defined by T in subclasses of ResponseListener. It is also possible that responseObject will be nil for operations that don't require data to be returned (move mouse, send key code, etc). - */ -public interface ResponseListener extends ErrorListener { - - /** - * Returns the success of the call of type T. - * - * @param object Response object, can be any number of object types, depending on the protocol/capability/etc - */ - abstract public void onSuccess(T object); -} diff --git a/src/com/connectsdk/service/command/NotSupportedServiceSubscription.java b/src/com/connectsdk/service/command/NotSupportedServiceSubscription.java deleted file mode 100644 index 68a9037e..00000000 --- a/src/com/connectsdk/service/command/NotSupportedServiceSubscription.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * ErrorListener - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Jason Lai on 31 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.command; - -import java.util.ArrayList; -import java.util.List; - - -public class NotSupportedServiceSubscription implements ServiceSubscription { - private List listeners = new ArrayList(); - - @Override - public void unsubscribe() { - } - - @Override - public T addListener(T listener) { - listeners.add(listener); - - return listener; - } - - @Override - public List getListeners() { - return listeners; - } - - @Override - public void removeListener(T listener) { - listeners.remove(listener); - } -} diff --git a/src/com/connectsdk/service/command/ServiceCommand.java b/src/com/connectsdk/service/command/ServiceCommand.java deleted file mode 100644 index 020f9eda..00000000 --- a/src/com/connectsdk/service/command/ServiceCommand.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * ServiceCommand - * 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.command; - -import org.apache.http.client.methods.HttpDelete; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpRequestBase; -import org.json.JSONObject; - -import com.connectsdk.service.capability.listeners.ResponseListener; - -/** - * Internal implementation of ServiceCommand for URL-based commands - */ -public class ServiceCommand> { - public static final String TYPE_REQ = "request"; - public static final String TYPE_SUB = "subscribe"; - public static final String TYPE_GET = "GET"; - public static final String TYPE_POST = "POST"; - public static final String TYPE_DEL = "DELETE"; - - ServiceCommandProcessor processor; - String httpMethod; // WebOSTV: {request, subscribe}, NetcastTV: {GET, POST} - Object payload; - String target; - int requestId; - - ResponseListener responseListener; - - public ServiceCommand(ServiceCommandProcessor processor, String targetURL, Object payload, ResponseListener listener) { - this.processor = processor; - this.target = targetURL; - this.payload = payload; - this.responseListener = listener; - this.httpMethod = TYPE_POST; - } - - public ServiceCommand(ServiceCommandProcessor processor, String uri, JSONObject payload, boolean isWebOS, ResponseListener listener) { - this.processor = processor; - target = uri; - this.payload = payload; - requestId = -1; - httpMethod = "request"; - responseListener = listener; - } - - public void send() { - processor.sendCommand(this); - } - - public ServiceCommandProcessor getCommandProcessor() { - return processor; - } - - public void setCommandProcessor(ServiceCommandProcessor processor) { - this.processor = processor; - } - - public Object getPayload() { - return payload; - } - - public void setPayload(Object payload) { - this.payload = payload; - } - - public String getHttpMethod() { - return httpMethod; - } - - public void setHttpMethod(String httpMethod) { - this.httpMethod = httpMethod; - } - - public String getTarget() { - return target; - } - - public void setTarget(String target) { - this.target = target; - } - - public int getRequestId() { - return requestId; - } - - public void setRequestId(int requestId) { - this.requestId = requestId; - } - - public HttpRequestBase getRequest() { - if (target == null) { - throw new IllegalStateException("ServiceCommand has no target url"); - } - - if (this.httpMethod.equalsIgnoreCase(TYPE_GET)) { - return new HttpGet(target); - } else if (this.httpMethod.equalsIgnoreCase(TYPE_POST)) { - return new HttpPost(target); - } else if (this.httpMethod.equalsIgnoreCase(TYPE_DEL)) { - return new HttpDelete(target); - } else { - return null; - } - } - - public ResponseListener getResponseListener() { - return responseListener; - } - - public interface ServiceCommandProcessor { - public void unsubscribe(URLServiceSubscription subscription); - public void sendCommand(ServiceCommand command); - } -} \ No newline at end of file diff --git a/src/com/connectsdk/service/command/ServiceCommandError.java b/src/com/connectsdk/service/command/ServiceCommandError.java deleted file mode 100644 index dcd77f1e..00000000 --- a/src/com/connectsdk/service/command/ServiceCommandError.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * ServiceCommandError - * 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.command; - - -public class ServiceCommandError extends Error { - /** - * - */ - private static final long serialVersionUID = 4232138682873631468L; - - int code; - Object payload; - - public static ServiceCommandError notSupported() { - return new ServiceCommandError(503, "not supported", null); - } - - public ServiceCommandError(int code, String desc, Object payload) { - super(desc); - this.code = code; - this.payload = payload; - } - - public int getCode() { - return code; - } - - public Object getPayload() { - return payload; - } - - public static ServiceCommandError getError(int code) { - String desc = null; - if ( code == 400 ) { - desc = "Bad Request"; - } - else if ( code == 401 ) { - desc = "Unauthorized"; - } - else if ( code == 500 ) { - desc = "Internal Server Error"; - } - else if ( code == 503 ) { - desc = "Service Unavailable"; - } - else { - desc = "Unknown Error"; - } - - return new ServiceCommandError(code, desc, null); - } -} diff --git a/src/com/connectsdk/service/command/ServiceSubscription.java b/src/com/connectsdk/service/command/ServiceSubscription.java deleted file mode 100644 index e7a9cb24..00000000 --- a/src/com/connectsdk/service/command/ServiceSubscription.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * ServiceSubscription - * 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.command; - -import java.util.List; - -public interface ServiceSubscription { - public void unsubscribe(); - - public T addListener(T listener); - - public void removeListener(T listener); - - public List getListeners(); -} diff --git a/src/com/connectsdk/service/command/URLServiceSubscription.java b/src/com/connectsdk/service/command/URLServiceSubscription.java deleted file mode 100644 index ab10ece3..00000000 --- a/src/com/connectsdk/service/command/URLServiceSubscription.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * URLServiceSubscription - * 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.command; - -import java.util.ArrayList; -import java.util.List; - -import org.json.JSONObject; - -import com.connectsdk.service.capability.listeners.ResponseListener; - -/** - * Internal implementation of ServiceSubscription for URL-based commands - */ -public class URLServiceSubscription> extends ServiceCommand implements ServiceSubscription { - private List listeners = new ArrayList(); - - public URLServiceSubscription(ServiceCommandProcessor processor, String uri, JSONObject payload, ResponseListener listener) { - super(processor, uri, payload, listener); - } - - public URLServiceSubscription(ServiceCommandProcessor processor, String uri, JSONObject payload, boolean isWebOS, ResponseListener listener) { - super(processor, uri, payload, isWebOS, listener); - - if (isWebOS) - httpMethod = "subscribe"; - } - - public void send() { - this.subscribe(); - } - - public void subscribe() { - if ( !(httpMethod.equalsIgnoreCase(TYPE_GET) - || httpMethod.equalsIgnoreCase(TYPE_POST)) ) { - httpMethod = "subscribe"; - } - processor.sendCommand(this); - } - - public void unsubscribe() { - processor.unsubscribe(this); - } - - public T addListener(T listener) { - listeners.add(listener); - - return listener; - } - - public void removeListener(T listener) { - listeners.remove(listener); - } - - public void removeListeners() { - listeners.clear(); - } - - public List getListeners() { - return listeners; - } -} diff --git a/src/com/connectsdk/service/config/NetcastTVServiceConfig.java b/src/com/connectsdk/service/config/NetcastTVServiceConfig.java deleted file mode 100644 index e3f23d4b..00000000 --- a/src/com/connectsdk/service/config/NetcastTVServiceConfig.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * NetcastTVServiceConfig - * 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.config; - -import org.json.JSONException; -import org.json.JSONObject; - -public class NetcastTVServiceConfig extends ServiceConfig { - public static final String KEY_PAIRING = "pairingKey"; - String pairingKey; - - public NetcastTVServiceConfig(String serviceUUID) { - super(serviceUUID); - } - - public NetcastTVServiceConfig(String serviceUUID, String pairingKey) { - super(serviceUUID); - this.pairingKey = pairingKey; - } - - public NetcastTVServiceConfig(JSONObject json) { - super(json); - - pairingKey = json.optString(KEY_PAIRING, null); - } - - public String getPairingKey() { - return pairingKey; - } - - public void setPairingKey(String pairingKey) { - this.pairingKey = pairingKey; - } - - @Override - public JSONObject toJSONObject() { - JSONObject jsonObj = super.toJSONObject(); - - try { - jsonObj.put(KEY_PAIRING, pairingKey); - } catch (JSONException e) { - e.printStackTrace(); - } - - return jsonObj; - } - -} diff --git a/src/com/connectsdk/service/config/ServiceConfig.java b/src/com/connectsdk/service/config/ServiceConfig.java deleted file mode 100644 index d4602c69..00000000 --- a/src/com/connectsdk/service/config/ServiceConfig.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * ServiceConfig - * 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.config; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; - -import org.json.JSONException; -import org.json.JSONObject; - -import com.connectsdk.core.Util; - -public class ServiceConfig { - public static final String KEY_CLASS = "class"; - public static final String KEY_LAST_DETECT = "lastDetection"; - public static final String KEY_UUID = "UUID"; - private String serviceUUID; - private long lastDetected = Long.MAX_VALUE; - - boolean connected; - boolean wasConnected; - - public ServiceConfigListener listener; - - public ServiceConfig(String serviceUUID) { - this.serviceUUID = serviceUUID; - } - - public ServiceConfig(ServiceDescription desc) { - this.serviceUUID = desc.getUUID(); - this.connected = false; - this.wasConnected = false; - this.lastDetected = Util.getTime(); - } - - public ServiceConfig(ServiceConfig config) { - this.serviceUUID = config.serviceUUID; - this.connected = config.connected; - this.wasConnected = config.wasConnected; - this.lastDetected = config.lastDetected; - - this.listener = config.listener; - } - - public ServiceConfig(JSONObject json) { - serviceUUID = json.optString(KEY_UUID); - lastDetected = json.optLong(KEY_LAST_DETECT); - } - - @SuppressWarnings("unchecked") - public static ServiceConfig getConfig(JSONObject json) { - Class newServiceClass; - try { - newServiceClass = (Class) Class.forName(ServiceConfig.class.getPackage().getName() + "." + json.optString(KEY_CLASS)); - Constructor constructor = newServiceClass.getConstructor(JSONObject.class); - - return constructor.newInstance(json); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (InstantiationException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } - - return null; - } - - public String getServiceUUID() { - return serviceUUID; - } - - public void setServiceUUID(String serviceUUID) { - this.serviceUUID = serviceUUID; - } - - public String toString() { - return serviceUUID; - } - - public long getLastDetected() { - return lastDetected; - } - - public void setLastDetected(long value) { - lastDetected = value; - } - - public void detect() { - lastDetected = Util.getTime(); - } - - public ServiceConfigListener getListener() { - return listener; - } - - public void setListener(ServiceConfigListener listener) { - this.listener = listener; - } - - public JSONObject toJSONObject() { - JSONObject jsonObj = new JSONObject(); - - try { - jsonObj.put(KEY_CLASS, this.getClass().getSimpleName()); - jsonObj.put(KEY_LAST_DETECT, lastDetected); - jsonObj.put(KEY_UUID, serviceUUID); - } catch (JSONException e) { - e.printStackTrace(); - } - - return jsonObj; - } - - public static interface ServiceConfigListener { - public void onServiceConfigUpdate(ServiceConfig serviceConfig); - } -} diff --git a/src/com/connectsdk/service/config/ServiceDescription.java b/src/com/connectsdk/service/config/ServiceDescription.java deleted file mode 100644 index 11450d1e..00000000 --- a/src/com/connectsdk/service/config/ServiceDescription.java +++ /dev/null @@ -1,278 +0,0 @@ -/* - * ServiceDescription - * 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.config; - -import java.util.List; -import java.util.Map; - -import org.json.JSONException; -import org.json.JSONObject; - -import com.connectsdk.core.upnp.service.Service; - -public class ServiceDescription implements Cloneable { - - public static final String KEY_FILTER = "filter"; - public static final String KEY_IP_ADDRESS = "ipAddress"; - public static final String KEY_UUID = "uuid"; - public static final String KEY_FRIENDLY = "friendlyName"; - public static final String KEY_MODEL_NAME = "modelName"; - public static final String KEY_MODEL_NUMBER = "modelNumber"; - public static final String KEY_PORT = "port"; - public static final String KEY_VERSION = "version"; - public static final String KEY_SERVICE_ID = "serviceId"; - - String UUID; - String ipAddress; - String friendlyName; - String modelName; - String modelNumber; - String manufacturer; - String modelDescription; - String serviceFilter; - int port; - String applicationURL; - String version; - List serviceList; - String locationXML; - - String serviceURI; - - Map> responseHeaders; - - String serviceID; - - long lastDetection = Long.MAX_VALUE; - - public ServiceDescription() { } - - public ServiceDescription(String serviceFilter, String UUID, String ipAddress) { - this.serviceFilter = serviceFilter; - this.UUID = UUID; - this.ipAddress = ipAddress; - } - - public ServiceDescription(JSONObject json) { - serviceFilter = json.optString(KEY_FILTER, null); - ipAddress = json.optString(KEY_IP_ADDRESS, null); - UUID = json.optString(KEY_UUID, null); - friendlyName = json.optString(KEY_FRIENDLY, null); - modelName = json.optString(KEY_MODEL_NAME, null); - modelNumber = json.optString(KEY_MODEL_NUMBER, null); - port = json.optInt(KEY_PORT, -1); - version = json.optString(KEY_VERSION, null); - serviceID = json.optString(KEY_SERVICE_ID, null); - } - - public static ServiceDescription getDescription(JSONObject json) { - return new ServiceDescription(json); - } - - public String getServiceFilter() { - return serviceFilter; - } - - public void setServiceFilter(String serviceFilter) { - this.serviceFilter = serviceFilter; - } - - public String getUUID() { - return UUID; - } - - public void setUUID(String uUID) { - UUID = uUID; - } - - public String getIpAddress() { - return ipAddress; - } - - public void setIpAddress(String getIpAddress) { - this.ipAddress = getIpAddress; - } - - public void setPort(int port) { - this.port = port; - } - - public int getPort() { - return port; - } - - public String getFriendlyName() { - return friendlyName; - } - - public void setFriendlyName(String friendlyName) { - this.friendlyName = friendlyName; - } - - public String getModelName() { - return modelName; - } - - public void setModelName(String modelName) { - this.modelName = modelName; - } - - public String getModelNumber() { - return modelNumber; - } - - public void setModelNumber(String modelNumber) { - this.modelNumber = modelNumber; - } - - public String getManufacturer() { - return manufacturer; - } - - public void setManufacturer(String manufacturer) { - this.manufacturer = manufacturer; - } - - public String getModelDescription() { - return modelDescription; - } - - public void setModelDescription(String modelDescription) { - this.modelDescription = modelDescription; - } - - public void setServiceList(List serviceList) { - this.serviceList = serviceList; - } - - public String getApplicationURL() { - return applicationURL; - } - - public void setApplicationURL(String applicationURL) { - this.applicationURL = applicationURL; - } - - public List getServiceList() { - return serviceList; - } - - public long getLastDetection() { - return lastDetection; - } - - public void setLastDetection(long last) { - lastDetection = last; - } - - public String getServiceID() { - return serviceID; - } - - public void setServiceID(String serviceID) { - this.serviceID = serviceID; - } - - public Map> getResponseHeaders() { - return responseHeaders; - } - - public void setResponseHeaders(Map> responseHeaders) { - this.responseHeaders = responseHeaders; - } - - public String getVersion() { - return version; - } - - public void setVersion(String version) { - this.version = version; - } - - public String getLocationXML() { - return locationXML; - } - - public void setLocationXML(String locationXML) { - this.locationXML = locationXML; - } - - public String getServiceURI() { - return serviceURI; - } - - public void setServiceURI(String serviceURI) { - this.serviceURI = serviceURI; - } - - public JSONObject toJSONObject() { - JSONObject jsonObj = new JSONObject(); - - try { - jsonObj.putOpt(KEY_FILTER, serviceFilter); - jsonObj.putOpt(KEY_IP_ADDRESS, ipAddress); - jsonObj.putOpt(KEY_UUID, UUID); - jsonObj.putOpt(KEY_FRIENDLY, friendlyName); - jsonObj.putOpt(KEY_MODEL_NAME, modelName); - jsonObj.putOpt(KEY_MODEL_NUMBER, modelNumber); - jsonObj.putOpt(KEY_PORT, port); - jsonObj.putOpt(KEY_VERSION, version); - jsonObj.putOpt(KEY_SERVICE_ID, serviceID); -// if (responseHeaders != null) { -// jsonObj.putOpt("responseHeaders", new JSONObject() {{ -// for (final String key : responseHeaders.keySet()) { -// putOpt(key, new JSONArray(){{ -// List items = responseHeaders.get(key); -// for (String item : items) -// put(item); -// }}); -// } -// }}); -// } - } catch (JSONException e) { - e.printStackTrace(); - } - - return jsonObj; - } - - public ServiceDescription clone() { - ServiceDescription service = new ServiceDescription(); - service.setPort(this.port); - - // we can ignore all these NullPointerExceptions, it's OK if those properties don't have values - try { service.setServiceID(new String(this.serviceID)); } catch (NullPointerException ex) { } - try { service.setIpAddress(new String(this.ipAddress)); } catch (NullPointerException ex) { } - try { service.setUUID(new String(this.UUID)); } catch (NullPointerException ex) { } - try { service.setVersion(new String(this.version)); } catch (NullPointerException ex) { } - try { service.setFriendlyName(new String(this.friendlyName)); } catch (NullPointerException ex) { } - try { service.setManufacturer(new String(this.manufacturer)); } catch (NullPointerException ex) { } - try { service.setModelName(new String(this.modelName)); } catch (NullPointerException ex) { } - try { service.setModelNumber(new String(this.modelNumber)); } catch (NullPointerException ex) { } - try { service.setModelDescription(new String(this.modelDescription)); } catch (NullPointerException ex) { } - try { service.setApplicationURL(new String(this.applicationURL)); } catch (NullPointerException ex) { } - try { service.setLocationXML(new String(this.locationXML)); } catch (NullPointerException ex) { } - try { service.setResponseHeaders(this.responseHeaders); } catch (NullPointerException ex) { } - try { service.setServiceList(this.serviceList); } catch (NullPointerException ex) { } - try { service.setServiceFilter(new String(this.serviceFilter)); } catch (NullPointerException ex) { } - - return service; - } -} diff --git a/src/com/connectsdk/service/config/WebOSTVServiceConfig.java b/src/com/connectsdk/service/config/WebOSTVServiceConfig.java deleted file mode 100644 index b4804258..00000000 --- a/src/com/connectsdk/service/config/WebOSTVServiceConfig.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * WebOSTVServiceConfig - * 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.config; - -import java.io.ByteArrayInputStream; -import java.io.UnsupportedEncodingException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; - -import org.json.JSONException; -import org.json.JSONObject; - -import android.util.Base64; - -public class WebOSTVServiceConfig extends ServiceConfig { - - public static final String KEY_CLIENT_KEY = "clientKey"; - public static final String KEY_CERT = "serverCertificate"; - String clientKey; - X509Certificate cert; - - public WebOSTVServiceConfig(String serviceUUID) { - super(serviceUUID); - } - - public WebOSTVServiceConfig(String serviceUUID, String clientKey) { - super(serviceUUID); - this.clientKey = clientKey; - this.cert = null; - } - - public WebOSTVServiceConfig(String serviceUUID, String clientKey, X509Certificate cert) { - super(serviceUUID); - this.clientKey = clientKey; - this.cert = cert; - } - - public WebOSTVServiceConfig(String serviceUUID, String clientKey, String cert) { - super(serviceUUID); - this.clientKey = clientKey; - this.cert = loadCertificateFromPEM(cert); - } - - public WebOSTVServiceConfig(JSONObject json) { - super(json); - - clientKey = json.optString(KEY_CLIENT_KEY); - cert = null; // TODO: loadCertificateFromPEM(json.optString(KEY_CERT)); - } - - public String getClientKey() { - return clientKey; - } - - public void setClientKey(String clientKey) { - this.clientKey = clientKey; - } - - public X509Certificate getServerCertificate() { - return cert; - } - - public void setServerCertificate(X509Certificate cert) { - this.cert = cert; - } - - public void setServerCertificate(String cert) { - this.cert = loadCertificateFromPEM(cert); - } - - public String getServerCertificateInString() { - return exportCertificateToPEM(this.cert); - } - - private String exportCertificateToPEM(X509Certificate cert) { - try { - if ( cert == null ) - return null; - return Base64.encodeToString(cert.getEncoded(), Base64.DEFAULT); - } catch (CertificateEncodingException e) { - e.printStackTrace(); - return null; - } - } - - private X509Certificate loadCertificateFromPEM(String pemString) { - CertificateFactory certFactory; - try { - certFactory = CertificateFactory.getInstance("X.509"); - ByteArrayInputStream inputStream = new ByteArrayInputStream(pemString.getBytes("US-ASCII")); - - return (X509Certificate)certFactory.generateCertificate(inputStream); - } catch (CertificateException e) { - e.printStackTrace(); - return null; - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - return null; - } - } - - @Override - public JSONObject toJSONObject() { - JSONObject jsonObj = super.toJSONObject(); - - try { - jsonObj.put(KEY_CLIENT_KEY, clientKey); - jsonObj.put(KEY_CERT, exportCertificateToPEM(cert)); - } catch (JSONException e) { - e.printStackTrace(); - } - - return jsonObj; - } - -} diff --git a/src/com/connectsdk/service/sessions/LaunchSession.java b/src/com/connectsdk/service/sessions/LaunchSession.java deleted file mode 100644 index 240b163a..00000000 --- a/src/com/connectsdk/service/sessions/LaunchSession.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * LaunchSession - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Jeffrey Glenn on 07 Mar 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.sessions; - -import java.util.List; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import com.connectsdk.core.JSONDeserializable; -import com.connectsdk.core.JSONSerializable; -import com.connectsdk.service.DeviceService; -import com.connectsdk.service.capability.listeners.ResponseListener; - -/** - * Any time anything is launched onto a first screen device, there will be important session information that needs to be tracked. LaunchSession will track this data, and must be retained to perform certain actions within the session. - */ -public class LaunchSession implements JSONSerializable, JSONDeserializable { - // @cond INTERNAL - protected String appId; - protected String appName; - protected String sessionId; - protected Object rawData; - - protected DeviceService service; - protected LaunchSessionType sessionType; - // @endcond - - /** - * LaunchSession type is used to help DeviceService's know how to close a LunchSession. - */ - public enum LaunchSessionType { - /** Unknown LaunchSession type, may be unable to close this launch session */ - Unknown, - /** LaunchSession represents a launched app */ - App, - /** LaunchSession represents an external input picker that was launched */ - ExternalInputPicker, - /** LaunchSession represents a media app */ - Media, - /** LaunchSession represents a web app */ - WebApp - } - - public LaunchSession() { - } - - /** - * Instantiates a LaunchSession object for a given app ID. - * - * @param appId System-specific, unique ID of the app - */ - public static LaunchSession launchSessionForAppId(String appId) { - LaunchSession launchSession = new LaunchSession(); - launchSession.appId = appId; - - return launchSession; - } - - // @cond INTERNAL - public static LaunchSession launchSessionFromJSONObject(JSONObject json) { - LaunchSession launchSession = new LaunchSession(); - try { - launchSession.fromJSONObject(json); - } catch (JSONException e) { - e.printStackTrace(); - } - - return launchSession; - } - // @endcond - - /** System-specific, unique ID of the app (ex. youtube.leanback.v4, 0000134, hulu) */ - public String getAppId() { - return appId; - } - - /** - * Sets the system-specific, unique ID of the app (ex. youtube.leanback.v4, 0000134, hulu) - * - * @param appId Id of the app - */ - public void setAppId(String appId) { - this.appId = appId; - } - - /** User-friendly name of the app (ex. YouTube, Browser, Hulu) */ - public String getAppName() { - return appName; - } - - /** - * Sets the user-friendly name of the app (ex. YouTube, Browser, Hulu) - * - * @param appName Name of the app - */ - public void setAppName(String appName) { - this.appName = appName; - } - - /** Unique ID for the session (only provided by certain protocols) */ - public String getSessionId() { - return sessionId; - } - - /** - * Sets the session id (only provided by certain protocols) - * - * @param sessionId Id of the current session - */ - public void setSessionId(String sessionId) { - this.sessionId = sessionId; - } - - /** DeviceService responsible for launching the session. */ - public DeviceService getService() { - return service; - } - - /** - * DeviceService responsible for launching the session. - * - * @param service Sets the DeviceService - */ - public void setService(DeviceService service) { - this.service = service; - } - - /** Raw data from the first screen device about the session. In most cases, this is a JSONObject. */ - public Object getRawData() { - return rawData; - } - - /** - * Sets the raw data from the first screen device about the session. In most cases, this is a JSONObject. - * - * @param rawData Sets the raw data - */ - public void setRawData(Object rawData) { - this.rawData = rawData; - } - - /** - * When closing a LaunchSession, the DeviceService relies on the sessionType to determine the method of closing the session. - */ - public LaunchSessionType getSessionType() { - return sessionType; - } - - /** - * Sets the LaunchSessionType of this LaunchSession. - * - * @param sessionType The type of LaunchSession - */ - public void setSessionType(LaunchSessionType sessionType) { - this.sessionType = sessionType; - } - - /** - * Close the app/media associated with the session. - * @param listener - */ - public void close (ResponseListener listener) { - service.closeLaunchSession(this, listener); - } - - // @cond INTERNAl - @Override - public JSONObject toJSONObject() throws JSONException { - JSONObject obj = new JSONObject(); - - obj.putOpt("appId", appId); - obj.putOpt("sessionId", sessionId); - obj.putOpt("name", appName); - obj.putOpt("sessionType", sessionType.name()); - if (service != null) obj.putOpt("serviceName", service.getServiceName()); - - if (rawData != null) { - if (rawData instanceof JSONObject) obj.putOpt("rawData", rawData); - if (rawData instanceof List) { - JSONArray arr = new JSONArray(); - for (Object item : (List) rawData) - arr.put(item); - obj.putOpt("rawData", arr); - } - if (rawData instanceof Object[]) { - JSONArray arr = new JSONArray(); - for (Object item : (Object[]) rawData) - arr.put(item); - obj.putOpt("rawData", arr); - } - if (rawData instanceof String) obj.putOpt("rawData", rawData); - } - - return obj; - } - - @Override - public void fromJSONObject(JSONObject obj) throws JSONException { - this.appId = obj.optString("appId"); - this.sessionId = obj.optString("sessionId"); - this.appName = obj.optString("name"); - this.sessionType = LaunchSessionType.valueOf(obj.optString("sessionType")); - this.rawData = obj.opt("rawData"); - } - - // @endcond - - /** - * Compares two LaunchSession objects. - * - * @param launchSession LaunchSession object to compare. - * - * @return true if both LaunchSession id and sessionId values are equal - */ - @Override - public boolean equals(Object launchSession) { - // TODO Auto-generated method stub - return super.equals(launchSession); - } -} diff --git a/src/com/connectsdk/service/sessions/WebAppSession.java b/src/com/connectsdk/service/sessions/WebAppSession.java deleted file mode 100644 index ef8565aa..00000000 --- a/src/com/connectsdk/service/sessions/WebAppSession.java +++ /dev/null @@ -1,411 +0,0 @@ -/* - * WebAppSession - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Jeffrey Glenn on 07 Mar 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.sessions; - -import org.json.JSONObject; - -import com.connectsdk.core.MediaInfo; -import com.connectsdk.core.Util; -import com.connectsdk.service.DeviceService; -import com.connectsdk.service.capability.MediaControl; -import com.connectsdk.service.capability.MediaPlayer; -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.command.ServiceCommandError; -import com.connectsdk.service.command.ServiceSubscription; - -/** - * ###Overview When a web app is launched on a first screen device, there are - * certain tasks that can be performed with that web app. WebAppSession serves - * as a second screen reference of the web app that was launched. It behaves - * similarly to LaunchSession, but is not nearly as static. - * - * ###In Depth On top of maintaining session information (contained in the - * launchSession property), WebAppSession provides access to a number of - * capabilities. - MediaPlayer - MediaControl - Bi-directional communication - * with web app - * - * MediaPlayer and MediaControl are provided to allow for the most common first - * screen use cases -- a media player (audio, video, & images). - * - * The Connect SDK JavaScript Bridge has been produced to provide normalized - * support for these capabilities across protocols (Chromecast, webOS, etc). - */ -public class WebAppSession implements MediaControl, MediaPlayer { - - /** Status of the web app */ - public enum WebAppStatus { - /** Web app status is unknown */ - Unknown, - /** Web app is running and in the foreground */ - Open, - /** Web app is running and in the background */ - Background, - /** Web app is in the foreground but has not started running yet */ - Foreground, - /** Web app is not running and is not in the foreground or background */ - Closed - } - - /** - * LaunchSession object containing key session information. Much of this - * information is required for web app messaging & closing the web app. - */ - public LaunchSession launchSession; - - // @cond INTERNAL - protected DeviceService service; - private WebAppSessionListener webAppListener; - - // @endcond - - /** - * Instantiates a WebAppSession object with all the information necessary to - * interact with a web app. - * - * @param launchSession - * LaunchSession containing info about the web app session - * @param service - * DeviceService that was responsible for launching this web app - */ - public WebAppSession(LaunchSession launchSession, DeviceService service) { - this.launchSession = launchSession; - this.service = service; - } - - /** - * DeviceService that was responsible for launching this web app. - */ - protected void setService(DeviceService service) { - } - - /** - * Subscribes to changes in the web app's status. - * - * @param listener - * (optional) MessageListener to be called on app status change - */ - public ServiceSubscription subscribeWebAppStatus( - MessageListener listener) { - if (listener != null) - listener.onError(ServiceCommandError.notSupported()); - - return null; - } - - /** - * Establishes a communication channel with the web app. - * - * @param connectionListener - * (optional) ResponseListener to be called on success - */ - public void connect(ResponseListener connectionListener) { - Util.postError(connectionListener, ServiceCommandError.notSupported()); - } - - /** - * Establishes a communication channel with a currently running web app. - * - * @param connectionListener - */ - public void join(ResponseListener connectionListener) { - Util.postError(connectionListener, ServiceCommandError.notSupported()); - } - - /** - * Closes any open communication channel with the web app. - */ - public void disconnectFromWebApp() { - } - - /** - * Closes the web app on the first screen device. - * - * @param listener - * (optional) ResponseListener to be called on success - */ - public void close(ResponseListener listener) { - if (listener != null) - listener.onError(ServiceCommandError.notSupported()); - } - - /** - * Sends a simple string to the web app. The Connect SDK JavaScript Bridge - * will receive this message and hand it off as a string object. - * - * @param listener - * (optional) ResponseListener to be called on success - */ - public void sendMessage(String message, ResponseListener listener) { - if (listener != null) - listener.onError(ServiceCommandError.notSupported()); - } - - /** - * Sends a JSON object to the web app. The Connect SDK JavaScript Bridge - * will receive this message and hand it off as a JavaScript object. - * - * @param success - * (optional) ResponseListener to be called on success - */ - public void sendMessage(JSONObject message, - ResponseListener listener) { - if (listener != null) { - listener.onError(ServiceCommandError.notSupported()); - } - } - - // @cond INTERNAL - @Override - public MediaControl getMediaControl() { - return null; - } - - @Override - public CapabilityPriorityLevel getMediaControlCapabilityLevel() { - return CapabilityPriorityLevel.VERY_LOW; - } - - @Override - public void play(ResponseListener listener) { - MediaControl mediaControl = null; - - if (service != null) - mediaControl = service.getAPI(MediaControl.class); - - if (mediaControl != null) - mediaControl.play(listener); - else if (listener != null) - listener.onError(ServiceCommandError.notSupported()); - } - - @Override - public void pause(ResponseListener listener) { - MediaControl mediaControl = null; - - if (service != null) - mediaControl = service.getAPI(MediaControl.class); - - if (mediaControl != null) - mediaControl.pause(listener); - else if (listener != null) - listener.onError(ServiceCommandError.notSupported()); - } - - @Override - public void stop(ResponseListener listener) { - MediaControl mediaControl = null; - - if (service != null) - mediaControl = service.getAPI(MediaControl.class); - - if (mediaControl != null) - mediaControl.stop(listener); - else if (listener != null) - listener.onError(ServiceCommandError.notSupported()); - } - - @Override - public void rewind(ResponseListener listener) { - MediaControl mediaControl = null; - - if (service != null) - mediaControl = service.getAPI(MediaControl.class); - - if (mediaControl != null) - mediaControl.rewind(listener); - else if (listener != null) - listener.onError(ServiceCommandError.notSupported()); - } - - @Override - public void fastForward(ResponseListener listener) { - MediaControl mediaControl = null; - - if (service != null) - mediaControl = service.getAPI(MediaControl.class); - - if (mediaControl != null) - mediaControl.fastForward(listener); - else if (listener != null) - listener.onError(ServiceCommandError.notSupported()); - } - - @Override - public void seek(long position, ResponseListener listener) { - MediaControl mediaControl = null; - - if (service != null) - mediaControl = service.getAPI(MediaControl.class); - - if (mediaControl != null) - mediaControl.seek(position, listener); - else if (listener != null) - listener.onError(ServiceCommandError.notSupported()); - } - - @Override - public void getDuration(DurationListener listener) { - MediaControl mediaControl = null; - - if (service != null) - mediaControl = service.getAPI(MediaControl.class); - - if (mediaControl != null) - mediaControl.getDuration(listener); - else if (listener != null) - listener.onError(ServiceCommandError.notSupported()); - } - - @Override - public void getPosition(PositionListener listener) { - MediaControl mediaControl = null; - - if (service != null) - mediaControl = service.getAPI(MediaControl.class); - - if (mediaControl != null) - mediaControl.getPosition(listener); - else if (listener != null) - listener.onError(ServiceCommandError.notSupported()); - } - - @Override - public void getPlayState(PlayStateListener listener) { - MediaControl mediaControl = null; - - if (service != null) - mediaControl = service.getAPI(MediaControl.class); - - if (mediaControl != null) - mediaControl.getPlayState(listener); - else if (listener != null) - listener.onError(ServiceCommandError.notSupported()); - } - - @Override - public ServiceSubscription subscribePlayState( - PlayStateListener listener) { - MediaControl mediaControl = null; - - if (service != null) - mediaControl = service.getAPI(MediaControl.class); - - if (mediaControl != null) - return mediaControl.subscribePlayState(listener); - else if (listener != null) - listener.onError(ServiceCommandError.notSupported()); - - return null; - } - - @Override - public void closeMedia(LaunchSession launchSession, - ResponseListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void displayImage(String url, String mimeType, String title, - String description, String iconSrc, - MediaPlayer.LaunchListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void displayImage(MediaInfo mediaInfo, MediaPlayer.LaunchListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void playMedia(String url, String mimeType, String title, String description, String iconSrc, boolean shouldLoop, MediaPlayer.LaunchListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } - - @Override - public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, - MediaPlayer.LaunchListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - - } - - @Override - public MediaPlayer getMediaPlayer() { - return null; - } - - @Override - public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { - return CapabilityPriorityLevel.VERY_LOW; - } - - // @endcond - - /** - * When messages are received from a web app, they are parsed into the - * appropriate object type (string vs JSON/NSDictionary) and routed to the - * WebAppSessionListener. - */ - public WebAppSessionListener getWebAppSessionListener() { - return webAppListener; - } - - /** - * When messages are received from a web app, they are parsed into the - * appropriate object type (string vs JSON/NSDictionary) and routed to the - * WebAppSessionListener. - * - * @param listener - * WebAppSessionListener to be called when messages are received - * from the web app - */ - public void setWebAppSessionListener(WebAppSessionListener listener) { - webAppListener = listener; - } - - /** - * Success block that is called upon successfully launch of a web app. - * - * Passes a WebAppSession Object containing important information about the - * web app's session. This object is required to perform many functions with - * the web app, including app-to-app communication, media playback, closing, - * etc. - */ - public static interface LaunchListener extends - ResponseListener { - } - - /** - * Success block that is called upon successfully getting a web app's - * status. - * - * Passes a WebAppStatus of the current running & foreground status of the - * web app - */ - public static interface StatusListener extends - ResponseListener { - } - - // @cond INTERNAL - public static interface MessageListener extends ResponseListener { - abstract public void onMessage(Object message); - } - // @endcond -} diff --git a/src/com/connectsdk/service/sessions/WebAppSessionListener.java b/src/com/connectsdk/service/sessions/WebAppSessionListener.java deleted file mode 100644 index 48d1c7b7..00000000 --- a/src/com/connectsdk/service/sessions/WebAppSessionListener.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * WebAppSessionListener - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Jeffrey Glenn on 07 Mar 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.sessions; - -public interface WebAppSessionListener { - - /** - * This method is called when a message is received from a web app. - * - * @param webAppSession WebAppSession that corresponds to the web app that sent the message - * @param message Object from the web app, either an String or a JSONObject - */ - public void onReceiveMessage(WebAppSession webAppSession, Object message); - - /** - * This method is called when a web app's communication channel (WebSocket, etc) has become disconnected. - * - * @param webAppSession WebAppSession that became disconnected - */ - public void onWebAppSessionDisconnect(WebAppSession webAppSession); -} diff --git a/src/com/connectsdk/service/sessions/WebOSWebAppSession.java b/src/com/connectsdk/service/sessions/WebOSWebAppSession.java deleted file mode 100644 index 3859fe42..00000000 --- a/src/com/connectsdk/service/sessions/WebOSWebAppSession.java +++ /dev/null @@ -1,1027 +0,0 @@ -/* - * WebOSWebAppSession - * Connect SDK - * - * Copyright (c) 2014 LG Electronics. - * Created by Jeffrey Glenn on 07 Mar 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.sessions; - -import java.util.Enumeration; -import java.util.Locale; -import java.util.concurrent.ConcurrentHashMap; - -import org.json.JSONException; -import org.json.JSONObject; - -import com.connectsdk.core.MediaInfo; -import com.connectsdk.core.Util; -import com.connectsdk.service.DeviceService; -import com.connectsdk.service.WebOSTVService; -import com.connectsdk.service.capability.MediaControl; -import com.connectsdk.service.capability.MediaPlayer; -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.sessions.LaunchSession.LaunchSessionType; -import com.connectsdk.service.webos.WebOSTVServiceSocketClient; -import com.connectsdk.service.webos.WebOSTVServiceSocketClient.WebOSTVServiceSocketClientListener; - -public class WebOSWebAppSession extends WebAppSession { - private static final String namespaceKey = "connectsdk."; - protected WebOSTVService service; - - ResponseListener>> mConnectionListener; - - public WebOSTVServiceSocketClient socket; - public URLServiceSubscription> appToAppSubscription; - - private ServiceSubscription mPlayStateSubscription; - private ServiceSubscription mMessageSubscription; - private ConcurrentHashMap> mActiveCommands; - - String mFullAppId; - - private int UID; - private boolean connected; - - public WebOSWebAppSession(LaunchSession launchSession, DeviceService service) { - super(launchSession, service); - - UID = 0; - mActiveCommands = new ConcurrentHashMap>(0, - 0.75f, 10); - connected = false; - - this.service = (WebOSTVService) service; - } - - private int getNextId() { - return ++UID; - } - - public Boolean isConnected() { - return connected; - } - - public void setConnected(Boolean connected) { - this.connected = connected; - } - - public void handleMediaEvent(JSONObject payload) { - String type = ""; - - type = payload.optString("type"); - if (type.length() == 0) - return; - - if (type.equals("playState")) { - if (mPlayStateSubscription == null) - return; - - String playStateString = payload.optString(type); - if (playStateString.length() == 0) - return; - - final MediaControl.PlayStateStatus playState = parsePlayState(playStateString); - - for (PlayStateListener listener : mPlayStateSubscription - .getListeners()) { - Util.postSuccess(listener, playState); - } - } - } - - public String getFullAppId() { - if (mFullAppId == null) { - if (launchSession.getSessionType() != LaunchSessionType.WebApp) - mFullAppId = launchSession.getAppId(); - else { - Enumeration enumeration = service.getWebAppIdMappings() - .keys(); - - while (enumeration.hasMoreElements()) { - String mappedFullAppId = enumeration.nextElement(); - String mappedAppId = service.getWebAppIdMappings().get( - mappedFullAppId); - - if (mappedAppId.equalsIgnoreCase(launchSession.getAppId())) { - mFullAppId = mappedAppId; - break; - } - } - } - } - - if (mFullAppId == null) - return launchSession.getAppId(); - else - return mFullAppId; - } - - public void setFullAppId(String fullAppId) { - mFullAppId = fullAppId; - } - - private WebOSTVServiceSocketClientListener mSocketListener = new WebOSTVServiceSocketClientListener() { - - @Override - public void onRegistrationFailed(ServiceCommandError error) { - } - - @Override - public Boolean onReceiveMessage(JSONObject payload) { - String type = payload.optString("type"); - - if ("p2p".equals(type)) { - String fromAppId = null; - - fromAppId = payload.optString("from"); - - if (!fromAppId.equalsIgnoreCase(getFullAppId())) - return false; - - Object message = payload.opt("payload"); - - if (message instanceof JSONObject) { - JSONObject messageJSON = (JSONObject) message; - - String contentType = messageJSON.optString("contentType"); - Integer contentTypeIndex = contentType - .indexOf("connectsdk."); - - if (contentType != null && contentTypeIndex >= 0) { - String payloadKey = contentType.split("connectsdk.")[1]; - - if (payloadKey == null || payloadKey.length() == 0) - return false; - - JSONObject messagePayload = messageJSON - .optJSONObject(payloadKey); - - if (messagePayload == null) - return false; - - if (payloadKey.equalsIgnoreCase("mediaEvent")) - handleMediaEvent(messagePayload); - else if (payloadKey - .equalsIgnoreCase("mediaCommandResponse")) - handleMediaCommandResponse(messagePayload); - } else { - handleMessage(messageJSON); - } - } else if (message instanceof String) { - handleMessage(message); - } - - return false; - } - - return true; - } - - @Override - public void onFailWithError(ServiceCommandError error) { - connected = false; - appToAppSubscription = null; - - if (mConnectionListener != null) { - if (error == null) - error = new ServiceCommandError(0, - "Unknown error connecting to web socket", null); - - mConnectionListener.onError(error); - } - - mConnectionListener = null; - } - - @Override - public void onConnect() { - if (mConnectionListener != null) - mConnectionListener.onSuccess(null); - - mConnectionListener = null; - } - - @Override - public void onCloseWithError(ServiceCommandError error) { - connected = false; - appToAppSubscription = null; - - if (mConnectionListener != null) { - if (error != null) - mConnectionListener.onError(error); - else { - if (getWebAppSessionListener() != null) - getWebAppSessionListener().onWebAppSessionDisconnect( - WebOSWebAppSession.this); - } - } - - mConnectionListener = null; - } - - @Override - public void onBeforeRegister() { - } - }; - - @SuppressWarnings("unchecked") - public void handleMediaCommandResponse(final JSONObject payload) { - String requetID = payload.optString("requestId"); - if (requetID.length() == 0) - return; - - final ServiceCommand> command = (ServiceCommand>) mActiveCommands - .get(requetID); - - if (command == null) - return; - - String mError = payload.optString("error"); - - if (mError.length() != 0) { - Util.postError(command.getResponseListener(), - new ServiceCommandError(0, mError, null)); - } else { - Util.postSuccess(command.getResponseListener(), payload); - } - - mActiveCommands.remove(requetID); - } - - public void handleMessage(final Object message) { - Util.runOnUI(new Runnable() { - - @Override - public void run() { - if (getWebAppSessionListener() != null) - getWebAppSessionListener().onReceiveMessage( - WebOSWebAppSession.this, message); - } - }); - - } - - public PlayStateStatus parsePlayState(String playStateString) { - if (playStateString.equals("playing")) - return PlayStateStatus.Playing; - else if (playStateString.equals("paused")) - return PlayStateStatus.Paused; - else if (playStateString.equals("idle")) - return PlayStateStatus.Idle; - else if (playStateString.equals("buffering")) - return PlayStateStatus.Buffering; - else if (playStateString.equals("finished")) - return PlayStateStatus.Finished; - - return PlayStateStatus.Unknown; - } - - public void connect(ResponseListener connectionListener) { - connect(false, connectionListener); - } - - @Override - public void join(ResponseListener connectionListener) { - connect(true, connectionListener); - } - - private void connect(final Boolean joinOnly, - final ResponseListener connectionListener) { - if (socket != null - && socket.getState() == WebOSTVServiceSocketClient.State.CONNECTING) { - if (connectionListener != null) - ; - connectionListener - .onError(new ServiceCommandError( - 0, - "You have a connection request pending, please wait until it has finished", - null)); - - return; - } - - if (isConnected()) { - if (connectionListener != null) - connectionListener.onSuccess(null); - - return; - } - - mConnectionListener = new ResponseListener>>() { - - @Override - public void onError(ServiceCommandError error) { - if (socket != null) - disconnectFromWebApp(); - - if (connectionListener != null) { - if (error == null) - error = new ServiceCommandError(0, - "Unknown error connecting to web app", null); - - connectionListener.onError(error); - } - } - - @Override - public void onSuccess( - ServiceCommand> object) { - ResponseListener finalConnectionListener = new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - disconnectFromWebApp(); - - if (connectionListener != null) - connectionListener.onError(error); - } - - @Override - public void onSuccess(Object object) { - connected = true; - - if (connectionListener != null) - connectionListener.onSuccess(object); - } - }; - - service.connectToWebApp(WebOSWebAppSession.this, joinOnly, - finalConnectionListener); - } - }; - - if (socket != null) { - if (socket.isConnected()) - mConnectionListener.onSuccess(null); - else - socket.connect(); - } else { - socket = new WebOSTVServiceSocketClient(service, - WebOSTVServiceSocketClient.getURI(service)); - socket.setListener(mSocketListener); - socket.connect(); - } - } - - public void disconnectFromWebApp() { - connected = false; - mConnectionListener = null; - - if (appToAppSubscription != null) { - appToAppSubscription.removeListeners(); - appToAppSubscription = null; - } - - if (socket != null) { - socket.setListener(null); - socket.disconnect(); - socket = null; - } - } - - @Override - public void sendMessage(final String message, - final ResponseListener listener) { - if (message == null || message.length() == 0) { - if (listener != null) - listener.onError(new ServiceCommandError(0, - "Cannot send an Empty Message", null)); - - return; - } - - sendP2PMessage(message, listener); - } - - @Override - public void sendMessage(final JSONObject message, - final ResponseListener listener) { - if (message == null || message.length() == 0) { - Util.postError(listener, new ServiceCommandError(0, - "Cannot send an Empty Message", null)); - - return; - } - - sendP2PMessage(message, listener); - } - - private void sendP2PMessage(final Object message, - final ResponseListener listener) { - JSONObject _payload = new JSONObject(); - - try { - _payload.put("type", "p2p"); - _payload.put("to", getFullAppId()); - _payload.put("payload", message); - } catch (JSONException ex) { - // do nothing - } - - final JSONObject payload = _payload; - - if (isConnected()) { - socket.sendMessage(payload, null); - - if (listener != null) - listener.onSuccess(null); - } else { - ResponseListener connectListener = new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) - listener.onError(error); - } - - @Override - public void onSuccess(Object object) { - sendP2PMessage(message, listener); - } - }; - - connect(connectListener); - } - } - - @Override - public void close(ResponseListener listener) { - mActiveCommands.clear(); - - if (mPlayStateSubscription != null) { - mPlayStateSubscription.unsubscribe(); - mPlayStateSubscription = null; - } - - if (mMessageSubscription != null) { - mMessageSubscription.unsubscribe(); - mMessageSubscription = null; - } - - service.getWebAppLauncher().closeWebApp(launchSession, listener); - } - - @Override - public void seek(final long position, ResponseListener listener) { - if (position < 0) { - if (listener != null) - listener.onError(new ServiceCommandError(0, - "Must pass a valid positive value", null)); - - return; - } - - int requestIdNumber = getNextId(); - final String requestId = String.format(Locale.US, "req%d", - requestIdNumber); - - JSONObject message = null; - try { - message = new JSONObject() { - { - put("contentType", namespaceKey + "mediaCommand"); - put("mediaCommand", new JSONObject() { - { - put("type", "seek"); - put("position", position / 1000); - put("requestId", requestId); - } - }); - } - }; - } catch (JSONException e) { - if (listener != null) - listener.onError(new ServiceCommandError(0, "JSON Parse error", - null)); - } - - ServiceCommand> command = new ServiceCommand>( - null, null, null, listener); - - mActiveCommands.put(requestId, command); - - sendMessage(message, listener); - } - - @Override - public void getPosition(final PositionListener listener) { - int requestIdNumber = getNextId(); - final String requestId = String.format(Locale.US, "req%d", - requestIdNumber); - - JSONObject message = null; - try { - message = new JSONObject() { - { - put("contentType", namespaceKey + "mediaCommand"); - put("mediaCommand", new JSONObject() { - { - put("type", "getPosition"); - put("requestId", requestId); - } - }); - } - }; - } catch (JSONException e) { - Util.postError(listener, new ServiceCommandError(0, - "JSON Parse error", null)); - } - - ServiceCommand> command = new ServiceCommand>( - null, null, null, new ResponseListener() { - - @Override - public void onSuccess(Object response) { - try { - long position = ((JSONObject) response) - .getLong("position"); - - if (listener != null) - listener.onSuccess(position * 1000); - } catch (JSONException e) { - this.onError(new ServiceCommandError(0, - "JSON Parse error", null)); - } - } - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) - listener.onError(error); - } - }); - - mActiveCommands.put(requestId, command); - - sendMessage(message, new ResponseListener() { - - @Override - public void onSuccess(Object response) { - } - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) - listener.onError(error); - } - }); - } - - @Override - public void getDuration(final DurationListener listener) { - int requestIdNumber = getNextId(); - final String requestId = String.format(Locale.US, "req%d", - requestIdNumber); - - JSONObject message = null; - try { - message = new JSONObject() { - { - put("contentType", namespaceKey + "mediaCommand"); - put("mediaCommand", new JSONObject() { - { - put("type", "getDuration"); - put("requestId", requestId); - } - }); - } - }; - } catch (JSONException e) { - if (listener != null) - listener.onError(new ServiceCommandError(0, "JSON Parse error", - null)); - } - - ServiceCommand> command = new ServiceCommand>( - null, null, null, new ResponseListener() { - - @Override - public void onSuccess(Object response) { - try { - long position = ((JSONObject) response) - .getLong("duration"); - - if (listener != null) - listener.onSuccess(position * 1000); - } catch (JSONException e) { - this.onError(new ServiceCommandError(0, - "JSON Parse error", null)); - } - } - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) - listener.onError(error); - } - }); - - mActiveCommands.put(requestId, command); - - sendMessage(message, new ResponseListener() { - - @Override - public void onSuccess(Object response) { - } - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) - listener.onError(error); - } - }); - } - - @Override - public void getPlayState(final PlayStateListener listener) { - int requestIdNumber = getNextId(); - final String requestId = String.format(Locale.US, "req%d", - requestIdNumber); - - JSONObject message = null; - try { - message = new JSONObject() { - { - put("contentType", namespaceKey + "mediaCommand"); - put("mediaCommand", new JSONObject() { - { - put("type", "getPlayState"); - put("requestId", requestId); - } - }); - } - }; - } catch (JSONException e) { - if (listener != null) - listener.onError(new ServiceCommandError(0, "JSON Parse error", - null)); - } - - ServiceCommand> command = new ServiceCommand>( - null, null, null, new ResponseListener() { - - @Override - public void onSuccess(Object response) { - try { - String playStateString = ((JSONObject) response) - .getString("playState"); - PlayStateStatus playState = parsePlayState(playStateString); - - if (listener != null) - listener.onSuccess(playState); - } catch (JSONException e) { - this.onError(new ServiceCommandError(0, - "JSON Parse error", null)); - } - } - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) - listener.onError(error); - } - }); - - mActiveCommands.put(requestId, command); - - sendMessage(message, new ResponseListener() { - - @Override - public void onSuccess(Object response) { - } - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) - listener.onError(error); - } - }); - } - - @Override - public ServiceSubscription subscribePlayState( - final PlayStateListener listener) { - if (mPlayStateSubscription == null) - mPlayStateSubscription = new URLServiceSubscription( - null, null, null, null); - - if (!connected) { - connect(new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object response) { - } - }); - } - - if (!mPlayStateSubscription.getListeners().contains(listener)) - mPlayStateSubscription.addListener(listener); - - return mPlayStateSubscription; - } - - /***************** - * Media Control * - *****************/ - @Override - public MediaControl getMediaControl() { - return this; - } - - @Override - public CapabilityPriorityLevel getMediaControlCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - /**************** - * Media Player * - ****************/ - @Override - public MediaPlayer getMediaPlayer() { - return this; - } - - @Override - public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { - return CapabilityPriorityLevel.HIGH; - } - - @Override - public void displayImage(final String url, final String mimeType, - final String title, final String description, final String iconSrc, - final MediaPlayer.LaunchListener listener) { - int requestIdNumber = getNextId(); - final String requestId = String.format(Locale.US, "req%d", - requestIdNumber); - - JSONObject message = null; - try { - message = new JSONObject() { - { - putOpt("contentType", namespaceKey + "mediaCommand"); - putOpt("mediaCommand", new JSONObject() { - { - putOpt("type", "displayImage"); - putOpt("mediaURL", url); - putOpt("iconURL", iconSrc); - putOpt("title", title); - putOpt("description", description); - putOpt("mimeType", mimeType); - putOpt("requestId", requestId); - } - }); - } - }; - } catch (JSONException e) { - e.printStackTrace(); - // Should never hit this - } - - ResponseListener response = new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - Util.postSuccess(listener, new MediaLaunchObject(launchSession, - getMediaControl())); - } - }; - - ServiceCommand> command = new ServiceCommand>( - socket, null, null, response); - - mActiveCommands.put(requestId, command); - - sendP2PMessage(message, new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - } - }); - } - - @Override - public void displayImage(final MediaInfo mediaInfo, - final MediaPlayer.LaunchListener listener) { - int requestIdNumber = getNextId(); - final String requestId = String.format(Locale.US, "req%d", - requestIdNumber); - - JSONObject message = null; - try { - message = new JSONObject() { - { - putOpt("contentType", namespaceKey + "mediaCommand"); - putOpt("mediaCommand", new JSONObject() { - { - putOpt("type", "displayImage"); - putOpt("mediaURL", mediaInfo.getUrl()); - putOpt("iconURL", mediaInfo.getImages().get(0) - .getUrl()); - putOpt("title", mediaInfo.getTitle()); - putOpt("description", mediaInfo.getDescription()); - putOpt("mimeType", mediaInfo.getMimeType()); - putOpt("requestId", requestId); - } - }); - } - }; - } catch (JSONException e) { - e.printStackTrace(); - // Should never hit this - } - - ResponseListener response = new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - Util.postSuccess(listener, new MediaLaunchObject(launchSession, - getMediaControl())); - } - }; - - ServiceCommand> command = new ServiceCommand>( - socket, null, null, response); - - mActiveCommands.put(requestId, command); - - sendP2PMessage(message, new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - } - }); - } - - @Override - public void playMedia(final String url, final String mimeType, - final String title, final String description, final String iconSrc, - final boolean shouldLoop, final MediaPlayer.LaunchListener listener) { - int requestIdNumber = getNextId(); - final String requestId = String.format(Locale.US, "req%d", - requestIdNumber); - - JSONObject message = null; - try { - message = new JSONObject() { - { - putOpt("contentType", namespaceKey + "mediaCommand"); - putOpt("mediaCommand", new JSONObject() { - { - putOpt("type", "playMedia"); - putOpt("mediaURL", url); - putOpt("iconURL", iconSrc); - putOpt("title", title); - putOpt("description", description); - putOpt("mimeType", mimeType); - putOpt("shouldLoop", shouldLoop); - putOpt("requestId", requestId); - } - }); - } - }; - } catch (JSONException e) { - e.printStackTrace(); - // Should never hit this - } - - ResponseListener response = new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - Util.postSuccess(listener, new MediaLaunchObject(launchSession, - getMediaControl())); - } - }; - - ServiceCommand> command = new ServiceCommand>( - null, null, null, response); - - mActiveCommands.put(requestId, command); - - sendMessage(message, new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - } - }); - } - - @Override - public void playMedia(final MediaInfo mediaInfo, - final boolean shouldLoop, final MediaPlayer.LaunchListener listener) { - int requestIdNumber = getNextId(); - final String requestId = String.format(Locale.US, "req%d", - requestIdNumber); - - JSONObject message = null; - try { - message = new JSONObject() { - { - putOpt("contentType", namespaceKey + "mediaCommand"); - putOpt("mediaCommand", new JSONObject() { - { - putOpt("type", "playMedia"); - putOpt("mediaURL", mediaInfo.getUrl()); - putOpt("iconURL", mediaInfo.getImages().get(0) - .getUrl() == null ? NULL : mediaInfo.getImages().get(0) - .getUrl()) ; - putOpt("poster", mediaInfo.getImages().get(1).getUrl() == null ? NULL : mediaInfo.getImages().get(1).getUrl()); - putOpt("title", mediaInfo.getTitle()); - putOpt("description", mediaInfo.getDescription()); - putOpt("mimeType", mediaInfo.getMimeType()); - putOpt("shouldLoop", shouldLoop); - putOpt("requestId", requestId); - } - }); - } - }; - } catch (JSONException e) { - e.printStackTrace(); - // Should never hit this - } - - ResponseListener response = new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - Util.postSuccess(listener, new MediaLaunchObject(launchSession, - getMediaControl())); - } - }; - - ServiceCommand> command = new ServiceCommand>( - null, null, null, response); - - mActiveCommands.put(requestId, command); - - sendMessage(message, new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - } - }); - } -} diff --git a/src/com/connectsdk/service/webos/WebOSTVKeyboardInput.java b/src/com/connectsdk/service/webos/WebOSTVKeyboardInput.java deleted file mode 100644 index 47f99d2c..00000000 --- a/src/com/connectsdk/service/webos/WebOSTVKeyboardInput.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * WebOSTVKeyboardInput - * 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.webos; - -import java.util.ArrayList; -import java.util.List; - -import org.json.JSONException; -import org.json.JSONObject; - -import com.connectsdk.core.TextInputStatusInfo; -import com.connectsdk.core.Util; -import com.connectsdk.service.WebOSTVService; -import com.connectsdk.service.capability.TextInputControl.TextInputStatusListener; -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.command.ServiceCommandError; -import com.connectsdk.service.command.ServiceCommand; -import com.connectsdk.service.command.URLServiceSubscription; - -public class WebOSTVKeyboardInput { - - WebOSTVService service; - boolean waiting; - List toSend; - - static String KEYBOARD_INPUT = "ssap://com.webos.service.ime/registerRemoteKeyboard"; - static String ENTER = "ENTER"; - static String DELETE = "DELETE"; - - boolean canReplaceText = false; - - public WebOSTVKeyboardInput(WebOSTVService service) { - this.service = service; - waiting = false; - - toSend = new ArrayList(); - } - - public void addToQueue(String input) { - toSend.add(input); - if ( !waiting ) { - sendData(); - } - } - - public void sendEnter() { - toSend.add(ENTER); - if ( !waiting ) { - sendData(); - } - } - - public void sendDel() { - if ( toSend.size() == 0 ) { - toSend.add(DELETE); - if (!waiting) { - sendData(); - } - } - else { - toSend.remove(toSend.size()-1); - } - } - - private void sendData() { - waiting = true; - - String uri; - String typeTest = toSend.get(0); - - JSONObject payload = new JSONObject(); - - if ( typeTest.equals(ENTER) ) { - toSend.remove(0); - uri = "ssap://com.webos.service.ime/sendEnterKey"; - } - else if ( typeTest.equals(DELETE) ) { - uri = "ssap://com.webos.service.ime/deleteCharacters"; - - int count = 0; - while ( toSend.size() > 0 && toSend.get(0).equals(DELETE) ) { - toSend.remove(0); - count++; - } - - try { - payload.put("count", count); - } catch (JSONException e) { - e.printStackTrace(); - } - } - else { - uri = "ssap://com.webos.service.ime/insertText"; - StringBuilder sb = new StringBuilder(); - - while ( toSend.size() > 0 && !(toSend.get(0).equals(DELETE) || toSend.get(0).equals(ENTER)) ) { - String text = toSend.get(0); - sb.append(text); - toSend.remove(0); - } - - try { - payload.put("text", sb.toString()); - payload.put("replace", 0); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - waiting = false; - if ( toSend.size() > 0 ) - sendData(); - } - - @Override - public void onError(ServiceCommandError error) { - waiting = false; - if ( toSend.size() > 0 ) - sendData(); - } - }; - - ServiceCommand> request = new ServiceCommand>(service, uri, payload, true, responseListener); - request.send(); - } - - public URLServiceSubscription connect(final TextInputStatusListener listener) { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - JSONObject jsonObj = (JSONObject)response; - - TextInputStatusInfo keyboard = parseRawKeyboardData(jsonObj); - - Util.postSuccess(listener, keyboard); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - URLServiceSubscription subscription = new URLServiceSubscription(service, KEYBOARD_INPUT, null, true, responseListener); - subscription.send(); - - return subscription; - } - - private TextInputStatusInfo parseRawKeyboardData(JSONObject rawData) { - boolean focused = false; - String contentType = null; - boolean predictionEnabled = false; - boolean correctionEnabled = false; - boolean autoCapitalization = false; - boolean hiddenText = false; - boolean focusChanged = false; - - TextInputStatusInfo keyboard = new TextInputStatusInfo(); - keyboard.setRawData(rawData); - - try { - if (rawData.has("currentWidget")) { - JSONObject currentWidget = (JSONObject) rawData.get("currentWidget"); - focused = (Boolean) currentWidget.get("focus"); - - if ( currentWidget.has("contentType") ) { - contentType = (String) currentWidget.get("contentType"); - } - if ( currentWidget.has("predictionEnabled") ) { - predictionEnabled = (Boolean) currentWidget.get("predictionEnabled"); - } - if ( currentWidget.has("correctionEnabled") ) { - correctionEnabled = (Boolean) currentWidget.get("correctionEnabled"); - } - if ( currentWidget.has("autoCapitalization") ) { - autoCapitalization = (Boolean) currentWidget.get("autoCapitalization"); - } - if ( currentWidget.has("hiddenText") ) { - hiddenText = (Boolean) currentWidget.get("hiddenText"); - } - } - if ( rawData.has("focusChanged") ) - focusChanged = (Boolean) rawData.get("focusChanged"); - - } catch (JSONException e) { - e.printStackTrace(); - } - - keyboard.setFocused(focused); - keyboard.setContentType(contentType); - keyboard.setPredictionEnabled(predictionEnabled); - keyboard.setCorrectionEnabled(correctionEnabled); - keyboard.setAutoCapitalization(autoCapitalization); - keyboard.setHiddenText(hiddenText); - keyboard.setFocusChanged(focusChanged); - - return keyboard; - } - -// public void disconnect() { -// subscription.unsubscribe(); -// } -} diff --git a/src/com/connectsdk/service/webos/WebOSTVMouseSocketConnection.java b/src/com/connectsdk/service/webos/WebOSTVMouseSocketConnection.java deleted file mode 100644 index a9a6e0c7..00000000 --- a/src/com/connectsdk/service/webos/WebOSTVMouseSocketConnection.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * WebOSTVMouseSocketConnection - * 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.webos; - -import java.net.URI; -import java.net.URISyntaxException; - -import org.java_websocket.WebSocket.READYSTATE; -import org.java_websocket.client.WebSocketClient; -import org.java_websocket.handshake.ServerHandshake; - -import android.util.Log; - -public class WebOSTVMouseSocketConnection { - WebSocketClient ws; - String socketPath; - - public enum ButtonType { - HOME, - BACK, - UP, - DOWN, - LEFT, - RIGHT, - } - - public WebOSTVMouseSocketConnection(String socketPath) { - Log.d("PointerAndKeyboardFragment", "got socketPath: " + socketPath); - - if (socketPath.startsWith("wss:")) { - this.socketPath = socketPath.replace("wss:", "ws:").replace(":3001/", ":3000/"); // downgrade to plaintext - Log.d("PointerAndKeyboardFragment", "downgraded socketPath: " + this.socketPath); - } - else - this.socketPath = socketPath; - - try { - URI uri = new URI(this.socketPath); - connectPointer(uri); - } catch (URISyntaxException e) { - e.printStackTrace(); - } - } - - public void connectPointer(URI uri) { - if (ws != null) { - ws.close(); - ws = null; - } - - ws = new WebSocketClient(uri) { - - @Override - public void onOpen(ServerHandshake arg0) { - Log.d("PointerAndKeyboardFragment", "connected to " + uri.toString()); - } - - @Override - public void onMessage(String arg0) { - } - - @Override - public void onError(Exception arg0) { - } - - @Override - public void onClose(int arg0, String arg1, boolean arg2) { - } - }; - - ws.connect(); - } - - public void disconnect() { - if ( ws != null ) { - ws.close(); - ws = null; - } - } - - public boolean isConnected() { - if ( ws == null ) - System.out.println("ws is null"); - else if (ws.getReadyState() != READYSTATE.OPEN) { - System.out.println("ws state is not ready"); - } - return (ws != null) && (ws.getReadyState() == READYSTATE.OPEN); - } - - public void click() { - if ( isConnected() ) { - StringBuilder sb = new StringBuilder(); - - sb.append("type:click\n"); - sb.append("\n"); - - ws.send(sb.toString()); - } - } - - public void button(ButtonType type) { - String keyName; - switch (type) { - case HOME: - keyName = "HOME"; - break; - case BACK: - keyName = "BACK"; - break; - case UP: - keyName = "UP"; - break; - case DOWN: - keyName = "DOWN"; - break; - case LEFT: - keyName = "LEFT"; - break; - case RIGHT: - keyName = "RIGHT"; - break; - - default: - keyName = "NONE"; - break; - } - - button(keyName); - } - - public void button(String keyName) { - if ( keyName != null ) { - if ( keyName.equals("HOME") - || keyName.equals("BACK") - || keyName.equals("UP") - || keyName.equals("DOWN") - || keyName.equals("LEFT") - || keyName.equals("RIGHT") - || keyName.equals("3D_MODE") ) { - - sendSpecialKey(keyName); - } - } - } - - private void sendSpecialKey(String keyName) { - if ( isConnected() ) { - StringBuilder sb = new StringBuilder(); - - sb.append("type:button\n"); - sb.append("name:" + keyName + "\n"); - sb.append("\n"); - - ws.send(sb.toString()); - } - } - - public void move(double dx, double dy) { - if ( isConnected() ) { - StringBuilder sb = new StringBuilder(); - - sb.append("type:move\n"); - sb.append("dx:" + dx + "\n"); - sb.append("dy:" + dy + "\n"); - sb.append("down:0\n"); - sb.append("\n"); - - ws.send(sb.toString()); - } - } - - public void move(double dx, double dy, boolean drag) { - if ( isConnected() ) { - StringBuilder sb = new StringBuilder(); - - sb.append("type:move\n"); - sb.append("dx:" + dx + "\n"); - sb.append("dy:" + dy + "\n"); - sb.append("down:" + (drag ? 1 : 0) + "\n"); - sb.append("\n"); - - ws.send(sb.toString()); - } - } - - public void scroll(double dx, double dy) { - if ( isConnected() ) { - StringBuilder sb = new StringBuilder(); - - sb.append("type:scroll\n"); - sb.append("dx:" + dx + "\n"); - sb.append("dy:" + dy + "\n"); - sb.append("\n"); - - ws.send(sb.toString()); - } - } -} diff --git a/src/com/connectsdk/service/webos/WebOSTVServiceSocketClient.java b/src/com/connectsdk/service/webos/WebOSTVServiceSocketClient.java deleted file mode 100644 index b2e8f33e..00000000 --- a/src/com/connectsdk/service/webos/WebOSTVServiceSocketClient.java +++ /dev/null @@ -1,793 +0,0 @@ -package com.connectsdk.service.webos; - -import java.io.ByteArrayInputStream; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.security.KeyException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Locale; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.X509TrustManager; - -import org.java_websocket.WebSocket; -import org.java_websocket.client.DefaultSSLWebSocketClientFactory; -import org.java_websocket.client.WebSocketClient; -import org.java_websocket.handshake.ServerHandshake; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.os.Build; -import android.util.Base64; -import android.util.Log; -import android.util.SparseArray; -import android.view.Display; -import android.view.WindowManager; - -import com.connectsdk.core.Util; -import com.connectsdk.discovery.DiscoveryManager; -import com.connectsdk.service.WebOSTVService; -import com.connectsdk.service.capability.listeners.ResponseListener; -import com.connectsdk.service.command.ServiceCommand; -import com.connectsdk.service.command.ServiceCommandError; -import com.connectsdk.service.command.URLServiceSubscription; -import com.connectsdk.service.command.ServiceCommand.ServiceCommandProcessor; -import com.connectsdk.service.config.WebOSTVServiceConfig; - -@SuppressLint("DefaultLocale") -public class WebOSTVServiceSocketClient extends WebSocketClient implements ServiceCommandProcessor { - - private static final String TAG = "Connect SDK"; - - public enum State { - NONE, - INITIAL, - CONNECTING, - REGISTERING, - REGISTERED, - DISCONNECTING - }; - - WebOSTVServiceSocketClientListener mListener; - WebOSTVService mService; - - int nextRequestId = 1; - - TrustManager customTrustManager; - State state = State.INITIAL; - - JSONObject manifest; - - static final int PORT = 3001; - - // Queue of commands that should be sent once register is complete - LinkedHashSet>> commandQueue = new LinkedHashSet>>(); - - public SparseArray> requests = new SparseArray>(); - - boolean mConnectSucceeded = false; - Boolean mConnected; - - public WebOSTVServiceSocketClient(WebOSTVService service, URI uri) { - super(uri); - - this.mService = service; - state = State.INITIAL; - - setDefaultManifest(); - } - - public static URI getURI(WebOSTVService service) { - String uriString = "wss://" + service.getServiceDescription().getIpAddress() + ":" + PORT; - URI uri = null; - - try { - uri = new URI(uriString); - } catch (URISyntaxException e) { - e.printStackTrace(); - } - - return uri; - } - - public WebOSTVServiceSocketClientListener getListener() { - return mListener; - } - - public void setListener(WebOSTVServiceSocketClientListener mListener) { - this.mListener = mListener; - } - - public State getState() { - return state; - } - - public void connect() { - synchronized (this) { - if (state != State.INITIAL) { - Log.w(TAG, "already connecting; not trying to connect again: " + state); - return; // don't try to connect again while connected - } - - state = State.CONNECTING; - } - - setupSSL(); - - super.connect(); - } - - public void disconnect() { - disconnectWithError(null); - } - - public void disconnectWithError(ServiceCommandError error) { - this.close(); - - state = State.INITIAL; - - if (mListener != null) - mListener.onCloseWithError(error); - } - - private void setDefaultManifest() { - manifest = new JSONObject(); - List permissions = mService.getPermissions(); - - try { - manifest.put("manifestVersion", 1); -// manifest.put("appId", 1); -// manifest.put("vendorId", 1); -// manifest.put("localizedAppNames", 1); - manifest.put("permissions", convertStringListToJSONArray(permissions)); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - private JSONArray convertStringListToJSONArray(List list) { - JSONArray jsonArray = new JSONArray(); - - for(String str: list) { - jsonArray.put(str); - } - - return jsonArray; - } - - @Override - public void onOpen(ServerHandshake handshakedata) { - mConnectSucceeded = true; - - this.handleConnected(); - } - - @Override - public void onMessage(String data) { - Log.d(TAG, "webOS Socket [IN] : " + data); - - this.handleMessage(data); - } - - @Override - public void onClose(int code, String reason, boolean remote) { - System.out.println("onClose: " + code + ": " + reason); - this.handleConnectionLost(true, null); - } - - @Override - public void onError(Exception ex) { - System.err.println("onError: " + ex); - - if (!mConnectSucceeded) { - this.handleConnectError(ex); - } else { - this.handleConnectionLost(false, ex); - } - } - - protected void handleConnected() { - helloTV(); - } - - protected void handleConnectError(Exception ex) { - System.err.println("connect error: " + ex.toString()); - - if (mListener != null) - mListener.onFailWithError(new ServiceCommandError(0, "connection error", null)); - } - - protected void handleMessage(String data) { - try { - JSONObject obj = new JSONObject(data); - - handleMessage(obj); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @SuppressWarnings("unchecked") - protected void handleMessage(JSONObject message) { - Boolean shouldProcess = true; - - if (mListener != null) - shouldProcess = mListener.onReceiveMessage(message); - - if (!shouldProcess) - return; - - String type = message.optString("type"); - Object payload = message.opt("payload"); - - String strId = message.optString("id"); - Integer id = null; - ServiceCommand> request = null; - - if ( isInteger(strId) ) { - id = Integer.valueOf(strId); - - try - { - request = (ServiceCommand>) requests.get(id); - } catch (ClassCastException ex) - { - // since request is assigned to null, don't need to do anything here - } - } - - if (type.length() == 0) - return; - - if ("response".equals(type)) { - if (request != null) { -// Log.d("Connect SDK", "Found requests need to handle response"); - if (payload != null) { - Util.postSuccess(request.getResponseListener(), payload); - } - else { - Util.postError(request.getResponseListener(), new ServiceCommandError(-1, "JSON parse error", null)); - } - - if (!(request instanceof URLServiceSubscription)) { - requests.remove(id); - } - } else { - System.err.println("no matching request id: " + strId + ", payload: " + payload.toString()); - } - } else if ("registered".equals(type)) { - if ( !(mService.getServiceConfig() instanceof WebOSTVServiceConfig) ) { - mService.setServiceConfig(new WebOSTVServiceConfig(mService.getServiceConfig().getServiceUUID())); - } - - if (payload instanceof JSONObject) { - String clientKey = ((JSONObject) payload).optString("client-key"); - ((WebOSTVServiceConfig) mService.getServiceConfig()).setClientKey(clientKey); - - // Track SSL certificate - // Not the prettiest way to get it, but we don't have direct access to the SSLEngine - ((WebOSTVServiceConfig) mService.getServiceConfig()).setServerCertificate(customTrustManager.getLastCheckedCertificate()); - - handleRegistered(); - - if (id != null) - requests.remove(id); - } - } else if ("error".equals(type) && message instanceof JSONObject) { - String error = ((JSONObject) message).optString("error"); - if (error.length() == 0) - return; - - int errorCode = -1; - String errorDesc = null; - - try { - String [] parts = error.split(" ", 2); - errorCode = Integer.parseInt(parts[0]); - errorDesc = parts[1]; - } catch (Exception e) { - e.printStackTrace(); - } - - if (payload != null) { - Log.d("Connect SDK", "Error Payload: " + payload.toString()); - } - - if ( message.has("id") ) { - Log.d("Connect SDK", "Error Desc: " + errorDesc); - - if (request != null) { - Util.postError(request.getResponseListener(), new ServiceCommandError(errorCode, errorDesc, payload)); - - if (!(request instanceof URLServiceSubscription)) - requests.remove(id); - - if ( errorCode == 403 ) { // 403 User Denied Access - disconnect(); - return; - } - } - } - } else if ("hello".equals(type)) { - JSONObject jsonObj = (JSONObject)payload; - - if (mService.getServiceConfig().getServiceUUID() != null) { - if (!mService.getServiceConfig().getServiceUUID().equals(jsonObj.optString("deviceUUID"))) { - ((WebOSTVServiceConfig)mService.getServiceConfig()).setClientKey(null); - String cert = null; - ((WebOSTVServiceConfig)mService.getServiceConfig()).setServerCertificate(cert); - ((WebOSTVServiceConfig)mService.getServiceConfig()).setServiceUUID(null); - mService.getServiceDescription().setIpAddress(null); - mService.getServiceDescription().setUUID(null); - - disconnect(); - } - } - else { - String uuid = jsonObj.optString("deviceUUID"); - mService.getServiceConfig().setServiceUUID(uuid); - mService.getServiceDescription().setUUID(uuid); - } - - state = State.REGISTERING; - sendRegister(); - } - } - - private void helloTV() { - Context context = DiscoveryManager.getInstance().getContext(); - PackageManager packageManager = context.getPackageManager(); - - // app Id - String packageName = context.getPackageName(); - - // SDK Version - String sdkVersion = DiscoveryManager.CONNECT_SDK_VERSION; - - // Device Model - String deviceModel = Build.MODEL; - - // OS Version - String OSVersion = String.valueOf(android.os.Build.VERSION.SDK_INT); - - // resolution - WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - Display display = wm.getDefaultDisplay(); - - @SuppressWarnings("deprecation") - int width = display.getWidth(); // deprecated, but still needed for supporting API levels 10-12 - - @SuppressWarnings("deprecation") - int height = display.getHeight(); // deprecated, but still needed for supporting API levels 10-12 - - String screenResolution = String.format("%dx%d", width, height); - - // app Name - ApplicationInfo applicationInfo; - try { - applicationInfo = packageManager.getApplicationInfo(context.getPackageName(), 0); - } catch (final NameNotFoundException e) { - applicationInfo = null; - } - String applicationName = (String) (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : "(unknown)"); - - // app Region - Locale current = context.getResources().getConfiguration().locale; - String appRegion = current.getDisplayCountry(); - - JSONObject payload = new JSONObject(); - try { - payload.put("sdkVersion", sdkVersion); - payload.put("deviceModel", deviceModel); - payload.put("OSVersion", OSVersion); - payload.put("resolution", screenResolution); - payload.put("appId", packageName); - payload.put("appName", applicationName); - payload.put("appRegion", appRegion); - } catch (JSONException e) { - e.printStackTrace(); - } - - int dataId = this.nextRequestId++; - - JSONObject sendData = new JSONObject(); - try { - sendData.put("id", dataId); - sendData.put("type", "hello"); - sendData.put("payload", payload); - } catch (JSONException e) { - e.printStackTrace(); - } - - ServiceCommand> request = new ServiceCommand>(this, null, sendData, true, null); - this.sendCommandImmediately(request); - } - - protected void sendRegister() { - ResponseListener listener = new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - if (mListener != null) - mListener.onRegistrationFailed(error); - } - - @Override - public void onSuccess(Object object) { } - }; - - int dataId = this.nextRequestId++; - - ServiceCommand> command = new ServiceCommand>(this, null, null, listener); - command.setRequestId(dataId); - - JSONObject headers = new JSONObject(); - JSONObject payload = new JSONObject(); - - try { - headers.put("type", "register"); - headers.put("id", dataId); - - if ( !(mService.getServiceConfig() instanceof WebOSTVServiceConfig) ) { - mService.setServiceConfig(new WebOSTVServiceConfig(mService.getServiceConfig().getServiceUUID())); - } - - if (((WebOSTVServiceConfig)mService.getServiceConfig()).getClientKey() != null) { - payload.put("client-key", ((WebOSTVServiceConfig)mService.getServiceConfig()).getClientKey()); - } - else { - if (mListener != null) - mListener.onBeforeRegister(); - } - - if (manifest != null) { - payload.put("manifest", manifest); - } - } catch (JSONException e) { - e.printStackTrace(); - } - - requests.put(dataId, command); - - sendMessage(headers, payload); - } - - protected void handleRegistered() { - state = State.REGISTERED; - - if (!commandQueue.isEmpty()) { - LinkedHashSet>> tempHashSet = new LinkedHashSet>>(commandQueue); - for (ServiceCommand> command : tempHashSet) { - Log.d("Connect SDK", "executing queued command for " + command.getTarget()); - - sendCommandImmediately(command); - commandQueue.remove(command); - } - } - - if (mListener != null) - mListener.onConnect(); - -// ConnectableDevice storedDevice = connectableDeviceStore.getDevice(mService.getServiceConfig().getServiceUUID()); -// if (storedDevice == null) { -// storedDevice = new ConnectableDevice( -// mService.getServiceDescription().getIpAddress(), -// mService.getServiceDescription().getFriendlyName(), -// mService.getServiceDescription().getModelName(), -// mService.getServiceDescription().getModelNumber()); -// } -// storedDevice.addService(WebOSTVService.this); -// connectableDeviceStore.addDevice(storedDevice); - } - - @SuppressWarnings("unchecked") - public void sendCommand(ServiceCommand command) { - Integer requestId; - if (command.getRequestId() == -1) { - requestId = this.nextRequestId++; - command.setRequestId(requestId); - } - else { - requestId = command.getRequestId(); - } - - requests.put(requestId, command); - - if (state == State.REGISTERED) { - this.sendCommandImmediately(command); - } else if (state == State.CONNECTING || state == State.DISCONNECTING){ - Log.d("Connect SDK", "queuing command for " + command.getTarget()); - commandQueue.add((ServiceCommand>) command); - } else { - Log.d("Connect SDK", "queuing command and restarting socket for " + command.getTarget()); - commandQueue.add((ServiceCommand>) command); - connect(); - } - } - - public void unsubscribe(URLServiceSubscription subscription) { - int requestId = subscription.getRequestId(); - - if (requests.get(requestId) != null) { - JSONObject headers = new JSONObject(); - - try{ - headers.put("type", "unsubscribe"); - headers.put("id", String.valueOf(requestId)); - } catch (JSONException e) - { - // Safe to ignore - e.printStackTrace(); - } - - sendMessage(headers, null); - requests.remove(requestId); - } - } - - protected void sendCommandImmediately(ServiceCommand command) { - JSONObject headers = new JSONObject(); - JSONObject payload = (JSONObject) command.getPayload(); - String payloadType = ""; - - try - { - payloadType = payload.getString("type"); - } catch (Exception ex) - { - // ignore - } - - if (payloadType == "p2p") - { - Iterator iterator = payload.keys(); - - while (iterator.hasNext()) - { - String key = (String) iterator.next(); - - try - { - headers.put(key, payload.get(key)); - } catch (JSONException ex) - { - // ignore - } - } - - this.sendMessage(headers, null); - } - else if (payloadType == "hello") { - this.send(payload.toString()); - } - else { - try - { - headers.put("type", command.getHttpMethod()); - headers.put("id", String.valueOf(command.getRequestId())); - headers.put("uri", command.getTarget()); - } catch (JSONException ex) - { - // TODO: handle this - } - - - this.sendMessage(headers, payload); - } - } - - private void setSSLContext(SSLContext sslContext) { - setWebSocketFactory(new DefaultSSLWebSocketClientFactory(sslContext)); - } - - protected void setupSSL() { - try { - SSLContext sslContext = SSLContext.getInstance("TLS"); - customTrustManager = new TrustManager(); - sslContext.init(null, new TrustManager [] {customTrustManager}, null); - setSSLContext(sslContext); - - if ( !(mService.getServiceConfig() instanceof WebOSTVServiceConfig) ) { - mService.setServiceConfig(new WebOSTVServiceConfig(mService.getServiceConfig().getServiceUUID())); - } - customTrustManager.setExpectedCertificate(((WebOSTVServiceConfig)mService.getServiceConfig()).getServerCertificate()); - } catch (KeyException e) { - } catch (NoSuchAlgorithmException e) { - } - } - - public boolean isConnected() { - return this.getReadyState() == WebSocket.READYSTATE.OPEN; - } - - public void sendMessage(JSONObject packet, JSONObject payload) { -// JSONObject packet = new JSONObject(); - - try { -// for (Map.Entry entry : headers.entrySet()) { -// packet.put(entry.getKey(), entry.getValue()); -// } - - if (payload != null) { - packet.put("payload", payload); - } - } catch (JSONException e) { - throw new Error(e); - } - - if ( isConnected() ) { - String message = packet.toString(); - - Log.d(TAG, "webOS Socket [OUT] : " + message); - - this.send(message); - } - else { - System.err.println("connection lost"); - handleConnectionLost(false, null); - } - } - - @SuppressWarnings("unchecked") - private void handleConnectionLost(boolean cleanDisconnect, Exception ex) { - ServiceCommandError error = null; - - if (ex != null || !cleanDisconnect) - error = new ServiceCommandError(0, "conneciton error", ex); - - if (mListener != null) - mListener.onCloseWithError(error); - - for (int i = 0; i < requests.size(); i++) { - ServiceCommand> request = (ServiceCommand>) requests.get(requests.keyAt(i)); - - if (request != null) - Util.postError(request.getResponseListener(), new ServiceCommandError(0, "connection lost", null)); - } - - requests.clear(); - } - - public void setServerCertificate(X509Certificate cert) { - if ( !(mService.getServiceConfig() instanceof WebOSTVServiceConfig) ) { - mService.setServiceConfig(new WebOSTVServiceConfig(mService.getServiceConfig().getServiceUUID())); - } - - ((WebOSTVServiceConfig)mService.getServiceConfig()).setServerCertificate(cert); - } - - public void setServerCertificate(String cert) { - if ( !(mService.getServiceConfig() instanceof WebOSTVServiceConfig) ) { - mService.setServiceConfig(new WebOSTVServiceConfig(mService.getServiceConfig().getServiceUUID())); - } - - ((WebOSTVServiceConfig)mService.getServiceConfig()).setServerCertificate(loadCertificateFromPEM(cert)); - } - - public X509Certificate getServerCertificate() { - if ( !(mService.getServiceConfig() instanceof WebOSTVServiceConfig) ) { - mService.setServiceConfig(new WebOSTVServiceConfig(mService.getServiceConfig().getServiceUUID())); - } - - return ((WebOSTVServiceConfig)mService.getServiceConfig()).getServerCertificate(); - } - - public String getServerCertificateInString() { - if ( !(mService.getServiceConfig() instanceof WebOSTVServiceConfig) ) { - mService.setServiceConfig(new WebOSTVServiceConfig(mService.getServiceConfig().getServiceUUID())); - } - - return exportCertificateToPEM(((WebOSTVServiceConfig)mService.getServiceConfig()).getServerCertificate()); - } - - private String exportCertificateToPEM(X509Certificate cert) { - try { - return Base64.encodeToString(cert.getEncoded(), Base64.DEFAULT); - } catch (CertificateEncodingException e) { - e.printStackTrace(); - return null; - } - } - - private X509Certificate loadCertificateFromPEM(String pemString) { - CertificateFactory certFactory; - try { - certFactory = CertificateFactory.getInstance("X.509"); - ByteArrayInputStream inputStream = new ByteArrayInputStream(pemString.getBytes("US-ASCII")); - - return (X509Certificate)certFactory.generateCertificate(inputStream); - } catch (CertificateException e) { - e.printStackTrace(); - return null; - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - return null; - } - } - - public static boolean isInteger(String s) { - try { - Integer.parseInt(s); - } catch(NumberFormatException e) { - return false; - } - // only got here if we didn't return false - return true; - } - - class TrustManager implements X509TrustManager { - X509Certificate expectedCert; - X509Certificate lastCheckedCert; - - public void setExpectedCertificate(X509Certificate cert) { - this.expectedCert = cert; - } - - public X509Certificate getLastCheckedCertificate () { - return lastCheckedCert; - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { - Log.d("Connect SDK", "Expecting device cert " + (expectedCert != null ? expectedCert.getSubjectDN() : "(any)")); - - if (chain != null && chain.length > 0) { - X509Certificate cert = chain[0]; - - lastCheckedCert = cert; - - if (expectedCert != null) { - byte [] certBytes = cert.getEncoded(); - byte [] expectedCertBytes = expectedCert.getEncoded(); - - Log.d("Connect SDK", "Device presented cert " + cert.getSubjectDN()); - - if (!Arrays.equals(certBytes, expectedCertBytes)) { - throw new CertificateException("certificate does not match"); - } - } - } else { - lastCheckedCert = null; - throw new CertificateException("no server certificate"); - } - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - } - - public interface WebOSTVServiceSocketClientListener { - - public void onConnect(); - public void onCloseWithError(ServiceCommandError error); - public void onFailWithError(ServiceCommandError error); - - public void onBeforeRegister(); - public void onRegistrationFailed(ServiceCommandError error); - public Boolean onReceiveMessage(JSONObject message); - - } - -} From 96be37279aabe8f19eba36667e664b1c0b1f6940 Mon Sep 17 00:00:00 2001 From: Simon Gladkoskok Date: Mon, 18 Aug 2014 10:50:32 -0700 Subject: [PATCH 17/76] added full path to Class names in DefaultPlatforms --- src/com/connectsdk/DefaultPlatform.java | 42 ++++++++----------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/src/com/connectsdk/DefaultPlatform.java b/src/com/connectsdk/DefaultPlatform.java index 8cbea76b..433c1078 100644 --- a/src/com/connectsdk/DefaultPlatform.java +++ b/src/com/connectsdk/DefaultPlatform.java @@ -4,39 +4,23 @@ public class DefaultPlatform { - -// registerDeviceService(WebOSTVService.class, SSDPDiscoveryProvider.class); -// registerDeviceService(NetcastTVService.class, SSDPDiscoveryProvider.class); -// registerDeviceService(DLNAService.class, SSDPDiscoveryProvider.class); -// registerDeviceService(DIALService.class, SSDPDiscoveryProvider.class); -// registerDeviceService(RokuService.class, SSDPDiscoveryProvider.class); -// registerDeviceService(CastService.class, CastDiscoveryProvider.class); -// registerDeviceService(AirPlayService.class, ZeroconfDiscoveryProvider.class); -// registerDeviceService(MultiScreenService.class, SSDPDiscoveryProvider.class); - private HashMap deviceServiceMap; + public DefaultPlatform() { - - deviceServiceMap = new HashMap(); } - - public HashMap getDeviceServiceMap() { - deviceServiceMap.put("WebOSTVService", "SSDPDiscoveryProvider"); - deviceServiceMap.put("NetcastTVService", "SSDPDiscoveryProvider"); - deviceServiceMap.put("DLNAService", "SSDPDiscoveryProvider"); - deviceServiceMap.put("DIALService", "SSDPDiscoveryProvider"); - deviceServiceMap.put("RokuService", "SSDPDiscoveryProvider"); - deviceServiceMap.put("CastService", "CastDiscoveryProvider"); - deviceServiceMap.put("AirPlayService", "ZeroconfDiscoveryProvider"); - deviceServiceMap.put("MultiScreenService", "SSDPDiscoveryProvider"); - return deviceServiceMap; + + public static HashMap getDeviceServiceMap() { + HashMap devicesList = new HashMap(); + devicesList.put("com.connectsdk.service.WebOSTVService", "com.connectsdk.discovery.provider.SSDPDiscoveryProvider"); + devicesList.put("com.connectsdk.service.NetcastTVService", "com.connectsdk.discovery.provider.SSDPDiscoveryProvider"); + devicesList.put("com.connectsdk.service.DLNAService", "com.connectsdk.discovery.provider.SSDPDiscoveryProvider"); + devicesList.put("com.connectsdk.service.DIALService", "com.connectsdk.discovery.provider.SSDPDiscoveryProvider"); + devicesList.put("com.connectsdk.service.RokuService", "com.connectsdk.discovery.provider.SSDPDiscoveryProvider"); + devicesList.put("com.connectsdk.androidgooglecast.CastService", "com.connectsdk.androidgooglecast.CastDiscoveryProvider"); + devicesList.put("com.connectsdk.service.AirPlayService", "com.connectsdk.discovery.provider.ZeroconfDiscoveryProvider"); + devicesList.put("com.connectsdk.samsungmultiscreen.MultiScreenService", "com.connectsdk.discovery.provider.SSDPDiscoveryProvider"); + return devicesList; } - - - - - - } From e3bee481fadb92bfb4a95e82597b7937ec283a61 Mon Sep 17 00:00:00 2001 From: simongladkoskok Date: Mon, 18 Aug 2014 14:00:34 -0700 Subject: [PATCH 18/76] Update README.md --- README.md | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 10c8283a..3568329e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ #Connect SDK Android -Connect SDK is an open source framework that unifies device discovery and connectivity by providing one set of methods that work across multiple television platforms and protocols. +Connect SDK is an open source framework that connects your mobile apps with multiple TV platforms. Because most TV platforms support a variety of protocols, Connect SDK integrates and abstracts the discovery and connectivity between all supported protocols. For more information, visit our [website](http://www.connectsdk.com/). @@ -8,31 +8,35 @@ For more information, visit our [website](http://www.connectsdk.com/). * [API documentation](http://www.connectsdk.com/apis/android/) ##Dependencies +This project has the following dependencies, some of which require manual setup. If you would like to use a version of the SDK which has no manual setup, consider using the [lite version](https://github.com/ConnectSDK/Connect-SDK-Android-Lite) of the SDK. + This project has the following dependencies. +* [Connect-SDK-Android-Core](https://github.com/ConnectSDK/Connect-SDK-Android-Core) submodule +* [Connect-SDK-Android-Google-Cast](https://github.com/ConnectSDK/Connect-SDK-Android-Google-Cast) submodule + - Requires [GoogleCast.framework](https://developers.google.com/cast/docs/downloads) +* [Connect-SDK-Android-Samsung-MultiScreen](https://github.com/ConnectSDK/Connect-SDK-Android-Samsung-MultiScreen) submodule + - Requires [SamsungMultiScreen.framework](http://multiscreen.samsung.com/downloads.html) * [Java-WebSocket library](https://github.com/TooTallNate/Java-WebSocket) -* [Android Support v7 Libraries](https://developer.android.com/tools/support-library/setup.html) - - appcompat - - mediarouter -* [Google Play Services](http://developer.android.com/google/play-services/setup.html) ##Including Connect SDK in your app -1. Setup up your dependencies, listed above -2. Clone Connect-SDK-Android project (or download & unzip) +1. Clone repository (or download & unzip) +2. Set up the submodules by running the following commands in Terminal + - `git submodule init` + - `git submodule update` 3. Open Eclipse 4. Click File > Import 5. Select `Existing Android Code Into Workspace` and click Next 6. Browse to the Connect-SDK-Android project folder and click Open 7. Click Finish -8. Right-click the Connect-SDK-Android project and select Properties -9. In the Library pane of the Android tab, add the following library references - - android-support-v7-appcompat - - android-support-v7-mediarouter - - google-play-services_lib -10. **You must update these libraries to API 10 in their manifest.** -11. Click OK -12. Right-click your project and select Properties -13. In the Library pane of the Android tab, add the Connect-SDK-Android project +8. Do the steps 4-7 for Connect-SDK-Android-Core which is located in `core` folder of the Connect-SDK-Android project +9. Do the steps 4-7 for Connect-SDK-Android-Google-Cast which is located in `modules/google_cast` folder of the Connect-SDK-Android project +10. Do the steps 4-7 for Connect-SDK-Android-Samsung-MultiScreen which is located in `modules/samsung_multiscreen` folder of the Connect-SDK-Android project +11. Follow the setup instructions for each of the service submodules + - [Connect-SDK-Android-Google-Cast](https://github.com/ConnectSDK/Connect-SDK-Android-Google-Cast) + - [Connect-SDK-Android-Samsung-MultiScreen](https://github.com/ConnectSDK/Connect-SDK-Android-Samsung-MultiScreen) +12. Right-click the Connect-SDK-Android project and select Properties +13. In the Library pane of the Android tab, add Connect-SDK-Android-Core, Connect-SDK-Android-Google-Cast, and Connect-SDK-Android-Samsung-MultiScreen projects 14. Set up your manifest file as per the instructions below ###Permissions to include in manifest From c7b04d3b051da6bc86f844f29b499b160676adee Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Tue, 19 Aug 2014 13:23:59 +0900 Subject: [PATCH 19/76] Removed duplicate code --- src/com/connectsdk/service/sessions/CastWebAppSession.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/com/connectsdk/service/sessions/CastWebAppSession.java b/src/com/connectsdk/service/sessions/CastWebAppSession.java index 48a3773f..216809cf 100644 --- a/src/com/connectsdk/service/sessions/CastWebAppSession.java +++ b/src/com/connectsdk/service/sessions/CastWebAppSession.java @@ -59,8 +59,6 @@ public void connect(final ResponseListener listener) { castServiceChannel = new CastServiceChannel(launchSession.getAppId(), this); try { - Cast.CastApi.removeMessageReceivedCallbacks(service.getApiClient(), castServiceChannel.getNamespace()); - Cast.CastApi.setMessageReceivedCallbacks(service.getApiClient(), castServiceChannel.getNamespace(), castServiceChannel); From acecd5147dca710fa020dee5a2ffde126acddb88 Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Wed, 20 Aug 2014 14:20:24 +0900 Subject: [PATCH 20/76] Refactored Service classes to remove duplicate methods and minor things --- .../connectsdk/service/AirPlayService.java | 111 ++---------- src/com/connectsdk/service/DLNAService.java | 121 +++---------- .../service/MultiScreenService.java | 90 ++-------- .../connectsdk/service/NetcastTVService.java | 149 +++++----------- src/com/connectsdk/service/RokuService.java | 81 ++------- .../connectsdk/service/WebOSTVService.java | 167 ++---------------- 6 files changed, 131 insertions(+), 588 deletions(-) diff --git a/src/com/connectsdk/service/AirPlayService.java b/src/com/connectsdk/service/AirPlayService.java index e0e40c0d..6cb4e62f 100644 --- a/src/com/connectsdk/service/AirPlayService.java +++ b/src/com/connectsdk/service/AirPlayService.java @@ -52,6 +52,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import com.connectsdk.core.ImageInfo; import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; import com.connectsdk.etc.helper.DeviceServiceReachability; @@ -167,7 +168,7 @@ public void fastForward(ResponseListener listener) { } @Override - public void seek(final long position, ResponseListener listener) { + public void seek(long position, ResponseListener listener) { float pos = ((float) position / 1000); Map params = new HashMap(); @@ -285,7 +286,6 @@ private void getPlaybackInfo(ResponseListener listener) { public ServiceSubscription subscribePlayState( PlayStateListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); - return null; } @@ -349,55 +349,15 @@ public void onError(ServiceCommandError error) { } }); } + @Override - public void displayImage(final MediaInfo mediaInfo, final LaunchListener listener) { - Util.runInBackground(new Runnable() { - - @Override - public void run() { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - LaunchSession launchSession = new LaunchSession(); - launchSession.setService(AirPlayService.this); - launchSession.setSessionType(LaunchSessionType.Media); - - Util.postSuccess(listener, new MediaLaunchObject(launchSession, AirPlayService.this)); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - String uri = getRequestURL("photo"); - HttpEntity entity = null; - - try { - URL imagePath = new URL(mediaInfo.getUrl()); - HttpURLConnection connection = (HttpURLConnection) imagePath.openConnection(); - connection.setDoInput(true); - connection.connect(); - InputStream input = connection.getInputStream(); - Bitmap myBitmap = BitmapFactory.decodeStream(input); - - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - myBitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream); - - entity = new ByteArrayEntity(stream.toByteArray()); - } catch (MalformedURLException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - - ServiceCommand> request = new ServiceCommand>(AirPlayService.this, uri, entity, responseListener); - request.send(); - } - }); + public void displayImage(MediaInfo mediaInfo, LaunchListener listener) { + ImageInfo imageInfo = mediaInfo.getImages().get(0); + String iconSrc = imageInfo.getUrl(); + + displayImage(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, listener); } + public void playVideo(final String url, String mimeType, String title, String description, String iconSrc, boolean shouldLoop, final LaunchListener listener) { @@ -436,49 +396,12 @@ public void onError(ServiceCommandError error) { request.send(); } - public void playVideo(final MediaInfo mediaInfo, boolean shouldLoop, - final LaunchListener listener) { - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - LaunchSession launchSession = new LaunchSession(); - launchSession.setService(AirPlayService.this); - launchSession.setSessionType(LaunchSessionType.Media); - - Util.postSuccess(listener, new MediaLaunchObject(launchSession, AirPlayService.this)); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - String uri = getRequestURL("play"); - HttpEntity entity = null; - - PListBuilder builder = new PListBuilder(); - builder.putString("Content-Location", mediaInfo.getUrl()); - builder.putReal("Start-Position", 0); - - try { - entity = new StringEntity(builder.toString()); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - - ServiceCommand> request = new ServiceCommand>(this, uri, entity, responseListener); - request.send(); - } - @Override public void playMedia(String url, String mimeType, String title, String description, String iconSrc, boolean shouldLoop, LaunchListener listener) { - if ( mimeType.contains("image") ) { + if (mimeType.contains("image")) { displayImage(url, mimeType, title, description, iconSrc, listener); } else { @@ -487,15 +410,11 @@ public void playMedia(String url, String mimeType, String title, } @Override - public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, - LaunchListener listener) { - - if ( mediaInfo.getMimeType().contains("image") ) { - displayImage(mediaInfo, listener); - } - else { - playVideo(mediaInfo, shouldLoop, listener); - } + public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, LaunchListener listener) { + ImageInfo imageInfo = mediaInfo.getImages().get(0); + String iconSrc = imageInfo.getUrl(); + + playMedia(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, shouldLoop, listener); } @Override diff --git a/src/com/connectsdk/service/DLNAService.java b/src/com/connectsdk/service/DLNAService.java index 8840372f..03833b1b 100644 --- a/src/com/connectsdk/service/DLNAService.java +++ b/src/com/connectsdk/service/DLNAService.java @@ -42,6 +42,7 @@ import org.json.JSONException; import org.json.JSONObject; +import com.connectsdk.core.ImageInfo; import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; import com.connectsdk.core.upnp.service.Service; @@ -134,7 +135,7 @@ public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { return CapabilityPriorityLevel.NORMAL; } - public void displayMedia(final String url, final String mimeType, final String title, final String description, final String iconSrc, final LaunchListener listener) { + public void displayMedia(String url, String mimeType, String title, String description, String iconSrc, final LaunchListener listener) { final String instanceId = "0"; String[] mediaElements = mimeType.split("/"); String mediaType = mediaElements[0]; @@ -171,9 +172,7 @@ public void onSuccess(Object response) { @Override public void onError(ServiceCommandError error) { - if ( listener != null ) { - listener.onError(error); - } + Util.postError(listener, error); } }; @@ -183,9 +182,7 @@ public void onError(ServiceCommandError error) { @Override public void onError(ServiceCommandError error) { - if ( listener != null ) { - listener.onError(error); - } + Util.postError(listener, error); } }; @@ -196,68 +193,6 @@ public void onError(ServiceCommandError error) { request.send(); } - public void displayMedia(final MediaInfo mediaInfo, final LaunchListener listener) { - final String instanceId = "0"; - String[] mediaElements = mediaInfo.getMimeType().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 responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - String method = "Play"; - - Map parameters = new HashMap(); - parameters.put("Speed", "1"); - - JSONObject payload = getMethodBody(instanceId, method, parameters); - - ResponseListener playResponseListener = new ResponseListener () { - @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)); - } - - @Override - public void onError(ServiceCommandError error) { - if ( listener != null ) { - listener.onError(error); - } - } - }; - - ServiceCommand> request = new ServiceCommand>(DLNAService.this, method, payload, playResponseListener); - request.send(); - } - - @Override - public void onError(ServiceCommandError error) { - if ( listener != null ) { - listener.onError(error); - } - } - }; - - String method = "SetAVTransportURI"; - JSONObject httpMessage = getSetAVTransportURIBody(method, instanceId, mediaInfo.getUrl(), mMimeType, mediaInfo.getTitle()); - - ServiceCommand> request = new ServiceCommand>(DLNAService.this, method, httpMessage, responseListener); - request.send(); - } - @Override public void displayImage(String url, String mimeType, String title, String description, String iconSrc, LaunchListener listener) { displayMedia(url, mimeType, title, description, iconSrc, listener); @@ -265,13 +200,14 @@ public void displayImage(String url, String mimeType, String title, String descr @Override public void displayImage(MediaInfo mediaInfo, LaunchListener listener) { - - displayMedia(mediaInfo, listener); - + ImageInfo imageInfo = mediaInfo.getImages().get(0); + String iconSrc = imageInfo.getUrl(); + + displayImage(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, listener); } @Override - public void playMedia(final String url, final String mimeType, final String title, final String description, final String iconSrc, final boolean shouldLoop, final LaunchListener listener) { + public void playMedia(String url, String mimeType, String title, String description, String iconSrc, boolean shouldLoop, LaunchListener listener) { displayMedia(url, mimeType, title, description, iconSrc, listener); // stop(new ResponseListener() { // @@ -344,7 +280,10 @@ public void playMedia(final String url, final String mimeType, final String titl @Override public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, LaunchListener listener) { - + ImageInfo imageInfo = mediaInfo.getImages().get(0); + String iconSrc = imageInfo.getUrl(); + + playMedia(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, shouldLoop, listener); } @Override @@ -367,7 +306,7 @@ public CapabilityPriorityLevel getMediaControlCapabilityLevel() { } @Override - public void play(final ResponseListener listener) { + public void play(ResponseListener listener) { String method = "Play"; String instanceId = "0"; @@ -381,7 +320,7 @@ public void play(final ResponseListener listener) { } @Override - public void pause(final ResponseListener listener) { + public void pause(ResponseListener listener) { String method = "Pause"; String instanceId = "0"; @@ -392,7 +331,7 @@ public void pause(final ResponseListener listener) { } @Override - public void stop(final ResponseListener listener) { + public void stop(ResponseListener listener) { String method = "Stop"; String instanceId = "0"; @@ -403,12 +342,12 @@ public void stop(final ResponseListener listener) { } @Override - public void rewind(final ResponseListener listener) { + public void rewind(ResponseListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); } @Override - public void fastForward(final ResponseListener listener) { + public void fastForward(ResponseListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); } @@ -443,7 +382,6 @@ private void getPositionInfo(final PositionInfoListener listener) { @Override public void onSuccess(Object response) { - if (listener != null) { listener.onGetPositionInfoSuccess((String)response); } @@ -471,16 +409,12 @@ public void onGetPositionInfoSuccess(String positionInfoXml) { long milliTimes = convertStrTimeFormatToLong(strDuration) * 1000; - if (listener != null) { - listener.onSuccess(milliTimes); - } + Util.postSuccess(listener, milliTimes); } @Override public void onGetPositionInfoFailed(ServiceCommandError error) { - if (listener != null) { - listener.onError(error); - } + Util.postError(listener, error); } }); } @@ -495,16 +429,12 @@ public void onGetPositionInfoSuccess(String positionInfoXml) { long milliTimes = convertStrTimeFormatToLong(strDuration) * 1000; - if (listener != null) { - listener.onSuccess(milliTimes); - } + Util.postSuccess(listener, milliTimes); } @Override public void onGetPositionInfoFailed(ServiceCommandError error) { - if (listener != null) { - listener.onError(error); - } + Util.postError(listener, error); } }); } @@ -716,15 +646,12 @@ private long convertStrTimeFormatToLong(String strTime) { @Override public void getPlayState(PlayStateListener listener) { - if (listener != null) - listener.onError(ServiceCommandError.notSupported()); + Util.postError(listener, ServiceCommandError.notSupported()); } @Override public ServiceSubscription subscribePlayState(PlayStateListener listener) { - if (listener != null) - listener.onError(ServiceCommandError.notSupported()); - + Util.postError(listener, ServiceCommandError.notSupported()); return null; } diff --git a/src/com/connectsdk/service/MultiScreenService.java b/src/com/connectsdk/service/MultiScreenService.java index 26c248a3..1bd9bc44 100644 --- a/src/com/connectsdk/service/MultiScreenService.java +++ b/src/com/connectsdk/service/MultiScreenService.java @@ -9,6 +9,7 @@ import org.json.JSONException; import org.json.JSONObject; +import com.connectsdk.core.ImageInfo; import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; import com.connectsdk.service.capability.MediaControl; @@ -161,45 +162,11 @@ public void onSuccess(Object object) { } @Override - public void displayImage(final MediaInfo mediaInfo, final LaunchListener listener) { - final String webAppId = "ConnectSDKSampler"; - - getWebAppLauncher().joinWebApp(webAppId, new WebAppSession.LaunchListener() { - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.getMediaPlayer().displayImage(mediaInfo, listener); - } - - @Override - public void onError(ServiceCommandError error) { - getWebAppLauncher().launchWebApp(webAppId, new WebAppSession.LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) { - Util.postError(listener, error); - } - } - - @Override - public void onSuccess(final WebAppSession webAppSession) { - webAppSession.connect(new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - webAppSession.getMediaPlayer().displayImage(mediaInfo, listener); - } - }); - } - }); - } - }); + public void displayImage(MediaInfo mediaInfo, LaunchListener listener) { + ImageInfo imageInfo = mediaInfo.getImages().get(0); + String iconSrc = imageInfo.getUrl(); + + displayImage(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, listener); } @Override @@ -247,46 +214,11 @@ public void onSuccess(Object object) { } @Override - public void playMedia(final MediaInfo mediaInfo, final boolean shouldLoop, - final LaunchListener listener) { - final String webAppId = "ConnectSDKSampler"; - - getWebAppLauncher().joinWebApp(webAppId, new WebAppSession.LaunchListener() { - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.playMedia(mediaInfo, shouldLoop, listener); - } - - @Override - public void onError(ServiceCommandError error) { - getWebAppLauncher().launchWebApp(webAppId, new WebAppSession.LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) { - Util.postError(listener, error); - } - } - - @Override - public void onSuccess(final WebAppSession webAppSession) { - webAppSession.connect(new ResponseListener() { - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - - @Override - public void onSuccess(Object object) { - webAppSession.playMedia(mediaInfo, shouldLoop, listener); - } - }); - } - }); - } - }); + public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, LaunchListener listener) { + ImageInfo imageInfo = mediaInfo.getImages().get(0); + String iconSrc = imageInfo.getUrl(); + + playMedia(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, shouldLoop, listener); } @Override diff --git a/src/com/connectsdk/service/NetcastTVService.java b/src/com/connectsdk/service/NetcastTVService.java index de2a8bc7..6dc67a47 100644 --- a/src/com/connectsdk/service/NetcastTVService.java +++ b/src/com/connectsdk/service/NetcastTVService.java @@ -57,6 +57,7 @@ import com.connectsdk.core.AppInfo; import com.connectsdk.core.ChannelInfo; import com.connectsdk.core.ExternalInputInfo; +import com.connectsdk.core.ImageInfo; import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; import com.connectsdk.device.ConnectableDevice; @@ -193,12 +194,12 @@ public void connect() { return; // don't try to connect again while connected } - if ( !(serviceConfig instanceof NetcastTVServiceConfig) ) { + if (!(serviceConfig instanceof NetcastTVServiceConfig)) { serviceConfig = new NetcastTVServiceConfig(serviceConfig.getServiceUUID()); } - if ( DiscoveryManager.getInstance().getPairingLevel() == PairingLevel.ON ) { - if ( ((NetcastTVServiceConfig) serviceConfig).getPairingKey() != null + if (DiscoveryManager.getInstance().getPairingLevel() == PairingLevel.ON) { + if (((NetcastTVServiceConfig) serviceConfig).getPairingKey() != null && ((NetcastTVServiceConfig)serviceConfig).getPairingKey().length() != 0) { sendPairingKey(((NetcastTVServiceConfig) serviceConfig).getPairingKey()); @@ -239,7 +240,7 @@ public void run() { } }); - if ( httpServer != null ) { + if (httpServer != null) { httpServer.stop(); httpServer = null; } @@ -347,7 +348,7 @@ public void onError(ServiceCommandError error) { public void sendPairingKey(final String pairingKey) { state = State.PAIRING; - if ( !(serviceConfig instanceof NetcastTVServiceConfig) ) { + if (!(serviceConfig instanceof NetcastTVServiceConfig)) { serviceConfig = new NetcastTVServiceConfig(serviceConfig.getServiceUUID()); } @@ -453,14 +454,14 @@ public void onSuccess(Object response) { setId(decToHex(strObj)); }}; - if ( appId != null ) { + if (appId != null) { Util.postSuccess(listener, appId); } } @Override public void onError(ServiceCommandError error) { - if ( listener != null ) + if (listener != null) Util.postError(listener, error); } }; @@ -543,10 +544,10 @@ public void onError(ServiceCommandError error) { Map params = new HashMap(); params.put("name", "AppExecute"); params.put("auid", auid); - if ( appName != null ) { + if (appName != null) { params.put("appname", appName); } - if ( contentId != null ) { + if (contentId != null) { params.put("contentid", contentId); } @@ -583,7 +584,7 @@ public void launchAppWithInfo(AppInfo appInfo, Object params, Launcher.AppLaunch @Override public void launchBrowser(String url, final Launcher.AppLaunchListener listener) { - if ( !(url == null || url.length() == 0) ) + if (!(url == null || url.length() == 0)) Log.w("Connect SDK", "Netcast TV does not support deeplink for Browser"); final String appName = "Internet"; @@ -630,9 +631,7 @@ public void onError(ServiceCommandError error) { }); } else { - if (listener != null) { - listener.onError(new ServiceCommandError(0, "Cannot reach DIAL service for launching with provided start time", null)); - } + Util.postError(listener, new ServiceCommandError(0, "Cannot reach DIAL service for launching with provided start time", null)); } } @@ -690,8 +689,7 @@ public void onSuccess(Object response) { @Override public void onError(ServiceCommandError error) { - if ( listener != null ) - Util.postError(listener, error); + Util.postError(listener, error); } }; @@ -716,8 +714,7 @@ public void onError(ServiceCommandError error) { @Override public void onError(ServiceCommandError error) { - if ( listener != null ) - Util.postError(listener, error); + Util.postError(listener, error); } }); } @@ -781,7 +778,7 @@ public void closeApp(LaunchSession launchSession, ResponseListener liste command.send(); } - private void getTotalNumberOfApplications(final int type, final AppCountListener listener) { + private void getTotalNumberOfApplications(int type, final AppCountListener listener) { ResponseListener responseListener = new ResponseListener() { @Override @@ -806,7 +803,7 @@ public void onError(ServiceCommandError error) { command.send(); } - private void getApplications(final int type, final int number, final AppListListener listener) { + private void getApplications(int type, int number, final AppListListener listener) { ResponseListener responseListener = new ResponseListener() { @Override @@ -832,16 +829,12 @@ public void onSuccess(Object response) { } } - if ( listener != null ) { - Util.postSuccess(listener, appList); - } + Util.postSuccess(listener, appList); } @Override public void onError(ServiceCommandError error) { - if ( listener != null ) { -// listener.onGetApplicationsFailed(error) - } + Util.postError(listener, error); } }; @@ -1066,8 +1059,8 @@ public void onSuccess(List channelList) { String sourceIndex = (String) rawData.get("sourceIndex"); int physicalNum = (Integer) rawData.get("physicalNumber"); - if ( Integer.valueOf(major) == majorNumber - && Integer.valueOf(minor) == minorNumber ) { + if (Integer.valueOf(major) == majorNumber + && Integer.valueOf(minor) == minorNumber) { params.put("name", "HandleChannelChange"); params.put("major", major); params.put("minor", minor); @@ -1114,7 +1107,7 @@ public void onSuccess(Object response) { JSONArray channelArray = parser.getJSONChannelArray(); - if ( channelArray.length() > 0 ) { + if (channelArray.length() > 0) { JSONObject rawData = (JSONObject) channelArray.get(0); ChannelInfo channel = NetcastChannelParser.parseRawChannelData(rawData); @@ -1188,7 +1181,7 @@ public void set3DEnabled(final boolean enabled, final ResponseListener l @Override public void onSuccess(Boolean isEnabled) { - if ( enabled != isEnabled ) { + if (enabled != isEnabled) { sendKeyCode(VirtualKeycodes.VIDEO_3D.getCode(), listener); } } @@ -1266,7 +1259,7 @@ public void volumeDown(ResponseListener listener) { @Override public void setVolume(float volume, ResponseListener listener) { // Do nothing - not supported - Util.postError(listener, ServiceCommandError.notSupported()); + Util.postError(listener, ServiceCommandError.notSupported()); } @Override @@ -1291,7 +1284,7 @@ public void setMute(final boolean isMute, final ResponseListener listene @Override public void onSuccess(VolumeStatus status) { - if ( isMute != status.isMute ) { + if (isMute != status.isMute) { sendKeyCode(VirtualKeycodes.MUTE.getCode(), listener); } } @@ -1392,7 +1385,7 @@ public void onSuccess(final AppInfo appInfo) { @Override public void onSuccess(LaunchSession session) { - if ( inputPickerSession == null ) { + if (inputPickerSession == null) { inputPickerSession = session; } @@ -1448,7 +1441,7 @@ public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { @Override public void displayImage(final String url, final String mimeType, final String title, final String description, final String iconSrc, final MediaPlayer.LaunchListener listener) { - if ( getDLNAService() != null ) { + if (getDLNAService() != null) { final MediaPlayer.LaunchListener launchListener = new LaunchListener() { @Override @@ -1477,40 +1470,16 @@ public void onSuccess(MediaLaunchObject object) { } @Override - public void displayImage(final MediaInfo mediaInfo, final LaunchListener listener) { - - if ( dlnaService != null ) { - final MediaPlayer.LaunchListener launchListener = new LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) - Util.postError(listener, error); - } - - @Override - public void onSuccess(MediaLaunchObject object) { - object.launchSession.setAppId("SmartShareª"); - object.launchSession.setAppName("SmartShareª"); - - object.mediaControl = NetcastTVService.this.getMediaControl(); - - if (listener != null) - Util.postSuccess(listener, object); - } - }; - - getDLNAService().displayImage(mediaInfo, launchListener); - } - else { - System.err.println("DLNA Service is not ready yet"); - } - + public void displayImage(MediaInfo mediaInfo, LaunchListener listener) { + ImageInfo imageInfo = mediaInfo.getImages().get(0); + String iconSrc = imageInfo.getUrl(); + + displayImage(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, listener); } @Override public void playMedia(final String url, final String mimeType, final String title, final String description, final String iconSrc, final boolean shouldLoop, final MediaPlayer.LaunchListener listener) { - if ( getDLNAService() != null ) { + if (getDLNAService() != null) { final MediaPlayer.LaunchListener launchListener = new LaunchListener() { @Override @@ -1539,33 +1508,11 @@ public void onSuccess(MediaLaunchObject object) { } @Override - public void playMedia(final MediaInfo mediaInfo, final boolean shouldLoop, final MediaPlayer.LaunchListener listener) { - if ( getDLNAService() != null ) { - final MediaPlayer.LaunchListener launchListener = new LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - if (listener != null) - Util.postError(listener, error); - } - - @Override - public void onSuccess(MediaLaunchObject object) { - object.launchSession.setAppId("SmartShareª"); - object.launchSession.setAppName("SmartShareª"); - - object.mediaControl = NetcastTVService.this.getMediaControl(); - - if (listener != null) - Util.postSuccess(listener, object); - } - }; - - getDLNAService().playMedia(mediaInfo, shouldLoop, launchListener); - } - else { - System.err.println("DLNA Service is not ready yet"); - } + public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, MediaPlayer.LaunchListener listener) { + ImageInfo imageInfo = mediaInfo.getImages().get(0); + String iconSrc = imageInfo.getUrl(); + + playMedia(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, shouldLoop, listener); } @Override @@ -1621,7 +1568,7 @@ public void fastForward(ResponseListener listener) { @Override public void seek(long position, ResponseListener listener) { - if ( getDLNAService() != null ) { + if (getDLNAService() != null) { getDLNAService().seek(position, listener); } else { if (listener != null) @@ -1631,7 +1578,7 @@ public void seek(long position, ResponseListener listener) { @Override public void getDuration(DurationListener listener) { - if ( getDLNAService() != null ) { + if (getDLNAService() != null) { getDLNAService().getDuration(listener); } else { if (listener != null) @@ -1641,7 +1588,7 @@ public void getDuration(DurationListener listener) { @Override public void getPosition(PositionListener listener) { - if ( getDLNAService() != null ) { + if (getDLNAService() != null) { getDLNAService().getPosition(listener); } else { if (listener != null) @@ -1813,7 +1760,7 @@ public void onError(ServiceCommandError error) { Map params = new HashMap(); params.put("name", "HandleTouchWheel"); - if ( dy > 0 ) + if (dy > 0) params.put("value", "up"); else params.put("value", "down"); @@ -1883,7 +1830,7 @@ public void onError(ServiceCommandError error) { @Override public void sendDelete() { - if ( keyboardString.length() > 1 ) { + if (keyboardString.length() > 1) { keyboardString.deleteCharAt(keyboardString.length()-1); } else { @@ -2141,21 +2088,21 @@ private String getUDAPRequestURL(String path, String target, String type, String sb.append(serviceDescription.getPort()); sb.append(path); - if ( target != null ) { + if (target != null) { sb.append("?target="); sb.append(target); - if ( type != null ) { + if (type != null) { sb.append("&type="); sb.append(type); } - if ( index != null ) { + if (index != null) { sb.append("&index="); sb.append(index); } - if ( number != null ) { + if (number != null) { sb.append("&number="); sb.append(number); } @@ -2194,7 +2141,7 @@ private String createNode(String tag, String value) { } public String decToHex(String dec) { - if ( dec != null && dec.length() > 0 ) + if (dec != null && dec.length() > 0) return decToHex(Long.parseLong(dec)); return null; } @@ -2240,7 +2187,7 @@ public void run() { final int code = response.getStatusLine().getStatusCode(); - if ( code == 200 ) { + if (code == 200) { HttpEntity entity = response.getEntity(); final String message = EntityUtils.toString(entity, "UTF-8"); diff --git a/src/com/connectsdk/service/RokuService.java b/src/com/connectsdk/service/RokuService.java index 0099c56b..6a9bdf94 100644 --- a/src/com/connectsdk/service/RokuService.java +++ b/src/com/connectsdk/service/RokuService.java @@ -53,6 +53,7 @@ import android.util.Log; import com.connectsdk.core.AppInfo; +import com.connectsdk.core.ImageInfo; import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; import com.connectsdk.device.ConnectableDevice; @@ -204,10 +205,9 @@ public void launchAppWithInfo(AppInfo appInfo, public void launchAppWithInfo(final AppInfo appInfo, Object params, final Launcher.AppLaunchListener listener) { if (appInfo == null || appInfo.getId() == null) { - if (listener != null) - Util.postError(listener, new ServiceCommandError(-1, - "Cannot launch app without valid AppInfo object", - appInfo)); + Util.postError(listener, new ServiceCommandError(-1, + "Cannot launch app without valid AppInfo object", + appInfo)); return; } @@ -380,12 +380,10 @@ public void launchYouTube(String contentId, float startTime, getDIALService().getLauncher().launchYouTube(contentId, startTime, listener); } else { - if (listener != null) { - listener.onError(new ServiceCommandError( - 0, - "Cannot reach DIAL service for launching with provided start time", - null)); - } + Util.postError(listener, new ServiceCommandError( + 0, + "Cannot reach DIAL service for launching with provided start time", + null)); } } @@ -711,59 +709,6 @@ public void onError(ServiceCommandError error) { request.send(); } - private void displayMedia(MediaInfo mediaInfo, - final MediaPlayer.LaunchListener listener) { - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - Util.postSuccess(listener, new MediaLaunchObject( - new RokuLaunchSession(RokuService.this), - RokuService.this)); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - String host = String.format("%s:%s", serviceDescription.getIpAddress(), - serviceDescription.getPort()); - - String action = "input"; - String mediaFormat = mediaInfo.getMimeType(); - if (mediaInfo.getMimeType().contains("/")) { - int index = mediaInfo.getMimeType().indexOf("/") + 1; - mediaFormat = mediaInfo.getMimeType().substring(index); - } - - String param; - if (mediaInfo.getMimeType().contains("image")) { - param = String.format("15985?t=p&u=%s&h=%s&tr=crossfade", - HttpMessage.encode(mediaInfo.getUrl()), HttpMessage.encode(host)); - } else if (mediaInfo.getMimeType().contains("video")) { - param = String.format( - "15985?t=v&u=%s&k=(null)&h=%s&videoName=%s&videoFormat=%s", - HttpMessage.encode(mediaInfo.getUrl()), HttpMessage.encode(host), - HttpMessage.encode(mediaInfo.getTitle()), HttpMessage.encode(mediaFormat)); - } else { // if (mimeType.contains("audio")) { - param = String - .format("15985?t=a&u=%s&k=(null)&h=%s&songname=%s&artistname=%s&songformat=%s&albumarturl=%s", - HttpMessage.encode(mediaInfo.getUrl()), HttpMessage.encode(host), - HttpMessage.encode(mediaInfo.getTitle()), - HttpMessage.encode(mediaInfo.getDescription()), - HttpMessage.encode(mediaFormat), - HttpMessage.encode(mediaInfo.getImages().get(0).getUrl())); - } - - String uri = requestURL(action, param); - - ServiceCommand> request = new ServiceCommand>( - this, uri, null, responseListener); - request.send(); - } - @Override public void displayImage(String url, String mimeType, String title, String description, String iconSrc, @@ -774,7 +719,10 @@ public void displayImage(String url, String mimeType, String title, @Override public void displayImage(MediaInfo mediaInfo, MediaPlayer.LaunchListener listener) { - displayMedia(mediaInfo, listener); + ImageInfo imageInfo = mediaInfo.getImages().get(0); + String iconSrc = imageInfo.getUrl(); + + displayImage(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, listener); } @Override @@ -787,7 +735,10 @@ public void playMedia(String url, String mimeType, String title, @Override public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, MediaPlayer.LaunchListener listener) { - displayMedia(mediaInfo, listener); + ImageInfo imageInfo = mediaInfo.getImages().get(0); + String iconSrc = imageInfo.getUrl(); + + playMedia(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, shouldLoop, listener); } @Override diff --git a/src/com/connectsdk/service/WebOSTVService.java b/src/com/connectsdk/service/WebOSTVService.java index 13895b0b..e20b46fd 100644 --- a/src/com/connectsdk/service/WebOSTVService.java +++ b/src/com/connectsdk/service/WebOSTVService.java @@ -46,6 +46,7 @@ import com.connectsdk.core.AppInfo; import com.connectsdk.core.ChannelInfo; import com.connectsdk.core.ExternalInputInfo; +import com.connectsdk.core.ImageInfo; import com.connectsdk.core.MediaInfo; import com.connectsdk.core.ProgramList; import com.connectsdk.core.Util; @@ -529,10 +530,7 @@ public void launchYouTube(final String contentId, float startTime, final AppLaun if (contentId != null && contentId.length() > 0) { if (startTime < 0.0) { - if (listener != null) { - listener.onError(new ServiceCommandError(0, "Start time may not be negative", null)); - } - + Util.postError(listener, new ServiceCommandError(0, "Start time may not be negative", null)); return; } @@ -1206,73 +1204,11 @@ public void onSuccess(WebAppSession webAppSession) { } @Override - public void displayImage(final MediaInfo mediaInfo, - final MediaPlayer.LaunchListener listener) { - if ("4.0.0".equalsIgnoreCase(this.serviceDescription.getVersion())) { - DeviceService dlnaService = this.getDLNAService(); - - if (dlnaService != null) { - MediaPlayer mediaPlayer = dlnaService.getAPI(MediaPlayer.class); - - if (mediaPlayer != null) { - mediaPlayer.displayImage(mediaInfo, listener); - return; - } - } - - JSONObject params = null; - - - try { - params = new JSONObject() { - { - put("target", mediaInfo.getUrl()); - put("title", mediaInfo.getTitle() == null ? NULL : mediaInfo.getTitle()); - put("description", mediaInfo.getDescription() == null ? NULL : mediaInfo.getDescription()); - put("mimeType", mediaInfo.getMimeType() == null ? NULL : mediaInfo.getMimeType()); - put("iconSrc", mediaInfo.getImages().get(0).getUrl() == null ? NULL : mediaInfo.getImages().get(0).getUrl()); - - }}; - } catch (JSONException ex) { - ex.printStackTrace(); - Util.postError(listener, new ServiceCommandError(-1, ex.getLocalizedMessage(), ex)); - } - - if (params != null) - this.displayMedia(params, listener); - } else { - final String webAppId = "MediaPlayer"; - - final WebAppSession.LaunchListener webAppLaunchListener = new WebAppSession.LaunchListener() { - - - - @Override - public void onError(ServiceCommandError error) { - listener.onError(error); - } - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.displayImage(mediaInfo, listener); - } - }; - - this.getWebAppLauncher().joinWebApp(webAppId, new WebAppSession.LaunchListener() { - - - - @Override - public void onError(ServiceCommandError error) { - getWebAppLauncher().launchWebApp(webAppId, webAppLaunchListener); - } - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.displayImage(mediaInfo, listener); - } - }); - } + public void displayImage(MediaInfo mediaInfo, MediaPlayer.LaunchListener listener) { + ImageInfo imageInfo = mediaInfo.getImages().get(0); + String iconSrc = imageInfo.getUrl(); + + displayImage(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, listener); } @Override @@ -1339,75 +1275,11 @@ public void onSuccess(WebAppSession webAppSession) { } @Override - public void playMedia(final MediaInfo mediaInfo, - final boolean shouldLoop, final MediaPlayer.LaunchListener listener) { - if ("4.0.0".equalsIgnoreCase(this.serviceDescription.getVersion())) { - DeviceService dlnaService = this.getDLNAService(); - - if (dlnaService != null) { - MediaPlayer mediaPlayer = dlnaService.getAPI(MediaPlayer.class); - - if (mediaPlayer != null) { - mediaPlayer.playMedia(mediaInfo, shouldLoop, listener); - return; - } - } - - JSONObject params = null; - - try { - params = new JSONObject() { - { - put("target", mediaInfo.getUrl()); - put("title", mediaInfo.getTitle() == null ? NULL : mediaInfo.getTitle()); - put("description", mediaInfo.getDescription() == null ? NULL - : mediaInfo.getDescription()); - put("mimeType", mediaInfo.getMimeType() == null ? NULL : mediaInfo.getMimeType()); - put("iconSrc", mediaInfo.getImages().get(0).getUrl() == null ? NULL : mediaInfo.getImages().get(0).getUrl()); - put("posterSrc", mediaInfo.getImages().get(1).getUrl() == null ? NULL : mediaInfo.getImages().get(1).getUrl()); - put("loop", shouldLoop); - } - }; - } catch (JSONException ex) { - ex.printStackTrace(); - Util.postError(listener, - new ServiceCommandError(-1, ex.getLocalizedMessage(), - ex)); - } - - if (params != null) - this.displayMedia(params, listener); - } else { - final String webAppId = "MediaPlayer"; - - final WebAppSession.LaunchListener webAppLaunchListener = new WebAppSession.LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - listener.onError(error); - } - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.playMedia(mediaInfo, shouldLoop, listener); - } - }; - - this.getWebAppLauncher().joinWebApp(webAppId, - new WebAppSession.LaunchListener() { - - @Override - public void onError(ServiceCommandError error) { - getWebAppLauncher().launchWebApp(webAppId, - webAppLaunchListener); - } - - @Override - public void onSuccess(WebAppSession webAppSession) { - webAppSession.playMedia(mediaInfo, shouldLoop, listener); - } - }); - } + public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, MediaPlayer.LaunchListener listener) { + ImageInfo imageInfo = mediaInfo.getImages().get(0); + String iconSrc = imageInfo.getUrl(); + + playMedia(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, shouldLoop, listener); } @Override @@ -2030,8 +1902,7 @@ public void onError(ServiceCommandError error) { @Override public void powerOn(ResponseListener listener) { - if (listener != null) - listener.onError(ServiceCommandError.notSupported()); + Util.postError(listener, ServiceCommandError.notSupported()); } @@ -2284,16 +2155,14 @@ public void closeWebApp(LaunchSession launchSession, final ResponseListener Date: Wed, 20 Aug 2014 14:20:55 +0900 Subject: [PATCH 21/76] Fixed capability levels --- src/com/connectsdk/service/AirPlayService.java | 4 ++-- src/com/connectsdk/service/RokuService.java | 8 ++++---- src/com/connectsdk/service/WebOSTVService.java | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/com/connectsdk/service/AirPlayService.java b/src/com/connectsdk/service/AirPlayService.java index 6cb4e62f..8e599cc1 100644 --- a/src/com/connectsdk/service/AirPlayService.java +++ b/src/com/connectsdk/service/AirPlayService.java @@ -110,7 +110,7 @@ public MediaControl getMediaControl() { @Override public CapabilityPriorityLevel getMediaControlCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; + return CapabilityPriorityLevel.HIGH; } @Override @@ -296,7 +296,7 @@ public MediaPlayer getMediaPlayer() { @Override public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; + return CapabilityPriorityLevel.HIGH; } @Override diff --git a/src/com/connectsdk/service/RokuService.java b/src/com/connectsdk/service/RokuService.java index 6a9bdf94..41df5c29 100644 --- a/src/com/connectsdk/service/RokuService.java +++ b/src/com/connectsdk/service/RokuService.java @@ -473,7 +473,7 @@ public KeyControl getKeyControl() { @Override public CapabilityPriorityLevel getKeyControlCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; + return CapabilityPriorityLevel.HIGH; } @Override @@ -567,7 +567,7 @@ public MediaControl getMediaControl() { @Override public CapabilityPriorityLevel getMediaControlCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; + return CapabilityPriorityLevel.HIGH; } @Override @@ -652,7 +652,7 @@ public MediaPlayer getMediaPlayer() { @Override public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; + return CapabilityPriorityLevel.HIGH; } private void displayMedia(String url, String mimeType, String title, @@ -754,7 +754,7 @@ public TextInputControl getTextInputControl() { @Override public CapabilityPriorityLevel getTextInputControlCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; + return CapabilityPriorityLevel.HIGH; } @Override diff --git a/src/com/connectsdk/service/WebOSTVService.java b/src/com/connectsdk/service/WebOSTVService.java index e20b46fd..f6a4811b 100644 --- a/src/com/connectsdk/service/WebOSTVService.java +++ b/src/com/connectsdk/service/WebOSTVService.java @@ -1310,7 +1310,7 @@ public MediaControl getMediaControl() { @Override public CapabilityPriorityLevel getMediaControlCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; + return CapabilityPriorityLevel.HIGH; } @Override From fd9fa7c47eb75af3725e4112c536cd11c546520a Mon Sep 17 00:00:00 2001 From: simongladkoskok Date: Wed, 20 Aug 2014 15:11:35 -0700 Subject: [PATCH 22/76] Update README.md --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3568329e..827cee5b 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,11 @@ This project has the following dependencies. 11. Follow the setup instructions for each of the service submodules - [Connect-SDK-Android-Google-Cast](https://github.com/ConnectSDK/Connect-SDK-Android-Google-Cast) - [Connect-SDK-Android-Samsung-MultiScreen](https://github.com/ConnectSDK/Connect-SDK-Android-Samsung-MultiScreen) -12. Right-click the Connect-SDK-Android project and select Properties -13. In the Library pane of the Android tab, add Connect-SDK-Android-Core, Connect-SDK-Android-Google-Cast, and Connect-SDK-Android-Samsung-MultiScreen projects -14. Set up your manifest file as per the instructions below +12. Right-click the Connect-SDK-Android-Core project and select Properties, in the Library pane of the Android tab, add Connect-SDK-Android +13. Right-click the Connect-SDK-Android-Google-Cast project and select Properties, in the Library pane of the Android tab, add Connect-SDK-Android-Core +14. Right-click the Connect-SDK-Android-Samsung-MultiScreen project and select Properties, in the Library pane of the Android tab, add Connect-SDK-Android-Core +15. In your project select Properties, in the Library pane of the Android tab, add Connect-SDK-Android-Core, Connect-SDK-Android-Google-Cast, and Connect-SDK-Android-Samsung-MultiScreen +15. Set up your manifest file as per the instructions below ###Permissions to include in manifest * Required for SSDP & Chromecast/Zeroconf discovery From abecc16cf41edc0c2a5b53929d2b0e432790847f Mon Sep 17 00:00:00 2001 From: Simon Gladkoskok Date: Thu, 21 Aug 2014 14:25:53 -0700 Subject: [PATCH 23/76] fixed bug with NullPointerExeption --- src/com/connectsdk/discovery/DiscoveryManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/connectsdk/discovery/DiscoveryManager.java b/src/com/connectsdk/discovery/DiscoveryManager.java index 9575a383..05110aed 100644 --- a/src/com/connectsdk/discovery/DiscoveryManager.java +++ b/src/com/connectsdk/discovery/DiscoveryManager.java @@ -850,8 +850,8 @@ public void addServiceDescriptionToDevice(ServiceDescription desc, ConnectableDe } DeviceService deviceService = DeviceService.getService(deviceServiceClass, desc, serviceConfig); - deviceService.setServiceDescription(desc); - device.addService(deviceService); + if (deviceService!=null) {deviceService.setServiceDescription(desc); + device.addService(deviceService);} } // @endcond } From 3e9bc82c308192ff25e09766afd6262097ab0704 Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Fri, 22 Aug 2014 15:35:49 +0900 Subject: [PATCH 24/76] Added extra blank line to present end of packet data --- src/com/connectsdk/device/netcast/NetcastHttpServer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/com/connectsdk/device/netcast/NetcastHttpServer.java b/src/com/connectsdk/device/netcast/NetcastHttpServer.java index b0dbc89b..6a85788a 100644 --- a/src/com/connectsdk/device/netcast/NetcastHttpServer.java +++ b/src/com/connectsdk/device/netcast/NetcastHttpServer.java @@ -148,6 +148,7 @@ public void start() { out.println("Date: " + date); out.println("Connection: Close"); out.println("Content-Length: 0"); + out.println(); out.flush(); } catch (IOException ex) { ex.printStackTrace(); From 3813067517d66983f9c463dd90a54bcbb3911473 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Sun, 24 Aug 2014 22:08:50 -0700 Subject: [PATCH 25/76] Added license info to headers of new files --- src/com/connectsdk/core/ImageInfo.java | 20 ++++++++++++++++++++ src/com/connectsdk/core/MediaInfo.java | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/com/connectsdk/core/ImageInfo.java b/src/com/connectsdk/core/ImageInfo.java index 3e368bed..494ba0ad 100644 --- a/src/com/connectsdk/core/ImageInfo.java +++ b/src/com/connectsdk/core/ImageInfo.java @@ -1,3 +1,23 @@ +/* + * ImageInfo + * Connect SDK + * + * Copyright (c) 2014 LG Electronics. + * Created by Simon Gladkoskok on 14 August 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.core; /** diff --git a/src/com/connectsdk/core/MediaInfo.java b/src/com/connectsdk/core/MediaInfo.java index 1b29f030..73fe935a 100644 --- a/src/com/connectsdk/core/MediaInfo.java +++ b/src/com/connectsdk/core/MediaInfo.java @@ -1,3 +1,23 @@ +/* + * MediaInfo + * Connect SDK + * + * Copyright (c) 2014 LG Electronics. + * Created by Simon Gladkoskok on 14 August 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.core; import java.util.ArrayList; From 3d8000b61e5b79eed7a8bb3b24294da1cca8ba53 Mon Sep 17 00:00:00 2001 From: Simon Gladkoskok Date: Mon, 25 Aug 2014 15:01:49 -0700 Subject: [PATCH 26/76] add displayImage and playMedia which using mediaInfo object --- .../sessions/MultiScreenWebAppSession.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/com/connectsdk/service/sessions/MultiScreenWebAppSession.java b/src/com/connectsdk/service/sessions/MultiScreenWebAppSession.java index 05e431cc..ab88a8cf 100644 --- a/src/com/connectsdk/service/sessions/MultiScreenWebAppSession.java +++ b/src/com/connectsdk/service/sessions/MultiScreenWebAppSession.java @@ -6,6 +6,7 @@ import org.json.JSONException; import org.json.JSONObject; +import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; import com.connectsdk.service.DeviceService; import com.connectsdk.service.MultiScreenService; @@ -404,6 +405,15 @@ public void onError(ServiceCommandError error) { } }); } + + + + @Override + public void displayImage(MediaInfo mediaInfo, MediaPlayer.LaunchListener listener) { + + displayImage(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), mediaInfo.getImages().get(0).getUrl(), listener); + + } @Override public void playMedia(String url, String mimeType, String title, @@ -465,6 +475,16 @@ public void onError(ServiceCommandError error) { } }); } + + + + @Override + public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, + MediaPlayer.LaunchListener listener) { + + playMedia(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), mediaInfo.getImages().get(0).getUrl(), shouldLoop, listener); + + } @Override public void closeMedia(LaunchSession launchSession, ResponseListener listener) { From cc81eeb5fae3074dc74ba3195d1013ba723ea189 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Mon, 25 Aug 2014 21:19:26 -0700 Subject: [PATCH 27/76] Removed unnecessary semicolon --- src/com/connectsdk/service/NetcastTVService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/connectsdk/service/NetcastTVService.java b/src/com/connectsdk/service/NetcastTVService.java index 6dc67a47..06592d14 100644 --- a/src/com/connectsdk/service/NetcastTVService.java +++ b/src/com/connectsdk/service/NetcastTVService.java @@ -1534,7 +1534,7 @@ public MediaControl getMediaControl() { return this.getDLNAService(); else return this; - }; + } @Override public CapabilityPriorityLevel getMediaControlCapabilityLevel() { From e63011a648e056fe281bc5fca60324c67858fa63 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Mon, 25 Aug 2014 21:20:38 -0700 Subject: [PATCH 28/76] Added MediaControl.previous and MediaControl.next Only DLNAService & NetcastTVService support these for now --- .../connectsdk/service/AirPlayService.java | 12 ++++++- src/com/connectsdk/service/CastService.java | 14 ++++++-- src/com/connectsdk/service/DLNAService.java | 32 ++++++++++++++++--- .../connectsdk/service/NetcastTVService.java | 14 ++++++-- src/com/connectsdk/service/RokuService.java | 12 ++++++- .../connectsdk/service/WebOSTVService.java | 21 ++++++++++-- .../service/capability/MediaControl.java | 9 +++++- .../service/sessions/WebAppSession.java | 26 +++++++++++++++ 8 files changed, 125 insertions(+), 15 deletions(-) diff --git a/src/com/connectsdk/service/AirPlayService.java b/src/com/connectsdk/service/AirPlayService.java index 8e599cc1..a1984a0c 100644 --- a/src/com/connectsdk/service/AirPlayService.java +++ b/src/com/connectsdk/service/AirPlayService.java @@ -167,7 +167,17 @@ public void fastForward(ResponseListener listener) { request.send(); } - @Override + @Override + public void previous(ResponseListener listener) { + Util.postError(listener, ServiceCommandError.notSupported()); + } + + @Override + public void next(ResponseListener listener) { + Util.postError(listener, ServiceCommandError.notSupported()); + } + + @Override public void seek(long position, ResponseListener listener) { float pos = ((float) position / 1000); diff --git a/src/com/connectsdk/service/CastService.java b/src/com/connectsdk/service/CastService.java index 59fd22b0..03137d13 100644 --- a/src/com/connectsdk/service/CastService.java +++ b/src/com/connectsdk/service/CastService.java @@ -235,8 +235,18 @@ public void rewind(ResponseListener listener) { public void fastForward(ResponseListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); } - - @Override + + @Override + public void previous(ResponseListener listener) { + Util.postError(listener, ServiceCommandError.notSupported()); + } + + @Override + public void next(ResponseListener listener) { + Util.postError(listener, ServiceCommandError.notSupported()); + } + + @Override public void seek(long position, final ResponseListener listener) { if (mMediaPlayer.getMediaStatus() == null) { Util.postError(listener, new ServiceCommandError(0, "There is no media currently available", null)); diff --git a/src/com/connectsdk/service/DLNAService.java b/src/com/connectsdk/service/DLNAService.java index 03833b1b..fe972191 100644 --- a/src/com/connectsdk/service/DLNAService.java +++ b/src/com/connectsdk/service/DLNAService.java @@ -340,15 +340,37 @@ public void stop(ResponseListener listener) { ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); request.send(); } - + + @Override + public void rewind(ResponseListener listener) { + Util.postError(listener, ServiceCommandError.notSupported()); + } + + @Override + public void fastForward(ResponseListener listener) { + Util.postError(listener, ServiceCommandError.notSupported()); + } + @Override - public void rewind(ResponseListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); + public void previous(ResponseListener listener) { + String method = "Previous"; + String instanceId = "0"; + + JSONObject payload = getMethodBody(instanceId, method); + + ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); + request.send(); } @Override - public void fastForward(ResponseListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); + public void next(ResponseListener listener) { + String method = "Next"; + String instanceId = "0"; + + JSONObject payload = getMethodBody(instanceId, method); + + ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); + request.send(); } @Override diff --git a/src/com/connectsdk/service/NetcastTVService.java b/src/com/connectsdk/service/NetcastTVService.java index 06592d14..11a8443f 100644 --- a/src/com/connectsdk/service/NetcastTVService.java +++ b/src/com/connectsdk/service/NetcastTVService.java @@ -1565,8 +1565,18 @@ public void rewind(ResponseListener listener) { public void fastForward(ResponseListener listener) { sendKeyCode(VirtualKeycodes.FAST_FORWARD.getCode(), listener); } - - @Override + + @Override + public void previous(ResponseListener listener) { + sendKeyCode(VirtualKeycodes.SKIP_BACKWARD.getCode(), listener); + } + + @Override + public void next(ResponseListener listener) { + sendKeyCode(VirtualKeycodes.SKIP_FORWARD.getCode(), listener); + } + + @Override public void seek(long position, ResponseListener listener) { if (getDLNAService() != null) { getDLNAService().seek(position, listener); diff --git a/src/com/connectsdk/service/RokuService.java b/src/com/connectsdk/service/RokuService.java index 41df5c29..8f5f4cff 100644 --- a/src/com/connectsdk/service/RokuService.java +++ b/src/com/connectsdk/service/RokuService.java @@ -630,7 +630,17 @@ public void fastForward(ResponseListener listener) { request.send(); } - @Override + @Override + public void previous(ResponseListener listener) { + Util.postError(listener, ServiceCommandError.notSupported()); + } + + @Override + public void next(ResponseListener listener) { + Util.postError(listener, ServiceCommandError.notSupported()); + } + + @Override public void getDuration(DurationListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); } diff --git a/src/com/connectsdk/service/WebOSTVService.java b/src/com/connectsdk/service/WebOSTVService.java index f6a4811b..d6fd7064 100644 --- a/src/com/connectsdk/service/WebOSTVService.java +++ b/src/com/connectsdk/service/WebOSTVService.java @@ -1352,8 +1352,18 @@ public void fastForward(ResponseListener listener) { request.send(); } - - @Override + + @Override + public void previous(ResponseListener listener) { + Util.postError(listener, ServiceCommandError.notSupported()); + } + + @Override + public void next(ResponseListener listener) { + Util.postError(listener, ServiceCommandError.notSupported()); + } + + @Override public void seek(long position, ResponseListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); } @@ -2623,7 +2633,12 @@ protected void updateCapabilities() { capabilities.add(WebAppLauncher.Close); } else { for (String capability : WebAppLauncher.Capabilities) { capabilities.add(capability); } - for (String capability : MediaControl.Capabilities) { capabilities.add(capability); } + for (String capability : MediaControl.Capabilities) { + if (capability.equalsIgnoreCase(MediaControl.Previous) || capability.equalsIgnoreCase(MediaControl.Next)) + continue; + + capabilities.add(capability); + } } } diff --git a/src/com/connectsdk/service/capability/MediaControl.java b/src/com/connectsdk/service/capability/MediaControl.java index 2a671155..b26cff40 100644 --- a/src/com/connectsdk/service/capability/MediaControl.java +++ b/src/com/connectsdk/service/capability/MediaControl.java @@ -31,7 +31,9 @@ public interface MediaControl extends CapabilityMethods { public final static String Stop = "MediaControl.Stop"; public final static String Rewind = "MediaControl.Rewind"; public final static String FastForward = "MediaControl.FastForward"; - public final static String Seek = "MediaControl.Seek"; + public final static String Seek = "MediaControl.Seek"; + public final static String Previous = "MediaControl.Previous"; + public final static String Next = "MediaControl.Next"; public final static String Duration = "MediaControl.Duration"; public final static String PlayState = "MediaControl.PlayState"; public final static String PlayState_Subscribe = "MediaControl.PlayState.Subscribe"; @@ -44,6 +46,8 @@ public interface MediaControl extends CapabilityMethods { Rewind, FastForward, Seek, + Previous, + Next, Duration, PlayState, PlayState_Subscribe, @@ -68,6 +72,9 @@ public enum PlayStateStatus { public void rewind(ResponseListener listener); public void fastForward(ResponseListener listener); + public void previous(ResponseListener listener); + public void next(ResponseListener listener); + public void seek(long position, ResponseListener listener); public void getDuration(DurationListener listener); public void getPosition(PositionListener listener); diff --git a/src/com/connectsdk/service/sessions/WebAppSession.java b/src/com/connectsdk/service/sessions/WebAppSession.java index ef8565aa..6321127e 100644 --- a/src/com/connectsdk/service/sessions/WebAppSession.java +++ b/src/com/connectsdk/service/sessions/WebAppSession.java @@ -248,6 +248,32 @@ else if (listener != null) listener.onError(ServiceCommandError.notSupported()); } + @Override + public void previous(ResponseListener listener) { + MediaControl mediaControl = null; + + if (service != null) + mediaControl = service.getAPI(MediaControl.class); + + if (mediaControl != null) + mediaControl.previous(listener); + else if (listener != null) + listener.onError(ServiceCommandError.notSupported()); + } + + @Override + public void next(ResponseListener listener) { + MediaControl mediaControl = null; + + if (service != null) + mediaControl = service.getAPI(MediaControl.class); + + if (mediaControl != null) + mediaControl.next(listener); + else if (listener != null) + listener.onError(ServiceCommandError.notSupported()); + } + @Override public void seek(long position, ResponseListener listener) { MediaControl mediaControl = null; From aeb9568e36a9b2061207be50d06ea23bd3979ce5 Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Fri, 29 Aug 2014 09:07:29 +0900 Subject: [PATCH 29/76] Added log messages for debug --- src/com/connectsdk/service/CastService.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/com/connectsdk/service/CastService.java b/src/com/connectsdk/service/CastService.java index 03137d13..523c71b1 100644 --- a/src/com/connectsdk/service/CastService.java +++ b/src/com/connectsdk/service/CastService.java @@ -800,11 +800,22 @@ public void onConnected(Bundle connectionHint) { } private void reconnectChannels() { + if (mApiClient == null) { + Log.d(TAG, "GoogleApiClient is null"); + } + else if (mApiClient.isConnected() == false) { + Log.d(TAG, "GoogleApiClient is not connected"); + } + if (Cast.CastApi.getApplicationStatus(mApiClient) != null && currentAppId != null) { CastWebAppSession webAppSession = sessions.get(currentAppId); webAppSession.connect(null); } + else { + Log.d(TAG, "Application Status: " + Cast.CastApi.getApplicationStatus(mApiClient)); + Log.d(TAG, "Current App Id: " + currentAppId); + } } } From db028f5ac60814c776ca5337150c0d6fef95d74a Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Wed, 3 Sep 2014 23:15:17 -0700 Subject: [PATCH 30/76] Changed some DLNA helper methods to protected for extensibility --- src/com/connectsdk/service/DLNAService.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/com/connectsdk/service/DLNAService.java b/src/com/connectsdk/service/DLNAService.java index fe972191..37d78e96 100644 --- a/src/com/connectsdk/service/DLNAService.java +++ b/src/com/connectsdk/service/DLNAService.java @@ -461,7 +461,7 @@ public void onGetPositionInfoFailed(ServiceCommandError error) { }); } - private JSONObject getSetAVTransportURIBody(String method, String instanceId, String mediaURL, String mime, String title) { + protected JSONObject getSetAVTransportURIBody(String method, String instanceId, String mediaURL, String mime, String title) { String action = "SetAVTransportURI"; String metadata = getMetadata(mediaURL, mime, title); @@ -490,11 +490,11 @@ private JSONObject getSetAVTransportURIBody(String method, String instanceId, St return obj; } - private JSONObject getMethodBody(String instanceId, String method) { + protected JSONObject getMethodBody(String instanceId, String method) { return getMethodBody(instanceId, method, null); } - - private JSONObject getMethodBody(String instanceId, String method, Map parameters) { + + protected JSONObject getMethodBody(String instanceId, String method, Map parameters) { StringBuilder sb = new StringBuilder(); sb.append(""); @@ -529,8 +529,8 @@ private JSONObject getMethodBody(String instanceId, String method, Map Date: Fri, 5 Sep 2014 16:19:59 -0700 Subject: [PATCH 31/76] Added DLNA event Subscriptions (Currently, we only handle AVTransport event) and support all generic DLNA devices (not only for LG) --- .../connectsdk/core/upnp/DLNAHttpServer.java | 191 ++++++++++++++ .../core/upnp/DLNANotifyParser.java | 55 ++++ .../discovery/DiscoveryManager.java | 4 - .../provider/SSDPDiscoveryProvider.java | 1 + src/com/connectsdk/service/CastService.java | 29 +- src/com/connectsdk/service/DLNAService.java | 248 ++++++++++++++++-- .../service/capability/MediaControl.java | 55 +++- 7 files changed, 535 insertions(+), 48 deletions(-) create mode 100644 src/com/connectsdk/core/upnp/DLNAHttpServer.java create mode 100644 src/com/connectsdk/core/upnp/DLNANotifyParser.java diff --git a/src/com/connectsdk/core/upnp/DLNAHttpServer.java b/src/com/connectsdk/core/upnp/DLNAHttpServer.java new file mode 100644 index 00000000..02cf0ef7 --- /dev/null +++ b/src/com/connectsdk/core/upnp/DLNAHttpServer.java @@ -0,0 +1,191 @@ +package com.connectsdk.core.upnp; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONException; +import org.json.JSONObject; +import org.xmlpull.v1.XmlPullParserException; + +import android.text.Html; + +import com.connectsdk.core.Util; +import com.connectsdk.service.capability.MediaControl.PlayStateStatus; +import com.connectsdk.service.capability.listeners.ResponseListener; +import com.connectsdk.service.command.URLServiceSubscription; + +public class DLNAHttpServer { + ServerSocket welcomeSocket; + + int port = 49291; + + boolean running = false; + + List> subscriptions; + + public DLNAHttpServer() { + subscriptions = new ArrayList>(); + } + + public void start() { + if (running) + return; + + running = true; + + try { + welcomeSocket = new ServerSocket(this.port); + } catch (IOException ex) { + ex.printStackTrace(); + } + + while (running) { + if (welcomeSocket == null || welcomeSocket.isClosed()) { + stop(); + break; + } + + Socket connectionSocket = null; + BufferedReader inFromClient = null; + DataOutputStream outToClient = null; + + try { + connectionSocket = welcomeSocket.accept(); + } catch (IOException ex) { + ex.printStackTrace(); + // this socket may have been closed, so we'll stop + stop(); + return; + } + + int c = 0; + + String body = null; + + try { + inFromClient = new BufferedReader(new InputStreamReader(connectionSocket.getInputStream())); + + StringBuilder sb = new StringBuilder(); + + while ((c = inFromClient.read()) != -1) { + sb.append((char)c); + + if (sb.toString().endsWith("\r\n\r\n")) + break; + } + + sb = new StringBuilder(); + + while ((c = inFromClient.read()) != -1) { + sb.append((char)c); + body = sb.toString(); + + if (body.endsWith("")) + break; + } + + body = Html.fromHtml(body).toString(); + } catch (IOException ex) { + ex.printStackTrace(); + } + + PrintWriter out = null; + + try { + outToClient = new DataOutputStream(connectionSocket.getOutputStream()); + out = new PrintWriter(outToClient); + out.println("HTTP/1.1 200 OK"); + out.println("Connection: Close"); + out.println("Content-Length: 0"); + out.println(); + out.flush(); + } catch (IOException ex) { + ex.printStackTrace(); + } finally { + try { + inFromClient.close(); + out.close(); + outToClient.close(); + connectionSocket.close(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + InputStream stream = null; + + try { + stream = new ByteArrayInputStream(body.getBytes("UTF-8")); + } catch (UnsupportedEncodingException ex) { + ex.printStackTrace(); + } + + JSONObject event; + DLNANotifyParser parser = new DLNANotifyParser(); + + try { + event = parser.parse(stream); + + if (!event.isNull("TransportState")) { + String transportState = event.getString("TransportState"); + PlayStateStatus status = PlayStateStatus.convertTransportStateToPlayStateStatus(transportState); + + for (URLServiceSubscription sub: subscriptions) { + if (sub.getTarget().equalsIgnoreCase("playState")) { + for (int i = 0; i < sub.getListeners().size(); i++) { + @SuppressWarnings("unchecked") + ResponseListener listener = (ResponseListener) sub.getListeners().get(i); + Util.postSuccess(listener, status); + } + } + } + } + } catch (XmlPullParserException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (JSONException e) { + e.printStackTrace(); + } + } + } + + + public void stop() { + if (!running) + return; + + if (welcomeSocket != null && !welcomeSocket.isClosed()) { + try { + welcomeSocket.close(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + welcomeSocket = null; + running = false; + } + + public int getPort() { + return port; + } + + public List> getSubscriptions() { + return subscriptions; + } + + public void setSubscriptions(List> subscriptions) { + this.subscriptions = subscriptions; + } +} diff --git a/src/com/connectsdk/core/upnp/DLNANotifyParser.java b/src/com/connectsdk/core/upnp/DLNANotifyParser.java new file mode 100644 index 00000000..141d7518 --- /dev/null +++ b/src/com/connectsdk/core/upnp/DLNANotifyParser.java @@ -0,0 +1,55 @@ +package com.connectsdk.core.upnp; + +import java.io.IOException; +import java.io.InputStream; + +import org.json.JSONException; +import org.json.JSONObject; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.util.Xml; + +public class DLNANotifyParser { + private static final String ns = null; + + public JSONObject parse(InputStream in) throws XmlPullParserException, IOException, JSONException { + try { + XmlPullParser parser = Xml.newPullParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); + parser.setInput(in, null); + parser.nextTag(); + return readEvent(parser); + } finally { + in.close(); + } + } + + public JSONObject readEvent(XmlPullParser parser) throws IOException, XmlPullParserException, JSONException { + JSONObject event = new JSONObject(); + + parser.require(XmlPullParser.START_TAG, ns, "Event"); + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + String name = parser.getName(); + if (name.equals("Event")) { + } + else if (name.equals("InstanceID")) { + } + else { + event.put(name, readEventValue(name, parser)); + } + } + return event; + } + + private String readEventValue(String target, XmlPullParser parser) throws IOException, XmlPullParserException { + parser.require(XmlPullParser.START_TAG, ns, target); + String value = parser.getAttributeValue(null, "val"); + parser.nextTag(); + parser.require(XmlPullParser.END_TAG, ns, target); + return value; + } +} diff --git a/src/com/connectsdk/discovery/DiscoveryManager.java b/src/com/connectsdk/discovery/DiscoveryManager.java index 05110aed..470cf4b8 100644 --- a/src/com/connectsdk/discovery/DiscoveryManager.java +++ b/src/com/connectsdk/discovery/DiscoveryManager.java @@ -796,10 +796,6 @@ public void addServiceDescriptionToDevice(ServiceDescription desc, ConnectableDe if (deviceServiceClass == DLNAService.class) { if (desc.getLocationXML() == null) return; - - // we only support LG DLNA devices, currently - if (!desc.getLocationXML().contains("LG")) - return; } else if (deviceServiceClass == NetcastTVService.class) { if (!isNetcast(desc)) return; diff --git a/src/com/connectsdk/discovery/provider/SSDPDiscoveryProvider.java b/src/com/connectsdk/discovery/provider/SSDPDiscoveryProvider.java index 78a13ca4..4858e7dd 100644 --- a/src/com/connectsdk/discovery/provider/SSDPDiscoveryProvider.java +++ b/src/com/connectsdk/discovery/provider/SSDPDiscoveryProvider.java @@ -407,6 +407,7 @@ public void run() { service.setResponseHeaders(device.headers); service.setLocationXML(device.locationXML); service.setServiceURI(device.serviceURI); + service.setPort(device.port); foundServices.put(uuid, service); diff --git a/src/com/connectsdk/service/CastService.java b/src/com/connectsdk/service/CastService.java index 523c71b1..bf6e0ef2 100644 --- a/src/com/connectsdk/service/CastService.java +++ b/src/com/connectsdk/service/CastService.java @@ -315,7 +315,7 @@ public void onStatusUpdated() { for (int i = 0; i < subscription.getListeners().size(); i++) { @SuppressWarnings("unchecked") ResponseListener listener = (ResponseListener) subscription.getListeners().get(i); - PlayStateStatus status = convertPlayerStateToPlayStateStatus(mMediaPlayer.getMediaStatus().getPlayerState()); + PlayStateStatus status = PlayStateStatus.convertPlayerStateToPlayStateStatus(mMediaPlayer.getMediaStatus().getPlayerState()); Util.postSuccess(listener, status); } } @@ -890,35 +890,10 @@ public void getPlayState(PlayStateListener listener) { return; } - PlayStateStatus status = convertPlayerStateToPlayStateStatus(mMediaPlayer.getMediaStatus().getPlayerState()); + PlayStateStatus status = PlayStateStatus.convertPlayerStateToPlayStateStatus(mMediaPlayer.getMediaStatus().getPlayerState()); Util.postSuccess(listener, status); } - private PlayStateStatus convertPlayerStateToPlayStateStatus(int playerState) { - PlayStateStatus status = PlayStateStatus.Unknown; - - switch (playerState) { - case MediaStatus.PLAYER_STATE_BUFFERING: - status = PlayStateStatus.Buffering; - break; - case MediaStatus.PLAYER_STATE_IDLE: - status = PlayStateStatus.Idle; - break; - case MediaStatus.PLAYER_STATE_PAUSED: - status = PlayStateStatus.Paused; - break; - case MediaStatus.PLAYER_STATE_PLAYING: - status = PlayStateStatus.Playing; - break; - case MediaStatus.PLAYER_STATE_UNKNOWN: - default: - status = PlayStateStatus.Unknown; - break; - } - - return status; - } - public GoogleApiClient getApiClient() { return mApiClient; } diff --git a/src/com/connectsdk/service/DLNAService.java b/src/com/connectsdk/service/DLNAService.java index 37d78e96..ea1a2035 100644 --- a/src/com/connectsdk/service/DLNAService.java +++ b/src/com/connectsdk/service/DLNAService.java @@ -22,13 +22,17 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; @@ -37,15 +41,20 @@ import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.message.BasicHttpRequest; import org.apache.http.params.HttpParams; import org.apache.http.util.EntityUtils; import org.json.JSONException; import org.json.JSONObject; +import android.content.Context; + import com.connectsdk.core.ImageInfo; import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; +import com.connectsdk.core.upnp.DLNAHttpServer; import com.connectsdk.core.upnp.service.Service; +import com.connectsdk.discovery.DiscoveryManager; import com.connectsdk.etc.helper.DeviceServiceReachability; import com.connectsdk.etc.helper.HttpMessage; import com.connectsdk.service.capability.MediaControl; @@ -54,21 +63,39 @@ 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; public class DLNAService extends DeviceService implements MediaControl, MediaPlayer { - public static final String ID = "DLNA"; + private static final String SUBSCRIBE = "SUBSCRIBE"; + private static final String UNSUBSCRIBE = "UNSUBSCRIBE"; + private static final String DATA = "XMLData"; private static final String ACTION = "SOAPAction"; private static final String ACTION_CONTENT = "\"urn:schemas-upnp-org:service:AVTransport:1#%s\""; + + private static final String AV_TRANSPORT = "AVTransport"; + private static final String CONNECTION_MANAGER = "ConnectionManager"; + private static final String RENDERING_CONTROL = "RenderingControl"; + + public final static String PLAY_STATE = "playState"; + + Context context; - String controlURL; + String controlURL; HttpClient httpClient; + + DLNAHttpServer httpServer; + + Map SIDList; + Timer resubscriptionTimer; + + private static int TIMEOUT = 300; interface PositionInfoListener { public void onGetPositionInfoSuccess(String positionInfoXml); @@ -83,6 +110,11 @@ public DLNAService(ServiceDescription serviceDescription, ServiceConfig serviceC HttpParams params = httpClient.getParams(); httpClient = new DefaultHttpClient(new ThreadSafeClientConnManager(params, mgr.getSchemeRegistry()), params); + context = DiscoveryManager.getInstance().getContext(); + + SIDList = new HashMap(); + resubscriptionTimer = new Timer(); + updateControlURL(); } @@ -110,9 +142,9 @@ private void updateControlURL() { StringBuilder sb = new StringBuilder(); List serviceList = serviceDescription.getServiceList(); - if ( serviceList != null ) { - for ( int i = 0; i < serviceList.size(); i++) { - if ( serviceList.get(i).serviceType.contains("AVTransport") ) { + if (serviceList != null) { + for (int i = 0; i < serviceList.size(); i++) { + if (serviceList.get(i).serviceType.contains(AV_TRANSPORT)) { sb.append(serviceList.get(i).baseURL); sb.append(serviceList.get(i).controlURL); break; @@ -265,7 +297,7 @@ public void playMedia(String url, String mimeType, String title, String descript // // @Override // public void onError(ServiceCommandError error) { -// if ( listener != null ) { +// if (listener != null) { // listener.onError(error); // } // } @@ -544,13 +576,13 @@ protected String getMetadata(String mediaURL, String mime, String title) { sb.append("<item id="" + id + "" parentID="" + parentID + "" restricted="" + restricted + "">"); sb.append("<dc:title>" + title + "</dc:title>"); - if ( mime.startsWith("image") ) { + if (mime.startsWith("image")) { objectClass = "object.item.imageItem"; } - else if ( mime.startsWith("video") ) { + else if (mime.startsWith("video")) { objectClass = "object.item.videoItem"; } - else if ( mime.startsWith("audio") ) { + else if (mime.startsWith("audio")) { objectClass = "object.item.audioItem"; } sb.append("<res protocolInfo="http-get:*:" + mime + ":DLNA.ORG_OP=01">" + mediaURL + "</res>"); @@ -586,11 +618,11 @@ public void run() { try { response = httpClient.execute(request); - final int code = response.getStatusLine().getStatusCode(); + int code = response.getStatusLine().getStatusCode(); if (code == 200) { HttpEntity entity = response.getEntity(); - final String message = EntityUtils.toString(entity, "UTF-8"); + String message = EntityUtils.toString(entity, "UTF-8"); Util.postSuccess(command.getResponseListener(), message); } @@ -627,6 +659,7 @@ protected void updateCapabilities() { capabilities.add(Position); capabilities.add(Duration); capabilities.add(PlayState); + capabilities.add(PlayState_Subscribe); setCapabilities(capabilities); } @@ -667,14 +700,47 @@ private long convertStrTimeFormatToLong(String strTime) { } @Override - public void getPlayState(PlayStateListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } + public void getPlayState(final PlayStateListener listener) { + String method = "GetTransportInfo"; + String instanceId = "0"; + + JSONObject payload = getMethodBody(instanceId, method); + + ResponseListener responseListener = new ResponseListener() { + + @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> request = new ServiceCommand>(this, method, payload, responseListener); + request.send(); + } + @Override public ServiceSubscription subscribePlayState(PlayStateListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - return null; + URLServiceSubscription request = new URLServiceSubscription(this, PLAY_STATE, null, null); + request.addListener(listener); + addSubscription(request); + return request; + } + + private void addSubscription(URLServiceSubscription subscription) { + httpServer.getSubscriptions().add(subscription); + } + + @Override + public void unsubscribe(URLServiceSubscription subscription) { + httpServer.getSubscriptions().remove(subscription); } @Override @@ -695,6 +761,17 @@ public void connect() { connected = true; + Util.runInBackground(new Runnable() { + + @Override + public void run() { + httpServer = new DLNAHttpServer(); + httpServer.start(); + } + }); + + subscribeEvents(); + reportConnected(true); } @@ -713,6 +790,13 @@ public void run() { listener.onDisconnect(DLNAService.this, null); } }); + + unsubscribeEvents(); + + if (httpServer != null) { + httpServer.stop(); + httpServer = null; + } } @Override @@ -723,4 +807,136 @@ public void onLoseReachability(DeviceServiceReachability reachability) { mServiceReachability.stop(); } } + + public void subscribeEvents() { + Util.runInBackground(new Runnable() { + + @Override + public void run() { + String myIpAddress = null; + try { + myIpAddress = Util.getIpAddress(context).getHostAddress(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + + HttpHost host = new HttpHost(serviceDescription.getIpAddress(), serviceDescription.getPort()); + List serviceList = serviceDescription.getServiceList(); + + if (serviceList != null) { + for (int i = 0; i < serviceList.size(); i++) { + BasicHttpRequest request = new BasicHttpRequest(SUBSCRIBE, serviceList.get(i).eventSubURL); + + request.setHeader("CALLBACK", ""); + request.setHeader("NT", "upnp:event"); + request.setHeader("TIMEOUT", "Second-" + TIMEOUT); + + HttpResponse response = null; + + try { + response = httpClient.execute(host, request); + + int code = response.getStatusLine().getStatusCode(); + + if (code == 200) { + SIDList.put(serviceList.get(i).serviceType, response.getFirstHeader("SID").getValue()); + } + response.getEntity().consumeContent(); + } catch (ClientProtocolException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + }); + + resubscribeEvent(); + } + + public void resubscribeEvent() { + resubscriptionTimer.scheduleAtFixedRate(new TimerTask() { + + @Override + public void run() { + Util.runInBackground(new Runnable() { + + @Override + public void run() { + HttpHost host = new HttpHost(serviceDescription.getIpAddress(), serviceDescription.getPort()); + List serviceList = serviceDescription.getServiceList(); + + if (serviceList != null) { + for (int i = 0; i < serviceList.size(); i++) { + String eventSubURL = serviceList.get(i).eventSubURL; + String SID = SIDList.get(serviceList.get(i).serviceType); + + BasicHttpRequest request = new BasicHttpRequest(SUBSCRIBE, eventSubURL); + + request.setHeader("TIMEOUT", "Second-" + TIMEOUT); + request.setHeader("SID", SID); + + HttpResponse response = null; + + try { + response = httpClient.execute(host, request); + + int code = response.getStatusLine().getStatusCode(); + System.out.println("[DEBUG] count: " + Thread.activeCount()); + + if (code == 200) { + } + response.getEntity().consumeContent(); + } catch (ClientProtocolException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + }); + } + }, TIMEOUT/2*1000, TIMEOUT/2*1000); + } + + public void unsubscribeEvents() { + resubscriptionTimer.cancel(); + + Util.runInBackground(new Runnable() { + + @Override + public void run() { + final List serviceList = serviceDescription.getServiceList(); + + if (serviceList != null) { + for (int i = 0; i < serviceList.size(); i++) { + BasicHttpRequest request = new BasicHttpRequest(UNSUBSCRIBE, serviceList.get(i).eventSubURL); + + String sid = SIDList.get(serviceList.get(i).serviceType); + request.setHeader("SID", sid); + HttpResponse response = null; + + try { + HttpHost host = new HttpHost(serviceDescription.getIpAddress(), serviceDescription.getPort()); + + response = httpClient.execute(host, request); + + int code = response.getStatusLine().getStatusCode(); + + if (code == 200) { + SIDList.remove(serviceList.get(i).serviceType); + } + response.getEntity().consumeContent(); + } catch (ClientProtocolException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + }); + } } diff --git a/src/com/connectsdk/service/capability/MediaControl.java b/src/com/connectsdk/service/capability/MediaControl.java index b26cff40..45b13167 100644 --- a/src/com/connectsdk/service/capability/MediaControl.java +++ b/src/com/connectsdk/service/capability/MediaControl.java @@ -22,6 +22,7 @@ import com.connectsdk.service.capability.listeners.ResponseListener; import com.connectsdk.service.command.ServiceSubscription; +import com.google.android.gms.cast.MediaStatus; public interface MediaControl extends CapabilityMethods { public final static String Any = "MediaControl.Any"; @@ -60,7 +61,59 @@ public enum PlayStateStatus { Playing, Paused, Buffering, - Finished + Finished; + + public static PlayStateStatus convertPlayerStateToPlayStateStatus(int playerState) { + PlayStateStatus status = PlayStateStatus.Unknown; + + switch (playerState) { + case MediaStatus.PLAYER_STATE_BUFFERING: + status = PlayStateStatus.Buffering; + break; + case MediaStatus.PLAYER_STATE_IDLE: + status = PlayStateStatus.Finished; + break; + case MediaStatus.PLAYER_STATE_PAUSED: + status = PlayStateStatus.Paused; + break; + case MediaStatus.PLAYER_STATE_PLAYING: + status = PlayStateStatus.Playing; + break; + case MediaStatus.PLAYER_STATE_UNKNOWN: + default: + status = PlayStateStatus.Unknown; + break; + } + + return status; + } + + public static PlayStateStatus convertTransportStateToPlayStateStatus(String transportState) { + PlayStateStatus status = PlayStateStatus.Unknown; + + if (transportState.equals("STOPPED")) { + status = PlayStateStatus.Finished; + } + else if (transportState.equals("PLAYING")) { + status = PlayStateStatus.Playing; + } + else if (transportState.equals("TRANSITIONING")) { + status = PlayStateStatus.Buffering; + } + else if (transportState.equals("PAUSED_PLAYBACK")) { + status = PlayStateStatus.Paused; + } + else if (transportState.equals("PAUSED_RECORDING")) { + + } + else if (transportState.equals("RECORDING")) { + + } + else if (transportState.equals("NO_MEDIA_PRESENT")) { + + } + return status; + } }; public MediaControl getMediaControl(); From 36bda2bead70325b3485760b57ef86ed029fa0ee Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Fri, 5 Sep 2014 18:04:01 -0700 Subject: [PATCH 32/76] Moved DLNA Server related files path --- src/com/connectsdk/service/DLNAService.java | 2 +- src/com/connectsdk/{core => service}/upnp/DLNAHttpServer.java | 2 +- src/com/connectsdk/{core => service}/upnp/DLNANotifyParser.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/com/connectsdk/{core => service}/upnp/DLNAHttpServer.java (99%) rename src/com/connectsdk/{core => service}/upnp/DLNANotifyParser.java (97%) diff --git a/src/com/connectsdk/service/DLNAService.java b/src/com/connectsdk/service/DLNAService.java index ea1a2035..5b63127a 100644 --- a/src/com/connectsdk/service/DLNAService.java +++ b/src/com/connectsdk/service/DLNAService.java @@ -52,7 +52,6 @@ import com.connectsdk.core.ImageInfo; import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; -import com.connectsdk.core.upnp.DLNAHttpServer; import com.connectsdk.core.upnp.service.Service; import com.connectsdk.discovery.DiscoveryManager; import com.connectsdk.etc.helper.DeviceServiceReachability; @@ -68,6 +67,7 @@ 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; public class DLNAService extends DeviceService implements MediaControl, MediaPlayer { public static final String ID = "DLNA"; diff --git a/src/com/connectsdk/core/upnp/DLNAHttpServer.java b/src/com/connectsdk/service/upnp/DLNAHttpServer.java similarity index 99% rename from src/com/connectsdk/core/upnp/DLNAHttpServer.java rename to src/com/connectsdk/service/upnp/DLNAHttpServer.java index 02cf0ef7..cdac1975 100644 --- a/src/com/connectsdk/core/upnp/DLNAHttpServer.java +++ b/src/com/connectsdk/service/upnp/DLNAHttpServer.java @@ -1,4 +1,4 @@ -package com.connectsdk.core.upnp; +package com.connectsdk.service.upnp; import java.io.BufferedReader; import java.io.ByteArrayInputStream; diff --git a/src/com/connectsdk/core/upnp/DLNANotifyParser.java b/src/com/connectsdk/service/upnp/DLNANotifyParser.java similarity index 97% rename from src/com/connectsdk/core/upnp/DLNANotifyParser.java rename to src/com/connectsdk/service/upnp/DLNANotifyParser.java index 141d7518..4ee4ccca 100644 --- a/src/com/connectsdk/core/upnp/DLNANotifyParser.java +++ b/src/com/connectsdk/service/upnp/DLNANotifyParser.java @@ -1,4 +1,4 @@ -package com.connectsdk.core.upnp; +package com.connectsdk.service.upnp; import java.io.IOException; import java.io.InputStream; From 5975e713d9fd32a7e2c0bd2ac75165a93d011293 Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Fri, 5 Sep 2014 18:07:38 -0700 Subject: [PATCH 33/76] Modified file path to keep consistency --- src/com/connectsdk/service/NetcastTVService.java | 12 ++++++------ src/com/connectsdk/service/RokuService.java | 2 +- .../netcast/NetcastAppNumberParser.java | 2 +- .../netcast/NetcastApplicationsParser.java | 2 +- .../netcast/NetcastChannelParser.java | 2 +- .../netcast/NetcastHttpServer.java | 2 +- .../netcast/NetcastPOSTRequestParser.java | 2 +- .../netcast/NetcastVolumeParser.java | 2 +- .../{device => service}/netcast/VirtualKeycodes.java | 2 +- .../roku/RokuApplicationListParser.java | 2 +- 10 files changed, 15 insertions(+), 15 deletions(-) rename src/com/connectsdk/{device => service}/netcast/NetcastAppNumberParser.java (97%) rename src/com/connectsdk/{device => service}/netcast/NetcastApplicationsParser.java (98%) rename src/com/connectsdk/{device => service}/netcast/NetcastChannelParser.java (99%) rename src/com/connectsdk/{device => service}/netcast/NetcastHttpServer.java (99%) rename src/com/connectsdk/{device => service}/netcast/NetcastPOSTRequestParser.java (99%) rename src/com/connectsdk/{device => service}/netcast/NetcastVolumeParser.java (98%) rename src/com/connectsdk/{device => service}/netcast/VirtualKeycodes.java (98%) rename src/com/connectsdk/{device => service}/roku/RokuApplicationListParser.java (98%) diff --git a/src/com/connectsdk/service/NetcastTVService.java b/src/com/connectsdk/service/NetcastTVService.java index 11a8443f..211ea732 100644 --- a/src/com/connectsdk/service/NetcastTVService.java +++ b/src/com/connectsdk/service/NetcastTVService.java @@ -61,12 +61,6 @@ import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; import com.connectsdk.device.ConnectableDevice; -import com.connectsdk.device.netcast.NetcastAppNumberParser; -import com.connectsdk.device.netcast.NetcastApplicationsParser; -import com.connectsdk.device.netcast.NetcastChannelParser; -import com.connectsdk.device.netcast.NetcastHttpServer; -import com.connectsdk.device.netcast.NetcastVolumeParser; -import com.connectsdk.device.netcast.VirtualKeycodes; import com.connectsdk.discovery.DiscoveryManager; import com.connectsdk.discovery.DiscoveryManager.PairingLevel; import com.connectsdk.etc.helper.DeviceServiceReachability; @@ -90,6 +84,12 @@ import com.connectsdk.service.config.NetcastTVServiceConfig; import com.connectsdk.service.config.ServiceConfig; import com.connectsdk.service.config.ServiceDescription; +import com.connectsdk.service.netcast.NetcastAppNumberParser; +import com.connectsdk.service.netcast.NetcastApplicationsParser; +import com.connectsdk.service.netcast.NetcastChannelParser; +import com.connectsdk.service.netcast.NetcastHttpServer; +import com.connectsdk.service.netcast.NetcastVolumeParser; +import com.connectsdk.service.netcast.VirtualKeycodes; import com.connectsdk.service.sessions.LaunchSession; import com.connectsdk.service.sessions.LaunchSession.LaunchSessionType; diff --git a/src/com/connectsdk/service/RokuService.java b/src/com/connectsdk/service/RokuService.java index 8f5f4cff..c52a5838 100644 --- a/src/com/connectsdk/service/RokuService.java +++ b/src/com/connectsdk/service/RokuService.java @@ -57,7 +57,6 @@ import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; import com.connectsdk.device.ConnectableDevice; -import com.connectsdk.device.roku.RokuApplicationListParser; import com.connectsdk.discovery.DiscoveryManager; import com.connectsdk.etc.helper.DeviceServiceReachability; import com.connectsdk.etc.helper.HttpMessage; @@ -74,6 +73,7 @@ import com.connectsdk.service.command.URLServiceSubscription; import com.connectsdk.service.config.ServiceConfig; import com.connectsdk.service.config.ServiceDescription; +import com.connectsdk.service.roku.RokuApplicationListParser; import com.connectsdk.service.sessions.LaunchSession; public class RokuService extends DeviceService implements Launcher, diff --git a/src/com/connectsdk/device/netcast/NetcastAppNumberParser.java b/src/com/connectsdk/service/netcast/NetcastAppNumberParser.java similarity index 97% rename from src/com/connectsdk/device/netcast/NetcastAppNumberParser.java rename to src/com/connectsdk/service/netcast/NetcastAppNumberParser.java index 029c6ebc..9342e20c 100644 --- a/src/com/connectsdk/device/netcast/NetcastAppNumberParser.java +++ b/src/com/connectsdk/service/netcast/NetcastAppNumberParser.java @@ -18,7 +18,7 @@ * limitations under the License. */ -package com.connectsdk.device.netcast; +package com.connectsdk.service.netcast; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; diff --git a/src/com/connectsdk/device/netcast/NetcastApplicationsParser.java b/src/com/connectsdk/service/netcast/NetcastApplicationsParser.java similarity index 98% rename from src/com/connectsdk/device/netcast/NetcastApplicationsParser.java rename to src/com/connectsdk/service/netcast/NetcastApplicationsParser.java index 9d4e9df5..48f6cc41 100644 --- a/src/com/connectsdk/device/netcast/NetcastApplicationsParser.java +++ b/src/com/connectsdk/service/netcast/NetcastApplicationsParser.java @@ -18,7 +18,7 @@ * limitations under the License. */ -package com.connectsdk.device.netcast; +package com.connectsdk.service.netcast; import org.json.JSONArray; import org.json.JSONException; diff --git a/src/com/connectsdk/device/netcast/NetcastChannelParser.java b/src/com/connectsdk/service/netcast/NetcastChannelParser.java similarity index 99% rename from src/com/connectsdk/device/netcast/NetcastChannelParser.java rename to src/com/connectsdk/service/netcast/NetcastChannelParser.java index 021f0189..61569ed2 100644 --- a/src/com/connectsdk/device/netcast/NetcastChannelParser.java +++ b/src/com/connectsdk/service/netcast/NetcastChannelParser.java @@ -18,7 +18,7 @@ * limitations under the License. */ -package com.connectsdk.device.netcast; +package com.connectsdk.service.netcast; import org.json.JSONArray; import org.json.JSONException; diff --git a/src/com/connectsdk/device/netcast/NetcastHttpServer.java b/src/com/connectsdk/service/netcast/NetcastHttpServer.java similarity index 99% rename from src/com/connectsdk/device/netcast/NetcastHttpServer.java rename to src/com/connectsdk/service/netcast/NetcastHttpServer.java index 6a85788a..54937b88 100644 --- a/src/com/connectsdk/device/netcast/NetcastHttpServer.java +++ b/src/com/connectsdk/service/netcast/NetcastHttpServer.java @@ -18,7 +18,7 @@ * limitations under the License. */ -package com.connectsdk.device.netcast; +package com.connectsdk.service.netcast; import java.io.BufferedReader; import java.io.ByteArrayInputStream; diff --git a/src/com/connectsdk/device/netcast/NetcastPOSTRequestParser.java b/src/com/connectsdk/service/netcast/NetcastPOSTRequestParser.java similarity index 99% rename from src/com/connectsdk/device/netcast/NetcastPOSTRequestParser.java rename to src/com/connectsdk/service/netcast/NetcastPOSTRequestParser.java index 6a63269e..33e1b29f 100644 --- a/src/com/connectsdk/device/netcast/NetcastPOSTRequestParser.java +++ b/src/com/connectsdk/service/netcast/NetcastPOSTRequestParser.java @@ -18,7 +18,7 @@ * limitations under the License. */ -package com.connectsdk.device.netcast; +package com.connectsdk.service.netcast; import org.json.JSONException; import org.json.JSONObject; diff --git a/src/com/connectsdk/device/netcast/NetcastVolumeParser.java b/src/com/connectsdk/service/netcast/NetcastVolumeParser.java similarity index 98% rename from src/com/connectsdk/device/netcast/NetcastVolumeParser.java rename to src/com/connectsdk/service/netcast/NetcastVolumeParser.java index 09f14237..fb72dbf3 100644 --- a/src/com/connectsdk/device/netcast/NetcastVolumeParser.java +++ b/src/com/connectsdk/service/netcast/NetcastVolumeParser.java @@ -18,7 +18,7 @@ * limitations under the License. */ -package com.connectsdk.device.netcast; +package com.connectsdk.service.netcast; import org.json.JSONException; import org.json.JSONObject; diff --git a/src/com/connectsdk/device/netcast/VirtualKeycodes.java b/src/com/connectsdk/service/netcast/VirtualKeycodes.java similarity index 98% rename from src/com/connectsdk/device/netcast/VirtualKeycodes.java rename to src/com/connectsdk/service/netcast/VirtualKeycodes.java index 59ae395c..76ba752d 100644 --- a/src/com/connectsdk/device/netcast/VirtualKeycodes.java +++ b/src/com/connectsdk/service/netcast/VirtualKeycodes.java @@ -18,7 +18,7 @@ * limitations under the License. */ -package com.connectsdk.device.netcast; +package com.connectsdk.service.netcast; public enum VirtualKeycodes { POWER (1), diff --git a/src/com/connectsdk/device/roku/RokuApplicationListParser.java b/src/com/connectsdk/service/roku/RokuApplicationListParser.java similarity index 98% rename from src/com/connectsdk/device/roku/RokuApplicationListParser.java rename to src/com/connectsdk/service/roku/RokuApplicationListParser.java index 0b007ff8..e0eb0081 100644 --- a/src/com/connectsdk/device/roku/RokuApplicationListParser.java +++ b/src/com/connectsdk/service/roku/RokuApplicationListParser.java @@ -18,7 +18,7 @@ * limitations under the License. */ -package com.connectsdk.device.roku; +package com.connectsdk.service.roku; import java.util.ArrayList; import java.util.List; From 349caac872eec162ef5cca462c579cc387d399f1 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Wed, 10 Sep 2014 21:30:04 -0700 Subject: [PATCH 34/76] Made DeviceService listener protected for easier extending --- src/com/connectsdk/service/DeviceService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/connectsdk/service/DeviceService.java b/src/com/connectsdk/service/DeviceService.java index 1ecfa8c0..65efbc41 100644 --- a/src/com/connectsdk/service/DeviceService.java +++ b/src/com/connectsdk/service/DeviceService.java @@ -95,7 +95,7 @@ public enum PairingType { List mCapabilities; // @cond INTERNAL - DeviceServiceListener listener; + protected DeviceServiceListener listener; public SparseArray> requests = new SparseArray>(); From c54627d5dc77fb51d31660209261fc09b2fd6544 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Wed, 10 Sep 2014 23:19:20 -0700 Subject: [PATCH 35/76] Fixed some really weird formatting --- src/com/connectsdk/discovery/DiscoveryManager.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/com/connectsdk/discovery/DiscoveryManager.java b/src/com/connectsdk/discovery/DiscoveryManager.java index 470cf4b8..fbfaa756 100644 --- a/src/com/connectsdk/discovery/DiscoveryManager.java +++ b/src/com/connectsdk/discovery/DiscoveryManager.java @@ -846,8 +846,11 @@ public void addServiceDescriptionToDevice(ServiceDescription desc, ConnectableDe } DeviceService deviceService = DeviceService.getService(deviceServiceClass, desc, serviceConfig); - if (deviceService!=null) {deviceService.setServiceDescription(desc); - device.addService(deviceService);} + + if (deviceService != null) { + deviceService.setServiceDescription(desc); + device.addService(deviceService); + } } // @endcond } From 844e39137c8d5e7de6fa943105cffd36e70efbc8 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Wed, 10 Sep 2014 23:21:42 -0700 Subject: [PATCH 36/76] Added generic unsubscribe method to ServiceCommandProcessor --- src/com/connectsdk/service/DeviceService.java | 5 +++++ src/com/connectsdk/service/command/ServiceCommand.java | 3 ++- .../connectsdk/service/webos/WebOSTVServiceSocketClient.java | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/com/connectsdk/service/DeviceService.java b/src/com/connectsdk/service/DeviceService.java index 65efbc41..69b0e54d 100644 --- a/src/com/connectsdk/service/DeviceService.java +++ b/src/com/connectsdk/service/DeviceService.java @@ -44,6 +44,7 @@ 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.command.ServiceCommand.ServiceCommandProcessor; import com.connectsdk.service.config.ServiceConfig; @@ -278,6 +279,10 @@ public void unsubscribe(URLServiceSubscription subscription) { } + public void unsubscribe(ServiceSubscription subscription) { + + } + public void sendCommand(ServiceCommand command) { } diff --git a/src/com/connectsdk/service/command/ServiceCommand.java b/src/com/connectsdk/service/command/ServiceCommand.java index 020f9eda..b6596a03 100644 --- a/src/com/connectsdk/service/command/ServiceCommand.java +++ b/src/com/connectsdk/service/command/ServiceCommand.java @@ -128,7 +128,8 @@ public ResponseListener getResponseListener() { } public interface ServiceCommandProcessor { - public void unsubscribe(URLServiceSubscription subscription); + public void unsubscribe(URLServiceSubscription subscription); + public void unsubscribe(ServiceSubscription subscription); public void sendCommand(ServiceCommand command); } } \ No newline at end of file diff --git a/src/com/connectsdk/service/webos/WebOSTVServiceSocketClient.java b/src/com/connectsdk/service/webos/WebOSTVServiceSocketClient.java index b2e8f33e..8569200d 100644 --- a/src/com/connectsdk/service/webos/WebOSTVServiceSocketClient.java +++ b/src/com/connectsdk/service/webos/WebOSTVServiceSocketClient.java @@ -45,6 +45,7 @@ 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.command.ServiceCommand.ServiceCommandProcessor; import com.connectsdk.service.config.WebOSTVServiceConfig; @@ -539,6 +540,8 @@ public void unsubscribe(URLServiceSubscription subscription) { requests.remove(requestId); } } + + public void unsubscribe(ServiceSubscription subscription) { } protected void sendCommandImmediately(ServiceCommand command) { JSONObject headers = new JSONObject(); From fcbb37e100ae5aae4df8c46acf0d338cdecc2a87 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Wed, 10 Sep 2014 23:21:58 -0700 Subject: [PATCH 37/76] Made internal properties of ServiceCommand protected --- src/com/connectsdk/service/command/ServiceCommand.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/com/connectsdk/service/command/ServiceCommand.java b/src/com/connectsdk/service/command/ServiceCommand.java index b6596a03..1d9b3c20 100644 --- a/src/com/connectsdk/service/command/ServiceCommand.java +++ b/src/com/connectsdk/service/command/ServiceCommand.java @@ -38,10 +38,10 @@ public class ServiceCommand> { public static final String TYPE_POST = "POST"; public static final String TYPE_DEL = "DELETE"; - ServiceCommandProcessor processor; - String httpMethod; // WebOSTV: {request, subscribe}, NetcastTV: {GET, POST} - Object payload; - String target; + protected ServiceCommandProcessor processor; + protected String httpMethod; // WebOSTV: {request, subscribe}, NetcastTV: {GET, POST} + protected Object payload; + protected String target; int requestId; ResponseListener responseListener; From 3f79adf39fdc58e0ab5b0ad79ad391d450fea285 Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Thu, 11 Sep 2014 11:31:05 -0700 Subject: [PATCH 38/76] Removed unnecessary log --- src/com/connectsdk/service/DLNAService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/com/connectsdk/service/DLNAService.java b/src/com/connectsdk/service/DLNAService.java index 5b63127a..435ace20 100644 --- a/src/com/connectsdk/service/DLNAService.java +++ b/src/com/connectsdk/service/DLNAService.java @@ -883,7 +883,6 @@ public void run() { response = httpClient.execute(host, request); int code = response.getStatusLine().getStatusCode(); - System.out.println("[DEBUG] count: " + Thread.activeCount()); if (code == 200) { } From e7c813f1feaa4410ed60bcdf5108227a7d6ec0d4 Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Thu, 11 Sep 2014 15:08:20 -0700 Subject: [PATCH 39/76] Fixed DLNA: Accessing canceled subscription timer causes crash #150 --- src/com/connectsdk/service/DLNAService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/connectsdk/service/DLNAService.java b/src/com/connectsdk/service/DLNAService.java index 435ace20..68b642f1 100644 --- a/src/com/connectsdk/service/DLNAService.java +++ b/src/com/connectsdk/service/DLNAService.java @@ -113,7 +113,6 @@ public DLNAService(ServiceDescription serviceDescription, ServiceConfig serviceC context = DiscoveryManager.getInstance().getContext(); SIDList = new HashMap(); - resubscriptionTimer = new Timer(); updateControlURL(); } @@ -856,6 +855,7 @@ public void run() { } public void resubscribeEvent() { + resubscriptionTimer = new Timer(); resubscriptionTimer.scheduleAtFixedRate(new TimerTask() { @Override From f43c8761169edf25f462a13e24e9beb77bf45bbc Mon Sep 17 00:00:00 2001 From: Simon Gladkoskok Date: Thu, 11 Sep 2014 18:13:55 -0700 Subject: [PATCH 40/76] Add DLNA volume and mute control and subscriptions --- .../connectsdk/etc/helper/HttpMessage.java | 9 + src/com/connectsdk/service/DLNAService.java | 1054 +++++++++++------ .../service/upnp/DLNAHttpServer.java | 115 +- 3 files changed, 801 insertions(+), 377 deletions(-) diff --git a/src/com/connectsdk/etc/helper/HttpMessage.java b/src/com/connectsdk/etc/helper/HttpMessage.java index dd3eda10..f87a40f9 100644 --- a/src/com/connectsdk/etc/helper/HttpMessage.java +++ b/src/com/connectsdk/etc/helper/HttpMessage.java @@ -63,6 +63,15 @@ public static HttpPost getDLNAHttpPost(String uri, String action) { HttpPost post = getHttpPost(uri); post.setHeader("Soapaction", soapAction); + return post; + } + + public static HttpPost getDLNAHttpPostRenderControl(String uri, String action) { + String soapAction = "\"urn:schemas-upnp-org:service:RenderingControl:1#" + action + "\""; + + HttpPost post = getHttpPost(uri); + post.setHeader("Soapaction", soapAction); + return post; } diff --git a/src/com/connectsdk/service/DLNAService.java b/src/com/connectsdk/service/DLNAService.java index 5b63127a..58cc5321 100644 --- a/src/com/connectsdk/service/DLNAService.java +++ b/src/com/connectsdk/service/DLNAService.java @@ -31,6 +31,7 @@ import java.util.Timer; import java.util.TimerTask; +import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; @@ -48,6 +49,7 @@ import org.json.JSONObject; import android.content.Context; +import android.util.Log; import com.connectsdk.core.ImageInfo; import com.connectsdk.core.MediaInfo; @@ -58,6 +60,9 @@ import com.connectsdk.etc.helper.HttpMessage; import com.connectsdk.service.capability.MediaControl; import com.connectsdk.service.capability.MediaPlayer; +import com.connectsdk.service.capability.VolumeControl; +import com.connectsdk.service.capability.VolumeControl.MuteListener; +import com.connectsdk.service.capability.VolumeControl.VolumeListener; import com.connectsdk.service.capability.listeners.ResponseListener; import com.connectsdk.service.command.ServiceCommand; import com.connectsdk.service.command.ServiceCommandError; @@ -69,47 +74,53 @@ import com.connectsdk.service.sessions.LaunchSession.LaunchSessionType; import com.connectsdk.service.upnp.DLNAHttpServer; -public class DLNAService extends DeviceService implements MediaControl, MediaPlayer { +public class DLNAService extends DeviceService implements MediaControl, + MediaPlayer, VolumeControl { + public static final String ID = "DLNA"; private static final String SUBSCRIBE = "SUBSCRIBE"; private static final String UNSUBSCRIBE = "UNSUBSCRIBE"; - + private static final String DATA = "XMLData"; private static final String ACTION = "SOAPAction"; - private static final String ACTION_CONTENT = "\"urn:schemas-upnp-org:service:AVTransport:1#%s\""; - + private static final String ACTION_CONTENT = "\"urn:schemas-upnp-org:service:AVTransport:1#%s\""; + private static final String ACTION_CONTENT_RENDER = "\"urn:schemas-upnp-org:service:RenderingControl:1#%s\""; + private static final String AV_TRANSPORT = "AVTransport"; private static final String CONNECTION_MANAGER = "ConnectionManager"; private static final String RENDERING_CONTROL = "RenderingControl"; public final static String PLAY_STATE = "playState"; - - Context context; - String controlURL; + Context context; + + String controlURL, renderURL; HttpClient httpClient; - + DLNAHttpServer httpServer; - + Map 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) { + + public DLNAService(ServiceDescription serviceDescription, + ServiceConfig serviceConfig) { super(serviceDescription, serviceConfig); httpClient = new DefaultHttpClient(); ClientConnectionManager mgr = httpClient.getConnectionManager(); HttpParams params = httpClient.getParams(); - httpClient = new DefaultHttpClient(new ThreadSafeClientConnManager(params, mgr.getSchemeRegistry()), params); - + httpClient = new DefaultHttpClient(new ThreadSafeClientConnManager( + params, mgr.getSchemeRegistry()), params); + context = DiscoveryManager.getInstance().getContext(); SIDList = new HashMap(); @@ -117,29 +128,31 @@ public DLNAService(ServiceDescription serviceDescription, ServiceConfig serviceC updateControlURL(); } - + public static JSONObject discoveryParameters() { JSONObject params = new JSONObject(); - + try { params.put("serviceId", ID); - params.put("filter", "urn:schemas-upnp-org:device:MediaRenderer:1"); + params.put("filter", "urn:schemas-upnp-org:device:MediaRenderer:1"); } catch (JSONException e) { e.printStackTrace(); } return params; } - + @Override public void setServiceDescription(ServiceDescription serviceDescription) { super.setServiceDescription(serviceDescription); - + updateControlURL(); } - + private void updateControlURL() { StringBuilder sb = new StringBuilder(); + StringBuilder sb1 = new StringBuilder(); + int x =0; List serviceList = serviceDescription.getServiceList(); if (serviceList != null) { @@ -147,71 +160,91 @@ private void updateControlURL() { if (serviceList.get(i).serviceType.contains(AV_TRANSPORT)) { sb.append(serviceList.get(i).baseURL); sb.append(serviceList.get(i).controlURL); - break; + x++; + if (x==2) break; + } + else if (serviceList.get(i).serviceType.contains(RENDERING_CONTROL)) { + sb1.append(serviceList.get(i).baseURL); + sb1.append(serviceList.get(i).controlURL); + x++; + if (x==2) break; } } controlURL = sb.toString(); + renderURL = sb1.toString(); } } + + /****************** - MEDIA PLAYER - *****************/ + * MEDIA PLAYER + *****************/ @Override public MediaPlayer getMediaPlayer() { return this; }; - + @Override public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { return CapabilityPriorityLevel.NORMAL; } - - public void displayMedia(String url, String mimeType, String title, String description, String iconSrc, final LaunchListener listener) { + + public void displayMedia(String url, 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]; + 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; + } - 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); - mediaFormat = "mp3".equals(mediaFormat) ? "mpeg" : mediaFormat; - String mMimeType = String.format("%s/%s", mediaType, mediaFormat); - ResponseListener responseListener = new ResponseListener() { - + @Override public void onSuccess(Object response) { String method = "Play"; - + Map parameters = new HashMap(); parameters.put("Speed", "1"); - - JSONObject payload = getMethodBody(instanceId, method, parameters); - - ResponseListener playResponseListener = new ResponseListener () { + + JSONObject payload = getMethodBody(instanceId, method, + parameters); + + ResponseListener playResponseListener = new ResponseListener() { @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)); + Util.postSuccess(listener, new MediaLaunchObject( + launchSession, DLNAService.this)); } - + @Override public void onError(ServiceCommandError error) { Util.postError(listener, error); } }; - - ServiceCommand> request = new ServiceCommand>(DLNAService.this, method, payload, playResponseListener); + + ServiceCommand> request = new ServiceCommand>( + DLNAService.this, method, payload, playResponseListener); request.send(); } - + @Override public void onError(ServiceCommandError error) { Util.postError(listener, error); @@ -219,119 +252,140 @@ public void onError(ServiceCommandError error) { }; String method = "SetAVTransportURI"; - JSONObject httpMessage = getSetAVTransportURIBody(method, instanceId, url, mMimeType, title); + JSONObject httpMessage = getSetAVTransportURIBody(method, instanceId, + url, mMimeType, title); - ServiceCommand> request = new ServiceCommand>(DLNAService.this, method, httpMessage, responseListener); + ServiceCommand> request = new ServiceCommand>( + DLNAService.this, method, httpMessage, responseListener); request.send(); } - + @Override - public void displayImage(String url, String mimeType, String title, String description, String iconSrc, LaunchListener listener) { + public void displayImage(String url, String mimeType, String title, + String description, String iconSrc, LaunchListener listener) { displayMedia(url, mimeType, title, description, iconSrc, listener); } - + @Override public void displayImage(MediaInfo mediaInfo, LaunchListener listener) { - ImageInfo imageInfo = mediaInfo.getImages().get(0); - String iconSrc = imageInfo.getUrl(); - - displayImage(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, listener); + ImageInfo imageInfo = mediaInfo.getImages().get(0); + String iconSrc = imageInfo.getUrl(); + + displayImage(mediaInfo.getUrl(), mediaInfo.getMimeType(), + mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, + listener); } - + @Override - public void playMedia(String url, String mimeType, String title, String description, String iconSrc, boolean shouldLoop, LaunchListener listener) { + public void playMedia(String url, String mimeType, String title, + String description, String iconSrc, boolean shouldLoop, + LaunchListener listener) { displayMedia(url, mimeType, title, description, iconSrc, listener); -// stop(new ResponseListener() { -// -// @Override -// public void onError(ServiceCommandError error) { -// Util.postError(listener, error); -// } -// -// @Override -// public void onSuccess(Object object) { -// 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); -// -// String shareXML = String.format("" + -// "" + -// "" + -// "" + -// "0" + -// "%s" + -// "" + -// "<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:dc="http://purl.org/dc/elements/1.1/"><item id="0" parentID="0" restricted="0"><dc:title>%s</dc:title><dc:description>%s</dc:description><res protocolInfo="http-get:*:%s:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01500000000000000000000000000000">%s</res><upnp:albumArtURI>%s</upnp:albumArtURI><upnp:class>object.item.%sItem</upnp:class></item></DIDL-Lite>" + -// "" + -// "" + -// "" + -// "", -// url, title, description, mMimeType, url, iconSrc, mediaType); -// -// String method = "SetAVTransportURI"; -// JSONObject obj = new JSONObject(); -// try { -// obj.put(ACTION, String.format(ACTION_CONTENT, method)); -// obj.put(DATA, shareXML); -// } catch (JSONException e) { -// e.printStackTrace(); -// } -// -// ResponseListener playResponseListener = new ResponseListener () { -// @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)); -// } -// -// @Override -// public void onError(ServiceCommandError error) { -// if (listener != null) { -// listener.onError(error); -// } -// } -// }; -// -// ServiceCommand> request = new ServiceCommand>(DLNAService.this, method, obj, playResponseListener); -// request.send(); -// } -// }); + // stop(new ResponseListener() { + // + // @Override + // public void onError(ServiceCommandError error) { + // Util.postError(listener, error); + // } + // + // @Override + // public void onSuccess(Object object) { + // 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); + // + // String shareXML = + // String.format("" + // + + // "" + // + + // "" + + // "" + // + + // "0" + + // "%s" + + // "" + + // "<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:dc="http://purl.org/dc/elements/1.1/"><item id="0" parentID="0" restricted="0"><dc:title>%s</dc:title><dc:description>%s</dc:description><res protocolInfo="http-get:*:%s:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01500000000000000000000000000000">%s</res><upnp:albumArtURI>%s</upnp:albumArtURI><upnp:class>object.item.%sItem</upnp:class></item></DIDL-Lite>" + // + + // "" + + // "" + + // "" + + // "", + // url, title, description, mMimeType, url, iconSrc, mediaType); + // + // String method = "SetAVTransportURI"; + // JSONObject obj = new JSONObject(); + // try { + // obj.put(ACTION, String.format(ACTION_CONTENT, method)); + // obj.put(DATA, shareXML); + // } catch (JSONException e) { + // e.printStackTrace(); + // } + // + // ResponseListener playResponseListener = new + // ResponseListener () { + // @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)); + // } + // + // @Override + // public void onError(ServiceCommandError error) { + // if (listener != null) { + // listener.onError(error); + // } + // } + // }; + // + // ServiceCommand> request = new + // ServiceCommand>(DLNAService.this, method, + // obj, playResponseListener); + // request.send(); + // } + // }); } - + @Override public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, LaunchListener listener) { - ImageInfo imageInfo = mediaInfo.getImages().get(0); - String iconSrc = imageInfo.getUrl(); - - playMedia(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, shouldLoop, listener); + ImageInfo imageInfo = mediaInfo.getImages().get(0); + String iconSrc = imageInfo.getUrl(); + + playMedia(mediaInfo.getUrl(), mediaInfo.getMimeType(), + mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, + shouldLoop, listener); } - + @Override - public void closeMedia(LaunchSession launchSession, ResponseListener listener) { + public void closeMedia(LaunchSession launchSession, + ResponseListener listener) { if (launchSession.getService() instanceof DLNAService) ((DLNAService) launchSession.getService()).stop(listener); } - + /****************** - MEDIA CONTROL - *****************/ + * MEDIA CONTROL + *****************/ @Override public MediaControl getMediaControl() { return this; }; - + @Override public CapabilityPriorityLevel getMediaControlCapabilityLevel() { return CapabilityPriorityLevel.NORMAL; @@ -339,108 +393,115 @@ public CapabilityPriorityLevel getMediaControlCapabilityLevel() { @Override public void play(ResponseListener listener) { - String method = "Play"; + String method = "Play"; String instanceId = "0"; Map parameters = new HashMap(); parameters.put("Speed", "1"); - + JSONObject payload = getMethodBody(instanceId, method, parameters); - ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); + ServiceCommand> request = new ServiceCommand>( + this, method, payload, listener); request.send(); } @Override public void pause(ResponseListener listener) { - String method = "Pause"; + String method = "Pause"; String instanceId = "0"; JSONObject payload = getMethodBody(instanceId, method); - ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); + ServiceCommand> request = new ServiceCommand>( + this, method, payload, listener); request.send(); } @Override public void stop(ResponseListener listener) { - String method = "Stop"; + String method = "Stop"; String instanceId = "0"; JSONObject payload = getMethodBody(instanceId, method); - ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); + ServiceCommand> request = new ServiceCommand>( + this, method, payload, listener); request.send(); } - @Override - public void rewind(ResponseListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } + @Override + public void rewind(ResponseListener listener) { + Util.postError(listener, ServiceCommandError.notSupported()); + } - @Override - public void fastForward(ResponseListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } + @Override + public void fastForward(ResponseListener listener) { + Util.postError(listener, ServiceCommandError.notSupported()); + } @Override public void previous(ResponseListener listener) { - String method = "Previous"; - String instanceId = "0"; + String method = "Previous"; + String instanceId = "0"; - JSONObject payload = getMethodBody(instanceId, method); + JSONObject payload = getMethodBody(instanceId, method); - ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); - request.send(); + ServiceCommand> request = new ServiceCommand>( + this, method, payload, listener); + request.send(); } @Override public void next(ResponseListener listener) { - String method = "Next"; - String instanceId = "0"; + String method = "Next"; + String instanceId = "0"; - JSONObject payload = getMethodBody(instanceId, method); + JSONObject payload = getMethodBody(instanceId, method); - ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); - request.send(); + ServiceCommand> request = new ServiceCommand>( + this, method, payload, listener); + request.send(); } - + @Override public void seek(long position, ResponseListener listener) { - String method = "Seek"; + String method = "Seek"; String instanceId = "0"; - + 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); - + String time = String.format(Locale.US, "%02d:%02d:%02d", hour, minute, + second); + Map parameters = new HashMap(); parameters.put("Unit", "REL_TIME"); parameters.put("Target", time); JSONObject payload = getMethodBody(instanceId, method, parameters); - ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); + ServiceCommand> request = new ServiceCommand>( + this, method, payload, listener); request.send(); } - + private void getPositionInfo(final PositionInfoListener listener) { - String method = "GetPositionInfo"; + String method = "GetPositionInfo"; String instanceId = "0"; JSONObject payload = getMethodBody(instanceId, method); - + ResponseListener responseListener = new ResponseListener() { - + @Override public void onSuccess(Object response) { if (listener != null) { - listener.onGetPositionInfoSuccess((String)response); + listener.onGetPositionInfoSuccess((String) response); } } - + @Override public void onError(ServiceCommandError error) { if (listener != null) { @@ -449,43 +510,44 @@ public void onError(ServiceCommandError error) { } }; - ServiceCommand> request = new ServiceCommand>(this, method, payload, responseListener); + ServiceCommand> request = new ServiceCommand>( + 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"); - + long milliTimes = convertStrTimeFormatToLong(strDuration) * 1000; - + Util.postSuccess(listener, milliTimes); } - + @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) * 1000; - + Util.postSuccess(listener, milliTimes); } - + @Override public void onGetPositionInfoFailed(ServiceCommandError error) { Util.postError(listener, error); @@ -493,76 +555,117 @@ public void onGetPositionInfoFailed(ServiceCommandError error) { }); } - protected JSONObject getSetAVTransportURIBody(String method, String instanceId, String mediaURL, String mime, String title) { + protected JSONObject getSetAVTransportURIBody(String method, + String instanceId, String mediaURL, String mime, String title) { String action = "SetAVTransportURI"; String metadata = getMetadata(mediaURL, mime, title); - - StringBuilder sb = new StringBuilder(); - sb.append(""); - sb.append(""); - - sb.append(""); - sb.append(""); - sb.append("" + instanceId + ""); - sb.append("" + mediaURL + ""); - sb.append(""+ metadata + ""); - - sb.append(""); - sb.append(""); - sb.append(""); - - JSONObject obj = new JSONObject(); - try { + + StringBuilder sb = new StringBuilder(); + sb.append(""); + sb.append(""); + + sb.append(""); + sb.append(""); + sb.append("" + instanceId + ""); + sb.append("" + mediaURL + ""); + sb.append("" + metadata + ""); + + sb.append(""); + sb.append(""); + sb.append(""); + + JSONObject obj = new JSONObject(); + try { obj.put(DATA, sb.toString()); obj.put(ACTION, String.format(ACTION_CONTENT, method)); } catch (JSONException e) { e.printStackTrace(); } - return obj; + return obj; } - + protected JSONObject getMethodBody(String instanceId, String method) { return getMethodBody(instanceId, method, null); } - protected JSONObject getMethodBody(String instanceId, String method, Map parameters) { - StringBuilder sb = new StringBuilder(); - - sb.append(""); - sb.append(""); - - sb.append(""); - sb.append(""); - sb.append("" + instanceId + ""); - - if (parameters != null) { - for (Map.Entry entry : parameters.entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - - sb.append("<" + key + ">"); - sb.append(value); - sb.append(""); - } - } - - sb.append(""); - sb.append(""); - sb.append(""); - - JSONObject obj = new JSONObject(); - try { + protected JSONObject getMethodBody(String instanceId, String method, + Map parameters) { + StringBuilder sb = new StringBuilder(); + + sb.append(""); + sb.append(""); + + sb.append(""); + sb.append(""); + sb.append("" + instanceId + ""); + + if (parameters != null) { + for (Map.Entry entry : parameters.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + + sb.append("<" + key + ">"); + sb.append(value); + sb.append(""); + } + } + + sb.append(""); + sb.append(""); + sb.append(""); + + JSONObject obj = new JSONObject(); + try { obj.put(DATA, sb.toString()); obj.put(ACTION, String.format(ACTION_CONTENT, method)); } catch (JSONException e) { e.printStackTrace(); } - return obj; + return obj; } - protected String getMetadata(String mediaURL, String mime, String title) { + protected JSONObject getRenderingControlMethodBody(String instanceId, + String method, String channel, String value) { + StringBuilder sb = new StringBuilder(); + + sb.append(""); + sb.append(""); + + sb.append(""); + sb.append(""); + sb.append("" + instanceId + ""); + sb.append("" + channel + ""); + if (method.equals("SetVolume")) + sb.append("" + value + ""); + else if (method.equals("SetMute")) sb.append("" + value + ""); + + sb.append(""); + sb.append(""); + sb.append(""); + + JSONObject obj = new JSONObject(); + try { + obj.put(DATA, sb.toString()); + obj.put(ACTION, String.format(ACTION_CONTENT_RENDER, method)); + } catch (JSONException e) { + e.printStackTrace(); + } + + return obj; + } + + protected JSONObject getRenderingControlMethodBody(String instanceId, + String method, String channel) { + return getRenderingControlMethodBody(instanceId, method, channel, null); + } + + protected String getMetadata(String mediaURL, String mime, String title) { String id = "1000"; String parentID = "0"; String restricted = "0"; @@ -573,63 +676,74 @@ protected String getMetadata(String mediaURL, String mime, String title) { sb.append("xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" "); sb.append("xmlns:dc="http://purl.org/dc/elements/1.1/">"); - sb.append("<item id="" + id + "" parentID="" + parentID + "" restricted="" + restricted + "">"); + sb.append("<item id="" + id + "" parentID="" + + parentID + "" restricted="" + restricted + + "">"); sb.append("<dc:title>" + title + "</dc:title>"); - + if (mime.startsWith("image")) { objectClass = "object.item.imageItem"; - } - else if (mime.startsWith("video")) { + } else if (mime.startsWith("video")) { objectClass = "object.item.videoItem"; - } - else if (mime.startsWith("audio")) { + } else if (mime.startsWith("audio")) { objectClass = "object.item.audioItem"; } - sb.append("<res protocolInfo="http-get:*:" + mime + ":DLNA.ORG_OP=01">" + mediaURL + "</res>"); + sb.append("<res protocolInfo="http-get:*:" + mime + + ":DLNA.ORG_OP=01">" + mediaURL + "</res>"); sb.append("<upnp:class>" + objectClass + "</upnp:class>"); sb.append("</item>"); sb.append("</DIDL-Lite>"); - + return sb.toString(); } - + @Override public void sendCommand(final ServiceCommand mCommand) { Util.runInBackground(new Runnable() { - + @SuppressWarnings("unchecked") @Override public void run() { ServiceCommand> command = (ServiceCommand>) mCommand; - + JSONObject payload = (JSONObject) command.getPayload(); - - HttpPost request = HttpMessage.getDLNAHttpPost(controlURL, command.getTarget()); + + HttpPost request; + + if ((command.getTarget().contains("Volume")) || (command.getTarget().contains("Mute"))) { + + request = HttpMessage.getDLNAHttpPostRenderControl(renderURL, + command.getTarget());} + else {request = HttpMessage.getDLNAHttpPost(controlURL, + command.getTarget());} request.setHeader(ACTION, payload.optString(ACTION)); try { - request.setEntity(new StringEntity(payload.optString(DATA).toString())); + request.setEntity(new StringEntity(payload.optString(DATA) + .toString())); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } - - HttpResponse response = null; + HttpResponse response = null; + try { response = httpClient.execute(request); - + int code = response.getStatusLine().getStatusCode(); - if (code == 200) { - HttpEntity entity = response.getEntity(); - String message = EntityUtils.toString(entity, "UTF-8"); - + if (code == 200) { + HttpEntity entity = response.getEntity(); + String message = EntityUtils.toString(entity, "UTF-8"); + Util.postSuccess(command.getResponseListener(), message); + } else { + Util.postError(command.getResponseListener(), + ServiceCommandError.getError(code)); + + } - else { - Util.postError(command.getResponseListener(), ServiceCommandError.getError(code)); - } - + response.getEntity().consumeContent(); } catch (ClientProtocolException e) { e.printStackTrace(); @@ -639,16 +753,16 @@ public void run() { } }); } - + @Override protected void updateCapabilities() { List capabilities = new ArrayList(); - + capabilities.add(Display_Image); capabilities.add(Display_Video); capabilities.add(Display_Audio); capabilities.add(Close); - + capabilities.add(MetaData_Title); capabilities.add(MetaData_MimeType); @@ -661,144 +775,165 @@ protected void updateCapabilities() { capabilities.add(PlayState); capabilities.add(PlayState_Subscribe); + 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 { + public LaunchSession decodeLaunchSession(String type, JSONObject sessionObj) + throws JSONException { if (type == "dlna") { - LaunchSession launchSession = LaunchSession.launchSessionFromJSONObject(sessionObj); + LaunchSession launchSession = LaunchSession + .launchSessionFromJSONObject(sessionObj); launchSession.setService(this); return launchSession; } return null; } - + private String parseData(String response, String key) { String startTag = "<" + key + ">"; String endTag = ""; - + int start = response.indexOf(startTag); int end = response.indexOf(endTag); - + String data = response.substring(start + startTag.length(), end); - + return data; } - + private long convertStrTimeFormatToLong(String strTime) { String[] tokens = strTime.split(":"); long time = 0; - + for (int i = 0; i < tokens.length; i++) { time *= 60; time += Integer.parseInt(tokens[i]); } - + return time; } @Override public void getPlayState(final PlayStateListener listener) { - String method = "GetTransportInfo"; + String method = "GetTransportInfo"; String instanceId = "0"; JSONObject payload = getMethodBody(instanceId, method); - + ResponseListener responseListener = new ResponseListener() { - + @Override public void onSuccess(Object response) { - String transportState = parseData((String)response, "CurrentTransportState"); - PlayStateStatus status = PlayStateStatus.convertTransportStateToPlayStateStatus(transportState); - + 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> request = new ServiceCommand>(this, method, payload, responseListener); + ServiceCommand> request = new ServiceCommand>( + this, method, payload, responseListener); request.send(); } - + @Override - public ServiceSubscription subscribePlayState(PlayStateListener listener) { - URLServiceSubscription request = new URLServiceSubscription(this, PLAY_STATE, null, null); + public ServiceSubscription subscribePlayState( + PlayStateListener listener) { + URLServiceSubscription request = new URLServiceSubscription( + this, PLAY_STATE, null, null); request.addListener(listener); addSubscription(request); return request; } - + private void addSubscription(URLServiceSubscription subscription) { httpServer.getSubscriptions().add(subscription); } - + @Override public void unsubscribe(URLServiceSubscription subscription) { - httpServer.getSubscriptions().remove(subscription); +// if (subscription!=null) +// httpServer.getSubscriptions().remove(subscription); + unsubscribeEvents(); } - + @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(); - + // 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; - + Util.runInBackground(new Runnable() { - + @Override public void run() { httpServer = new DLNAHttpServer(); httpServer.start(); } }); - + subscribeEvents(); - + reportConnected(true); } - + @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); } }); - + unsubscribeEvents(); - + if (httpServer != null) { httpServer.stop(); httpServer = null; } } - + @Override public void onLoseReachability(DeviceServiceReachability reachability) { if (connected) { @@ -807,39 +942,47 @@ public void onLoseReachability(DeviceServiceReachability reachability) { mServiceReachability.stop(); } } - + public void subscribeEvents() { Util.runInBackground(new Runnable() { @Override public void run() { - String myIpAddress = null; + String myIpAddress = null; try { myIpAddress = Util.getIpAddress(context).getHostAddress(); } catch (UnknownHostException e) { e.printStackTrace(); } - - HttpHost host = new HttpHost(serviceDescription.getIpAddress(), serviceDescription.getPort()); + + HttpHost host = new HttpHost(serviceDescription.getIpAddress(), + serviceDescription.getPort()); List serviceList = serviceDescription.getServiceList(); if (serviceList != null) { for (int i = 0; i < serviceList.size(); i++) { - BasicHttpRequest request = new BasicHttpRequest(SUBSCRIBE, serviceList.get(i).eventSubURL); - - request.setHeader("CALLBACK", ""); + BasicHttpRequest request = new BasicHttpRequest( + SUBSCRIBE, serviceList.get(i).eventSubURL); + + request.setHeader( + "CALLBACK", + ""); request.setHeader("NT", "upnp:event"); request.setHeader("TIMEOUT", "Second-" + TIMEOUT); - + HttpResponse response = null; - + try { response = httpClient.execute(host, request); - + int code = response.getStatusLine().getStatusCode(); - if (code == 200) { - SIDList.put(serviceList.get(i).serviceType, response.getFirstHeader("SID").getValue()); + if (code == 200) { + SIDList.put(serviceList.get(i).serviceType, + response.getFirstHeader("SID") + .getValue()); } response.getEntity().consumeContent(); } catch (ClientProtocolException e) { @@ -851,41 +994,48 @@ public void run() { } } }); - + resubscribeEvent(); } - + public void resubscribeEvent() { resubscriptionTimer.scheduleAtFixedRate(new TimerTask() { - + @Override public void run() { Util.runInBackground(new Runnable() { @Override public void run() { - HttpHost host = new HttpHost(serviceDescription.getIpAddress(), serviceDescription.getPort()); - List serviceList = serviceDescription.getServiceList(); + HttpHost host = new HttpHost(serviceDescription + .getIpAddress(), serviceDescription.getPort()); + List serviceList = serviceDescription + .getServiceList(); if (serviceList != null) { for (int i = 0; i < serviceList.size(); i++) { String eventSubURL = serviceList.get(i).eventSubURL; String SID = SIDList.get(serviceList.get(i).serviceType); - - BasicHttpRequest request = new BasicHttpRequest(SUBSCRIBE, eventSubURL); - - request.setHeader("TIMEOUT", "Second-" + TIMEOUT); + + BasicHttpRequest request = new BasicHttpRequest( + SUBSCRIBE, eventSubURL); + + request.setHeader("TIMEOUT", "Second-" + + TIMEOUT); request.setHeader("SID", SID); - + HttpResponse response = null; - + try { - response = httpClient.execute(host, request); - - int code = response.getStatusLine().getStatusCode(); - System.out.println("[DEBUG] count: " + Thread.activeCount()); + response = httpClient + .execute(host, request); + + int code = response.getStatusLine() + .getStatusCode(); + System.out.println("[DEBUG] count: " + + Thread.activeCount()); - if (code == 200) { + if (code == 200) { } response.getEntity().consumeContent(); } catch (ClientProtocolException e) { @@ -896,37 +1046,41 @@ public void run() { } } } - }); + }); } - }, TIMEOUT/2*1000, TIMEOUT/2*1000); + }, TIMEOUT / 2 * 1000, TIMEOUT / 2 * 1000); } - + public void unsubscribeEvents() { resubscriptionTimer.cancel(); - + Util.runInBackground(new Runnable() { @Override public void run() { - final List serviceList = serviceDescription.getServiceList(); + final List serviceList = serviceDescription + .getServiceList(); if (serviceList != null) { for (int i = 0; i < serviceList.size(); i++) { - BasicHttpRequest request = new BasicHttpRequest(UNSUBSCRIBE, serviceList.get(i).eventSubURL); - + BasicHttpRequest request = new BasicHttpRequest( + UNSUBSCRIBE, serviceList.get(i).eventSubURL); + String sid = SIDList.get(serviceList.get(i).serviceType); request.setHeader("SID", sid); HttpResponse response = null; try { - HttpHost host = new HttpHost(serviceDescription.getIpAddress(), serviceDescription.getPort()); - + HttpHost host = new HttpHost(serviceDescription + .getIpAddress(), serviceDescription + .getPort()); + response = httpClient.execute(host, request); - + int code = response.getStatusLine().getStatusCode(); - if (code == 200) { - SIDList.remove(serviceList.get(i).serviceType); + if (code == 200) { + SIDList.remove(serviceList.get(i).serviceType); } response.getEntity().consumeContent(); } catch (ClientProtocolException e) { @@ -939,4 +1093,220 @@ public void run() { } }); } + + /****************** + * VOLUME CONTROL + *****************/ + + @Override + public VolumeControl getVolumeControl() { + return this; + } + + @Override + public CapabilityPriorityLevel getVolumeControlCapabilityLevel() { + return CapabilityPriorityLevel.NORMAL; + } + + @Override + public void volumeUp(final ResponseListener 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 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 listener) { + + String method = "SetVolume"; + String instanceId = "0"; + String channel = "Master"; + String value = String.valueOf((int)(volume*100)); + + JSONObject payload = getRenderingControlMethodBody(instanceId, method, + channel, value); + + ServiceCommand> request = new ServiceCommand>( + this, method, payload, listener); + request.send(); + + } + + private ServiceCommand getVolumeStatus(Boolean isSubscription, + final VolumeListener listener) { + + String method = "GetVolume"; + String instanceId = "0"; + String channel = "Master"; + + ServiceCommand request; + + JSONObject payload = getRenderingControlMethodBody(instanceId, method, + channel); + + ResponseListener responseListener = new ResponseListener() { + + @Override + public void onSuccess(Object response) { + String currentVolume = parseData((String) response, + "CurrentVolume"); + int iVolume = Integer.parseInt(currentVolume); + float fVolume = (float) (iVolume / 100.0); + + Util.postSuccess(listener, fVolume); + } + + @Override + public void onError(ServiceCommandError error) { + Util.postError(listener, error); + } + }; + + if (isSubscription) + request = new URLServiceSubscription(this, method, + payload, responseListener); + else + request = new ServiceCommand(this, method, payload, + responseListener); + + request.send(); + + return request; + + } + + @Override + public void getVolume(VolumeListener listener) { + + getVolumeStatus(false, listener); + } + + @Override + public void setMute(boolean isMute, ResponseListener listener) { + + String method = "SetMute"; + String instanceId = "0"; + String channel = "Master"; + String value = String.valueOf(isMute); + + JSONObject payload = getRenderingControlMethodBody(instanceId, method, + channel, value); + + ServiceCommand> request = new ServiceCommand>( + this, method, payload, listener); + request.send(); + + } + + + private ServiceCommand> getMuteStatus(boolean isSubscription, final MuteListener listener) { + + String method = "GetMute"; + String instanceId = "0"; + String channel = "Master"; + + ServiceCommand> request; + + JSONObject payload = getRenderingControlMethodBody(instanceId, method, + channel); + + ResponseListener responseListener = new ResponseListener() { + + @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); + } + }; + + if (isSubscription) + request = new URLServiceSubscription>(this, method , payload, responseListener); + else + request = new ServiceCommand>(this, method, payload, responseListener); + + request.send(); + + return request; + } + + @Override + public void getMute(MuteListener listener) { + getMuteStatus(false, listener); + } + + @Override + public ServiceSubscription subscribeVolume( + VolumeListener listener) { + URLServiceSubscription request = new URLServiceSubscription( + this, "volume", null, null); + request.addListener(listener); + addSubscription(request); + return request; } + + @Override + public ServiceSubscription subscribeMute(MuteListener listener) { + URLServiceSubscription request = new URLServiceSubscription( + this, "mute", null, null); + request.addListener(listener); + addSubscription(request); + return request; + } + } diff --git a/src/com/connectsdk/service/upnp/DLNAHttpServer.java b/src/com/connectsdk/service/upnp/DLNAHttpServer.java index cdac1975..0ac6cc92 100644 --- a/src/com/connectsdk/service/upnp/DLNAHttpServer.java +++ b/src/com/connectsdk/service/upnp/DLNAHttpServer.java @@ -18,6 +18,7 @@ import org.xmlpull.v1.XmlPullParserException; import android.text.Html; +import android.util.Log; import com.connectsdk.core.Util; import com.connectsdk.service.capability.MediaControl.PlayStateStatus; @@ -26,39 +27,39 @@ public class DLNAHttpServer { ServerSocket welcomeSocket; - + int port = 49291; boolean running = false; List> subscriptions; - + public DLNAHttpServer() { - subscriptions = new ArrayList>(); + subscriptions = new ArrayList>(); } public void start() { if (running) return; - + running = true; - + try { welcomeSocket = new ServerSocket(this.port); } catch (IOException ex) { ex.printStackTrace(); } - + while (running) { if (welcomeSocket == null || welcomeSocket.isClosed()) { stop(); break; } - + Socket connectionSocket = null; BufferedReader inFromClient = null; DataOutputStream outToClient = null; - + try { connectionSocket = welcomeSocket.accept(); } catch (IOException ex) { @@ -67,27 +68,28 @@ public void start() { stop(); return; } - + int c = 0; - + String body = null; - + try { - inFromClient = new BufferedReader(new InputStreamReader(connectionSocket.getInputStream())); + inFromClient = new BufferedReader(new InputStreamReader( + connectionSocket.getInputStream())); StringBuilder sb = new StringBuilder(); - + while ((c = inFromClient.read()) != -1) { - sb.append((char)c); + sb.append((char) c); if (sb.toString().endsWith("\r\n\r\n")) break; } - + sb = new StringBuilder(); - + while ((c = inFromClient.read()) != -1) { - sb.append((char)c); + sb.append((char) c); body = sb.toString(); if (body.endsWith("")) @@ -98,11 +100,12 @@ public void start() { } catch (IOException ex) { ex.printStackTrace(); } - + PrintWriter out = null; - + try { - outToClient = new DataOutputStream(connectionSocket.getOutputStream()); + outToClient = new DataOutputStream( + connectionSocket.getOutputStream()); out = new PrintWriter(outToClient); out.println("HTTP/1.1 200 OK"); out.println("Connection: Close"); @@ -121,35 +124,78 @@ public void start() { ex.printStackTrace(); } } - + InputStream stream = null; - + try { stream = new ByteArrayInputStream(body.getBytes("UTF-8")); } catch (UnsupportedEncodingException ex) { ex.printStackTrace(); } - + JSONObject event; DLNANotifyParser parser = new DLNANotifyParser(); - + try { event = parser.parse(stream); - + if (!event.isNull("TransportState")) { String transportState = event.getString("TransportState"); - PlayStateStatus status = PlayStateStatus.convertTransportStateToPlayStateStatus(transportState); - - for (URLServiceSubscription sub: subscriptions) { + PlayStateStatus status = PlayStateStatus + .convertTransportStateToPlayStateStatus(transportState); + + for (URLServiceSubscription sub : subscriptions) { if (sub.getTarget().equalsIgnoreCase("playState")) { for (int i = 0; i < sub.getListeners().size(); i++) { @SuppressWarnings("unchecked") - ResponseListener listener = (ResponseListener) sub.getListeners().get(i); + ResponseListener listener = (ResponseListener) sub + .getListeners().get(i); Util.postSuccess(listener, status); } } } } + + if (event.has("Volume")) { + Log.d("LG", "Volume " + event.getString("Volume")); + if (event.getString("Volume") != null) { + + int intVolume = event.getInt("Volume"); + float volume = (float) intVolume / 100; + + for (URLServiceSubscription sub : subscriptions) { + if (sub.getTarget().equalsIgnoreCase("volume")) { + for (int i = 0; i < sub.getListeners().size(); i++) { + @SuppressWarnings("unchecked") + ResponseListener listener = (ResponseListener) sub + .getListeners().get(i); + Util.postSuccess(listener, volume); + } + } + } + } + + } + + if (event.has("Mute")) { + Log.d("LG", "Mute " + event.getString("Mute")); + + int intMute = event.getInt("Mute"); + boolean mute = (intMute==1) ? true : false; + + + for (URLServiceSubscription sub : subscriptions) { + if (sub.getTarget().equalsIgnoreCase("mute")) { + for (int i = 0; i < sub.getListeners().size(); i++) { + @SuppressWarnings("unchecked") + ResponseListener listener = (ResponseListener) sub + .getListeners().get(i); + Util.postSuccess(listener, mute); + } + } + } + } + } catch (XmlPullParserException e) { e.printStackTrace(); } catch (IOException e) { @@ -159,12 +205,11 @@ public void start() { } } } - - + public void stop() { if (!running) return; - + if (welcomeSocket != null && !welcomeSocket.isClosed()) { try { welcomeSocket.close(); @@ -172,15 +217,15 @@ public void stop() { ex.printStackTrace(); } } - + welcomeSocket = null; running = false; } - + public int getPort() { return port; } - + public List> getSubscriptions() { return subscriptions; } From f1e028facd3a084d04ee26fc2d1cb29db75b7b01 Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Thu, 11 Sep 2014 18:16:59 -0700 Subject: [PATCH 41/76] Added null checker to avoid crash --- src/com/connectsdk/service/DLNAService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/com/connectsdk/service/DLNAService.java b/src/com/connectsdk/service/DLNAService.java index 68b642f1..5fef38b6 100644 --- a/src/com/connectsdk/service/DLNAService.java +++ b/src/com/connectsdk/service/DLNAService.java @@ -901,7 +901,8 @@ public void run() { } public void unsubscribeEvents() { - resubscriptionTimer.cancel(); + if (resubscriptionTimer != null) + resubscriptionTimer.cancel(); Util.runInBackground(new Runnable() { From 02e22b6add01f08b559f10c9fb5ed06358e4b85e Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Fri, 12 Sep 2014 11:48:02 -0700 Subject: [PATCH 42/76] Reverted auto corrections in (https://github.com/ConnectSDK/Connect-SDK-Android/commit/f43c8761169edf25f462a13e24e9beb77bf45bbc) --- src/com/connectsdk/service/DLNAService.java | 1041 ++++++----------- .../service/upnp/DLNAHttpServer.java | 115 +- 2 files changed, 372 insertions(+), 784 deletions(-) diff --git a/src/com/connectsdk/service/DLNAService.java b/src/com/connectsdk/service/DLNAService.java index ea6a41f1..5fef38b6 100644 --- a/src/com/connectsdk/service/DLNAService.java +++ b/src/com/connectsdk/service/DLNAService.java @@ -58,7 +58,6 @@ import com.connectsdk.etc.helper.HttpMessage; import com.connectsdk.service.capability.MediaControl; import com.connectsdk.service.capability.MediaPlayer; -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; @@ -70,84 +69,76 @@ import com.connectsdk.service.sessions.LaunchSession.LaunchSessionType; import com.connectsdk.service.upnp.DLNAHttpServer; -public class DLNAService extends DeviceService implements MediaControl, - MediaPlayer, VolumeControl { - +public class DLNAService extends DeviceService implements MediaControl, MediaPlayer { public static final String ID = "DLNA"; private static final String SUBSCRIBE = "SUBSCRIBE"; private static final String UNSUBSCRIBE = "UNSUBSCRIBE"; - + private static final String DATA = "XMLData"; private static final String ACTION = "SOAPAction"; - private static final String ACTION_CONTENT = "\"urn:schemas-upnp-org:service:AVTransport:1#%s\""; - private static final String ACTION_CONTENT_RENDER = "\"urn:schemas-upnp-org:service:RenderingControl:1#%s\""; - + private static final String ACTION_CONTENT = "\"urn:schemas-upnp-org:service:AVTransport:1#%s\""; + private static final String AV_TRANSPORT = "AVTransport"; private static final String CONNECTION_MANAGER = "ConnectionManager"; private static final String RENDERING_CONTROL = "RenderingControl"; public final static String PLAY_STATE = "playState"; + + Context context; - Context context; - - String controlURL, renderURL; + String controlURL; HttpClient httpClient; - + DLNAHttpServer httpServer; - + Map 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) { + + public DLNAService(ServiceDescription serviceDescription, ServiceConfig serviceConfig) { super(serviceDescription, serviceConfig); httpClient = new DefaultHttpClient(); ClientConnectionManager mgr = httpClient.getConnectionManager(); HttpParams params = httpClient.getParams(); - httpClient = new DefaultHttpClient(new ThreadSafeClientConnManager( - params, mgr.getSchemeRegistry()), params); - + httpClient = new DefaultHttpClient(new ThreadSafeClientConnManager(params, mgr.getSchemeRegistry()), params); + context = DiscoveryManager.getInstance().getContext(); SIDList = new HashMap(); updateControlURL(); } - + public static JSONObject discoveryParameters() { JSONObject params = new JSONObject(); - + try { params.put("serviceId", ID); - params.put("filter", "urn:schemas-upnp-org:device:MediaRenderer:1"); + params.put("filter", "urn:schemas-upnp-org:device:MediaRenderer:1"); } catch (JSONException e) { e.printStackTrace(); } return params; } - + @Override public void setServiceDescription(ServiceDescription serviceDescription) { super.setServiceDescription(serviceDescription); - + updateControlURL(); } - + private void updateControlURL() { StringBuilder sb = new StringBuilder(); - StringBuilder sb1 = new StringBuilder(); - int x =0; List serviceList = serviceDescription.getServiceList(); if (serviceList != null) { @@ -155,91 +146,71 @@ private void updateControlURL() { if (serviceList.get(i).serviceType.contains(AV_TRANSPORT)) { sb.append(serviceList.get(i).baseURL); sb.append(serviceList.get(i).controlURL); - x++; - if (x==2) break; - } - else if (serviceList.get(i).serviceType.contains(RENDERING_CONTROL)) { - sb1.append(serviceList.get(i).baseURL); - sb1.append(serviceList.get(i).controlURL); - x++; - if (x==2) break; + break; } } controlURL = sb.toString(); - renderURL = sb1.toString(); } } - - /****************** - * MEDIA PLAYER - *****************/ + MEDIA PLAYER + *****************/ @Override public MediaPlayer getMediaPlayer() { return this; }; - + @Override public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { return CapabilityPriorityLevel.NORMAL; } - - public void displayMedia(String url, String mimeType, String title, - String description, String iconSrc, final LaunchListener listener) { + + public void displayMedia(String url, 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; - } + String[] mediaElements = mimeType.split("/"); + String mediaType = mediaElements[0]; + String mediaFormat = mediaElements[1]; - mediaFormat = "mp3".equals(mediaFormat) ? "mpeg" : mediaFormat; - String mMimeType = String.format("%s/%s", mediaType, mediaFormat); + 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 responseListener = new ResponseListener() { - + @Override public void onSuccess(Object response) { String method = "Play"; - + Map parameters = new HashMap(); parameters.put("Speed", "1"); - - JSONObject payload = getMethodBody(instanceId, method, - parameters); - - ResponseListener playResponseListener = new ResponseListener() { + + JSONObject payload = getMethodBody(instanceId, method, parameters); + + ResponseListener playResponseListener = new ResponseListener () { @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)); + Util.postSuccess(listener, new MediaLaunchObject(launchSession, DLNAService.this)); } - + @Override public void onError(ServiceCommandError error) { Util.postError(listener, error); } }; - - ServiceCommand> request = new ServiceCommand>( - DLNAService.this, method, payload, playResponseListener); + + ServiceCommand> request = new ServiceCommand>(DLNAService.this, method, payload, playResponseListener); request.send(); } - + @Override public void onError(ServiceCommandError error) { Util.postError(listener, error); @@ -247,140 +218,119 @@ public void onError(ServiceCommandError error) { }; String method = "SetAVTransportURI"; - JSONObject httpMessage = getSetAVTransportURIBody(method, instanceId, - url, mMimeType, title); + JSONObject httpMessage = getSetAVTransportURIBody(method, instanceId, url, mMimeType, title); - ServiceCommand> request = new ServiceCommand>( - DLNAService.this, method, httpMessage, responseListener); + ServiceCommand> request = new ServiceCommand>(DLNAService.this, method, httpMessage, responseListener); request.send(); } - + @Override - public void displayImage(String url, String mimeType, String title, - String description, String iconSrc, LaunchListener listener) { + public void displayImage(String url, String mimeType, String title, String description, String iconSrc, LaunchListener listener) { displayMedia(url, mimeType, title, description, iconSrc, listener); } - + @Override public void displayImage(MediaInfo mediaInfo, LaunchListener listener) { - ImageInfo imageInfo = mediaInfo.getImages().get(0); - String iconSrc = imageInfo.getUrl(); - - displayImage(mediaInfo.getUrl(), mediaInfo.getMimeType(), - mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, - listener); + ImageInfo imageInfo = mediaInfo.getImages().get(0); + String iconSrc = imageInfo.getUrl(); + + displayImage(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, listener); } - + @Override - public void playMedia(String url, String mimeType, String title, - String description, String iconSrc, boolean shouldLoop, - LaunchListener listener) { + public void playMedia(String url, String mimeType, String title, String description, String iconSrc, boolean shouldLoop, LaunchListener listener) { displayMedia(url, mimeType, title, description, iconSrc, listener); - // stop(new ResponseListener() { - // - // @Override - // public void onError(ServiceCommandError error) { - // Util.postError(listener, error); - // } - // - // @Override - // public void onSuccess(Object object) { - // 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); - // - // String shareXML = - // String.format("" - // + - // "" - // + - // "" + - // "" - // + - // "0" + - // "%s" + - // "" + - // "<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:dc="http://purl.org/dc/elements/1.1/"><item id="0" parentID="0" restricted="0"><dc:title>%s</dc:title><dc:description>%s</dc:description><res protocolInfo="http-get:*:%s:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01500000000000000000000000000000">%s</res><upnp:albumArtURI>%s</upnp:albumArtURI><upnp:class>object.item.%sItem</upnp:class></item></DIDL-Lite>" - // + - // "" + - // "" + - // "" + - // "", - // url, title, description, mMimeType, url, iconSrc, mediaType); - // - // String method = "SetAVTransportURI"; - // JSONObject obj = new JSONObject(); - // try { - // obj.put(ACTION, String.format(ACTION_CONTENT, method)); - // obj.put(DATA, shareXML); - // } catch (JSONException e) { - // e.printStackTrace(); - // } - // - // ResponseListener playResponseListener = new - // ResponseListener () { - // @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)); - // } - // - // @Override - // public void onError(ServiceCommandError error) { - // if (listener != null) { - // listener.onError(error); - // } - // } - // }; - // - // ServiceCommand> request = new - // ServiceCommand>(DLNAService.this, method, - // obj, playResponseListener); - // request.send(); - // } - // }); +// stop(new ResponseListener() { +// +// @Override +// public void onError(ServiceCommandError error) { +// Util.postError(listener, error); +// } +// +// @Override +// public void onSuccess(Object object) { +// 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); +// +// String shareXML = String.format("" + +// "" + +// "" + +// "" + +// "0" + +// "%s" + +// "" + +// "<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:dc="http://purl.org/dc/elements/1.1/"><item id="0" parentID="0" restricted="0"><dc:title>%s</dc:title><dc:description>%s</dc:description><res protocolInfo="http-get:*:%s:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01500000000000000000000000000000">%s</res><upnp:albumArtURI>%s</upnp:albumArtURI><upnp:class>object.item.%sItem</upnp:class></item></DIDL-Lite>" + +// "" + +// "" + +// "" + +// "", +// url, title, description, mMimeType, url, iconSrc, mediaType); +// +// String method = "SetAVTransportURI"; +// JSONObject obj = new JSONObject(); +// try { +// obj.put(ACTION, String.format(ACTION_CONTENT, method)); +// obj.put(DATA, shareXML); +// } catch (JSONException e) { +// e.printStackTrace(); +// } +// +// ResponseListener playResponseListener = new ResponseListener () { +// @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)); +// } +// +// @Override +// public void onError(ServiceCommandError error) { +// if (listener != null) { +// listener.onError(error); +// } +// } +// }; +// +// ServiceCommand> request = new ServiceCommand>(DLNAService.this, method, obj, playResponseListener); +// request.send(); +// } +// }); } - + @Override public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, LaunchListener listener) { - ImageInfo imageInfo = mediaInfo.getImages().get(0); - String iconSrc = imageInfo.getUrl(); - - playMedia(mediaInfo.getUrl(), mediaInfo.getMimeType(), - mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, - shouldLoop, listener); + ImageInfo imageInfo = mediaInfo.getImages().get(0); + String iconSrc = imageInfo.getUrl(); + + playMedia(mediaInfo.getUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, shouldLoop, listener); } - + @Override - public void closeMedia(LaunchSession launchSession, - ResponseListener listener) { + public void closeMedia(LaunchSession launchSession, ResponseListener listener) { if (launchSession.getService() instanceof DLNAService) ((DLNAService) launchSession.getService()).stop(listener); } - + /****************** - * MEDIA CONTROL - *****************/ + MEDIA CONTROL + *****************/ @Override public MediaControl getMediaControl() { return this; }; - + @Override public CapabilityPriorityLevel getMediaControlCapabilityLevel() { return CapabilityPriorityLevel.NORMAL; @@ -388,115 +338,108 @@ public CapabilityPriorityLevel getMediaControlCapabilityLevel() { @Override public void play(ResponseListener listener) { - String method = "Play"; + String method = "Play"; String instanceId = "0"; Map parameters = new HashMap(); parameters.put("Speed", "1"); - + JSONObject payload = getMethodBody(instanceId, method, parameters); - ServiceCommand> request = new ServiceCommand>( - this, method, payload, listener); + ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); request.send(); } @Override public void pause(ResponseListener listener) { - String method = "Pause"; + String method = "Pause"; String instanceId = "0"; JSONObject payload = getMethodBody(instanceId, method); - ServiceCommand> request = new ServiceCommand>( - this, method, payload, listener); + ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); request.send(); } @Override public void stop(ResponseListener listener) { - String method = "Stop"; + String method = "Stop"; String instanceId = "0"; JSONObject payload = getMethodBody(instanceId, method); - ServiceCommand> request = new ServiceCommand>( - this, method, payload, listener); + ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); request.send(); } - @Override - public void rewind(ResponseListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } + @Override + public void rewind(ResponseListener listener) { + Util.postError(listener, ServiceCommandError.notSupported()); + } - @Override - public void fastForward(ResponseListener listener) { - Util.postError(listener, ServiceCommandError.notSupported()); - } + @Override + public void fastForward(ResponseListener listener) { + Util.postError(listener, ServiceCommandError.notSupported()); + } @Override public void previous(ResponseListener listener) { - String method = "Previous"; - String instanceId = "0"; + String method = "Previous"; + String instanceId = "0"; - JSONObject payload = getMethodBody(instanceId, method); + JSONObject payload = getMethodBody(instanceId, method); - ServiceCommand> request = new ServiceCommand>( - this, method, payload, listener); - request.send(); + ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); + request.send(); } @Override public void next(ResponseListener listener) { - String method = "Next"; - String instanceId = "0"; + String method = "Next"; + String instanceId = "0"; - JSONObject payload = getMethodBody(instanceId, method); + JSONObject payload = getMethodBody(instanceId, method); - ServiceCommand> request = new ServiceCommand>( - this, method, payload, listener); - request.send(); + ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); + request.send(); } - + @Override public void seek(long position, ResponseListener listener) { - String method = "Seek"; + String method = "Seek"; String instanceId = "0"; - + 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); - + String time = String.format(Locale.US, "%02d:%02d:%02d", hour, minute, second); + Map parameters = new HashMap(); parameters.put("Unit", "REL_TIME"); parameters.put("Target", time); JSONObject payload = getMethodBody(instanceId, method, parameters); - ServiceCommand> request = new ServiceCommand>( - this, method, payload, listener); + ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); request.send(); } - + private void getPositionInfo(final PositionInfoListener listener) { - String method = "GetPositionInfo"; + String method = "GetPositionInfo"; String instanceId = "0"; JSONObject payload = getMethodBody(instanceId, method); - + ResponseListener responseListener = new ResponseListener() { - + @Override public void onSuccess(Object response) { if (listener != null) { - listener.onGetPositionInfoSuccess((String) response); + listener.onGetPositionInfoSuccess((String)response); } } - + @Override public void onError(ServiceCommandError error) { if (listener != null) { @@ -505,44 +448,43 @@ public void onError(ServiceCommandError error) { } }; - ServiceCommand> request = new ServiceCommand>( - this, method, payload, responseListener); + ServiceCommand> request = new ServiceCommand>(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"); - + long milliTimes = convertStrTimeFormatToLong(strDuration) * 1000; - + Util.postSuccess(listener, milliTimes); } - + @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) * 1000; - + Util.postSuccess(listener, milliTimes); } - + @Override public void onGetPositionInfoFailed(ServiceCommandError error) { Util.postError(listener, error); @@ -550,117 +492,76 @@ public void onGetPositionInfoFailed(ServiceCommandError error) { }); } - protected JSONObject getSetAVTransportURIBody(String method, - String instanceId, String mediaURL, String mime, String title) { + protected JSONObject getSetAVTransportURIBody(String method, String instanceId, String mediaURL, String mime, String title) { String action = "SetAVTransportURI"; String metadata = getMetadata(mediaURL, mime, title); - - StringBuilder sb = new StringBuilder(); - sb.append(""); - sb.append(""); - - sb.append(""); - sb.append(""); - sb.append("" + instanceId + ""); - sb.append("" + mediaURL + ""); - sb.append("" + metadata + ""); - - sb.append(""); - sb.append(""); - sb.append(""); - - JSONObject obj = new JSONObject(); - try { + + StringBuilder sb = new StringBuilder(); + sb.append(""); + sb.append(""); + + sb.append(""); + sb.append(""); + sb.append("" + instanceId + ""); + sb.append("" + mediaURL + ""); + sb.append(""+ metadata + ""); + + sb.append(""); + sb.append(""); + sb.append(""); + + JSONObject obj = new JSONObject(); + try { obj.put(DATA, sb.toString()); obj.put(ACTION, String.format(ACTION_CONTENT, method)); } catch (JSONException e) { e.printStackTrace(); } - return obj; + return obj; } - + protected JSONObject getMethodBody(String instanceId, String method) { return getMethodBody(instanceId, method, null); } - protected JSONObject getMethodBody(String instanceId, String method, - Map parameters) { - StringBuilder sb = new StringBuilder(); - - sb.append(""); - sb.append(""); - - sb.append(""); - sb.append(""); - sb.append("" + instanceId + ""); - - if (parameters != null) { - for (Map.Entry entry : parameters.entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - - sb.append("<" + key + ">"); - sb.append(value); - sb.append(""); - } - } - - sb.append(""); - sb.append(""); - sb.append(""); - - JSONObject obj = new JSONObject(); - try { + protected JSONObject getMethodBody(String instanceId, String method, Map parameters) { + StringBuilder sb = new StringBuilder(); + + sb.append(""); + sb.append(""); + + sb.append(""); + sb.append(""); + sb.append("" + instanceId + ""); + + if (parameters != null) { + for (Map.Entry entry : parameters.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + + sb.append("<" + key + ">"); + sb.append(value); + sb.append(""); + } + } + + sb.append(""); + sb.append(""); + sb.append(""); + + JSONObject obj = new JSONObject(); + try { obj.put(DATA, sb.toString()); obj.put(ACTION, String.format(ACTION_CONTENT, method)); } catch (JSONException e) { e.printStackTrace(); } - return obj; + return obj; } - protected JSONObject getRenderingControlMethodBody(String instanceId, - String method, String channel, String value) { - StringBuilder sb = new StringBuilder(); - - sb.append(""); - sb.append(""); - - sb.append(""); - sb.append(""); - sb.append("" + instanceId + ""); - sb.append("" + channel + ""); - if (method.equals("SetVolume")) - sb.append("" + value + ""); - else if (method.equals("SetMute")) sb.append("" + value + ""); - - sb.append(""); - sb.append(""); - sb.append(""); - - JSONObject obj = new JSONObject(); - try { - obj.put(DATA, sb.toString()); - obj.put(ACTION, String.format(ACTION_CONTENT_RENDER, method)); - } catch (JSONException e) { - e.printStackTrace(); - } - - return obj; - } - - protected JSONObject getRenderingControlMethodBody(String instanceId, - String method, String channel) { - return getRenderingControlMethodBody(instanceId, method, channel, null); - } - - protected String getMetadata(String mediaURL, String mime, String title) { + protected String getMetadata(String mediaURL, String mime, String title) { String id = "1000"; String parentID = "0"; String restricted = "0"; @@ -671,74 +572,63 @@ protected String getMetadata(String mediaURL, String mime, String title) { sb.append("xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" "); sb.append("xmlns:dc="http://purl.org/dc/elements/1.1/">"); - sb.append("<item id="" + id + "" parentID="" - + parentID + "" restricted="" + restricted - + "">"); + sb.append("<item id="" + id + "" parentID="" + parentID + "" restricted="" + restricted + "">"); sb.append("<dc:title>" + title + "</dc:title>"); - + if (mime.startsWith("image")) { objectClass = "object.item.imageItem"; - } else if (mime.startsWith("video")) { + } + else if (mime.startsWith("video")) { objectClass = "object.item.videoItem"; - } else if (mime.startsWith("audio")) { + } + else if (mime.startsWith("audio")) { objectClass = "object.item.audioItem"; } - sb.append("<res protocolInfo="http-get:*:" + mime - + ":DLNA.ORG_OP=01">" + mediaURL + "</res>"); + sb.append("<res protocolInfo="http-get:*:" + mime + ":DLNA.ORG_OP=01">" + mediaURL + "</res>"); sb.append("<upnp:class>" + objectClass + "</upnp:class>"); sb.append("</item>"); sb.append("</DIDL-Lite>"); - + return sb.toString(); } - + @Override public void sendCommand(final ServiceCommand mCommand) { Util.runInBackground(new Runnable() { - + @SuppressWarnings("unchecked") @Override public void run() { ServiceCommand> command = (ServiceCommand>) mCommand; - - JSONObject payload = (JSONObject) command.getPayload(); - - HttpPost request; - - if ((command.getTarget().contains("Volume")) || (command.getTarget().contains("Mute"))) { - request = HttpMessage.getDLNAHttpPostRenderControl(renderURL, - command.getTarget());} - else {request = HttpMessage.getDLNAHttpPost(controlURL, - command.getTarget());} + JSONObject payload = (JSONObject) command.getPayload(); + + HttpPost request = HttpMessage.getDLNAHttpPost(controlURL, command.getTarget()); request.setHeader(ACTION, payload.optString(ACTION)); try { - request.setEntity(new StringEntity(payload.optString(DATA) - .toString())); + request.setEntity(new StringEntity(payload.optString(DATA).toString())); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } - - HttpResponse response = null; + HttpResponse response = null; + try { response = httpClient.execute(request); - + int code = response.getStatusLine().getStatusCode(); - if (code == 200) { - HttpEntity entity = response.getEntity(); - String message = EntityUtils.toString(entity, "UTF-8"); - + if (code == 200) { + HttpEntity entity = response.getEntity(); + String message = EntityUtils.toString(entity, "UTF-8"); + Util.postSuccess(command.getResponseListener(), message); - } else { - Util.postError(command.getResponseListener(), - ServiceCommandError.getError(code)); - - } - + else { + Util.postError(command.getResponseListener(), ServiceCommandError.getError(code)); + } + response.getEntity().consumeContent(); } catch (ClientProtocolException e) { e.printStackTrace(); @@ -748,16 +638,16 @@ public void run() { } }); } - + @Override protected void updateCapabilities() { List capabilities = new ArrayList(); - + capabilities.add(Display_Image); capabilities.add(Display_Video); capabilities.add(Display_Audio); capabilities.add(Close); - + capabilities.add(MetaData_Title); capabilities.add(MetaData_MimeType); @@ -770,165 +660,144 @@ protected void updateCapabilities() { capabilities.add(PlayState); capabilities.add(PlayState_Subscribe); - 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 { + public LaunchSession decodeLaunchSession(String type, JSONObject sessionObj) throws JSONException { if (type == "dlna") { - LaunchSession launchSession = LaunchSession - .launchSessionFromJSONObject(sessionObj); + LaunchSession launchSession = LaunchSession.launchSessionFromJSONObject(sessionObj); launchSession.setService(this); return launchSession; } return null; } - + private String parseData(String response, String key) { String startTag = "<" + key + ">"; String endTag = ""; - + int start = response.indexOf(startTag); int end = response.indexOf(endTag); - + String data = response.substring(start + startTag.length(), end); - + return data; } - + private long convertStrTimeFormatToLong(String strTime) { String[] tokens = strTime.split(":"); long time = 0; - + for (int i = 0; i < tokens.length; i++) { time *= 60; time += Integer.parseInt(tokens[i]); } - + return time; } @Override public void getPlayState(final PlayStateListener listener) { - String method = "GetTransportInfo"; + String method = "GetTransportInfo"; String instanceId = "0"; JSONObject payload = getMethodBody(instanceId, method); - + ResponseListener responseListener = new ResponseListener() { - + @Override public void onSuccess(Object response) { - String transportState = parseData((String) response, - "CurrentTransportState"); - PlayStateStatus status = PlayStateStatus - .convertTransportStateToPlayStateStatus(transportState); - + 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> request = new ServiceCommand>( - this, method, payload, responseListener); + ServiceCommand> request = new ServiceCommand>(this, method, payload, responseListener); request.send(); } - + @Override - public ServiceSubscription subscribePlayState( - PlayStateListener listener) { - URLServiceSubscription request = new URLServiceSubscription( - this, PLAY_STATE, null, null); + public ServiceSubscription subscribePlayState(PlayStateListener listener) { + URLServiceSubscription request = new URLServiceSubscription(this, PLAY_STATE, null, null); request.addListener(listener); addSubscription(request); return request; } - + private void addSubscription(URLServiceSubscription subscription) { httpServer.getSubscriptions().add(subscription); } - + @Override public void unsubscribe(URLServiceSubscription subscription) { -// if (subscription!=null) -// httpServer.getSubscriptions().remove(subscription); - unsubscribeEvents(); + httpServer.getSubscriptions().remove(subscription); } - + @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(); - + // 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; - + Util.runInBackground(new Runnable() { - + @Override public void run() { httpServer = new DLNAHttpServer(); httpServer.start(); } }); - + subscribeEvents(); - + reportConnected(true); } - + @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); } }); - + unsubscribeEvents(); - + if (httpServer != null) { httpServer.stop(); httpServer = null; } } - + @Override public void onLoseReachability(DeviceServiceReachability reachability) { if (connected) { @@ -937,47 +806,39 @@ public void onLoseReachability(DeviceServiceReachability reachability) { mServiceReachability.stop(); } } - + public void subscribeEvents() { Util.runInBackground(new Runnable() { @Override public void run() { - String myIpAddress = null; + String myIpAddress = null; try { myIpAddress = Util.getIpAddress(context).getHostAddress(); } catch (UnknownHostException e) { e.printStackTrace(); } - - HttpHost host = new HttpHost(serviceDescription.getIpAddress(), - serviceDescription.getPort()); + + HttpHost host = new HttpHost(serviceDescription.getIpAddress(), serviceDescription.getPort()); List serviceList = serviceDescription.getServiceList(); if (serviceList != null) { for (int i = 0; i < serviceList.size(); i++) { - BasicHttpRequest request = new BasicHttpRequest( - SUBSCRIBE, serviceList.get(i).eventSubURL); - - request.setHeader( - "CALLBACK", - ""); + BasicHttpRequest request = new BasicHttpRequest(SUBSCRIBE, serviceList.get(i).eventSubURL); + + request.setHeader("CALLBACK", ""); request.setHeader("NT", "upnp:event"); request.setHeader("TIMEOUT", "Second-" + TIMEOUT); - + HttpResponse response = null; - + try { response = httpClient.execute(host, request); - + int code = response.getStatusLine().getStatusCode(); - if (code == 200) { - SIDList.put(serviceList.get(i).serviceType, - response.getFirstHeader("SID") - .getValue()); + if (code == 200) { + SIDList.put(serviceList.get(i).serviceType, response.getFirstHeader("SID").getValue()); } response.getEntity().consumeContent(); } catch (ClientProtocolException e) { @@ -989,47 +850,41 @@ public void run() { } } }); - + resubscribeEvent(); } - + public void resubscribeEvent() { resubscriptionTimer = new Timer(); resubscriptionTimer.scheduleAtFixedRate(new TimerTask() { - + @Override public void run() { Util.runInBackground(new Runnable() { @Override public void run() { - HttpHost host = new HttpHost(serviceDescription - .getIpAddress(), serviceDescription.getPort()); - List serviceList = serviceDescription - .getServiceList(); + HttpHost host = new HttpHost(serviceDescription.getIpAddress(), serviceDescription.getPort()); + List serviceList = serviceDescription.getServiceList(); if (serviceList != null) { for (int i = 0; i < serviceList.size(); i++) { String eventSubURL = serviceList.get(i).eventSubURL; String SID = SIDList.get(serviceList.get(i).serviceType); - - BasicHttpRequest request = new BasicHttpRequest( - SUBSCRIBE, eventSubURL); - - request.setHeader("TIMEOUT", "Second-" - + TIMEOUT); + + BasicHttpRequest request = new BasicHttpRequest(SUBSCRIBE, eventSubURL); + + request.setHeader("TIMEOUT", "Second-" + TIMEOUT); request.setHeader("SID", SID); - + HttpResponse response = null; - + try { - response = httpClient.execute(host, request); int code = response.getStatusLine().getStatusCode(); - - if (code == 200) { + if (code == 200) { } response.getEntity().consumeContent(); } catch (ClientProtocolException e) { @@ -1040,44 +895,38 @@ public void run() { } } } - }); + }); } - }, TIMEOUT / 2 * 1000, TIMEOUT / 2 * 1000); + }, TIMEOUT/2*1000, TIMEOUT/2*1000); } - + public void unsubscribeEvents() { - if (resubscriptionTimer != null) resubscriptionTimer.cancel(); - Util.runInBackground(new Runnable() { @Override public void run() { - final List serviceList = serviceDescription - .getServiceList(); + final List serviceList = serviceDescription.getServiceList(); if (serviceList != null) { for (int i = 0; i < serviceList.size(); i++) { - BasicHttpRequest request = new BasicHttpRequest( - UNSUBSCRIBE, serviceList.get(i).eventSubURL); - + BasicHttpRequest request = new BasicHttpRequest(UNSUBSCRIBE, serviceList.get(i).eventSubURL); + String sid = SIDList.get(serviceList.get(i).serviceType); request.setHeader("SID", sid); HttpResponse response = null; try { - HttpHost host = new HttpHost(serviceDescription - .getIpAddress(), serviceDescription - .getPort()); - + HttpHost host = new HttpHost(serviceDescription.getIpAddress(), serviceDescription.getPort()); + response = httpClient.execute(host, request); - + int code = response.getStatusLine().getStatusCode(); - if (code == 200) { - SIDList.remove(serviceList.get(i).serviceType); + if (code == 200) { + SIDList.remove(serviceList.get(i).serviceType); } response.getEntity().consumeContent(); } catch (ClientProtocolException e) { @@ -1090,220 +939,4 @@ public void run() { } }); } - - /****************** - * VOLUME CONTROL - *****************/ - - @Override - public VolumeControl getVolumeControl() { - return this; - } - - @Override - public CapabilityPriorityLevel getVolumeControlCapabilityLevel() { - return CapabilityPriorityLevel.NORMAL; - } - - @Override - public void volumeUp(final ResponseListener 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 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 listener) { - - String method = "SetVolume"; - String instanceId = "0"; - String channel = "Master"; - String value = String.valueOf((int)(volume*100)); - - JSONObject payload = getRenderingControlMethodBody(instanceId, method, - channel, value); - - ServiceCommand> request = new ServiceCommand>( - this, method, payload, listener); - request.send(); - - } - - private ServiceCommand getVolumeStatus(Boolean isSubscription, - final VolumeListener listener) { - - String method = "GetVolume"; - String instanceId = "0"; - String channel = "Master"; - - ServiceCommand request; - - JSONObject payload = getRenderingControlMethodBody(instanceId, method, - channel); - - ResponseListener responseListener = new ResponseListener() { - - @Override - public void onSuccess(Object response) { - String currentVolume = parseData((String) response, - "CurrentVolume"); - int iVolume = Integer.parseInt(currentVolume); - float fVolume = (float) (iVolume / 100.0); - - Util.postSuccess(listener, fVolume); - } - - @Override - public void onError(ServiceCommandError error) { - Util.postError(listener, error); - } - }; - - if (isSubscription) - request = new URLServiceSubscription(this, method, - payload, responseListener); - else - request = new ServiceCommand(this, method, payload, - responseListener); - - request.send(); - - return request; - - } - - @Override - public void getVolume(VolumeListener listener) { - - getVolumeStatus(false, listener); - } - - @Override - public void setMute(boolean isMute, ResponseListener listener) { - - String method = "SetMute"; - String instanceId = "0"; - String channel = "Master"; - String value = String.valueOf(isMute); - - JSONObject payload = getRenderingControlMethodBody(instanceId, method, - channel, value); - - ServiceCommand> request = new ServiceCommand>( - this, method, payload, listener); - request.send(); - - } - - - private ServiceCommand> getMuteStatus(boolean isSubscription, final MuteListener listener) { - - String method = "GetMute"; - String instanceId = "0"; - String channel = "Master"; - - ServiceCommand> request; - - JSONObject payload = getRenderingControlMethodBody(instanceId, method, - channel); - - ResponseListener responseListener = new ResponseListener() { - - @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); - } - }; - - if (isSubscription) - request = new URLServiceSubscription>(this, method , payload, responseListener); - else - request = new ServiceCommand>(this, method, payload, responseListener); - - request.send(); - - return request; - } - - @Override - public void getMute(MuteListener listener) { - getMuteStatus(false, listener); - } - - @Override - public ServiceSubscription subscribeVolume( - VolumeListener listener) { - URLServiceSubscription request = new URLServiceSubscription( - this, "volume", null, null); - request.addListener(listener); - addSubscription(request); - return request; } - - @Override - public ServiceSubscription subscribeMute(MuteListener listener) { - URLServiceSubscription request = new URLServiceSubscription( - this, "mute", null, null); - request.addListener(listener); - addSubscription(request); - return request; - } - } diff --git a/src/com/connectsdk/service/upnp/DLNAHttpServer.java b/src/com/connectsdk/service/upnp/DLNAHttpServer.java index 0ac6cc92..cdac1975 100644 --- a/src/com/connectsdk/service/upnp/DLNAHttpServer.java +++ b/src/com/connectsdk/service/upnp/DLNAHttpServer.java @@ -18,7 +18,6 @@ import org.xmlpull.v1.XmlPullParserException; import android.text.Html; -import android.util.Log; import com.connectsdk.core.Util; import com.connectsdk.service.capability.MediaControl.PlayStateStatus; @@ -27,39 +26,39 @@ public class DLNAHttpServer { ServerSocket welcomeSocket; - + int port = 49291; boolean running = false; List> subscriptions; - + public DLNAHttpServer() { - subscriptions = new ArrayList>(); + subscriptions = new ArrayList>(); } public void start() { if (running) return; - + running = true; - + try { welcomeSocket = new ServerSocket(this.port); } catch (IOException ex) { ex.printStackTrace(); } - + while (running) { if (welcomeSocket == null || welcomeSocket.isClosed()) { stop(); break; } - + Socket connectionSocket = null; BufferedReader inFromClient = null; DataOutputStream outToClient = null; - + try { connectionSocket = welcomeSocket.accept(); } catch (IOException ex) { @@ -68,28 +67,27 @@ public void start() { stop(); return; } - + int c = 0; - + String body = null; - + try { - inFromClient = new BufferedReader(new InputStreamReader( - connectionSocket.getInputStream())); + inFromClient = new BufferedReader(new InputStreamReader(connectionSocket.getInputStream())); StringBuilder sb = new StringBuilder(); - + while ((c = inFromClient.read()) != -1) { - sb.append((char) c); + sb.append((char)c); if (sb.toString().endsWith("\r\n\r\n")) break; } - + sb = new StringBuilder(); - + while ((c = inFromClient.read()) != -1) { - sb.append((char) c); + sb.append((char)c); body = sb.toString(); if (body.endsWith("")) @@ -100,12 +98,11 @@ public void start() { } catch (IOException ex) { ex.printStackTrace(); } - + PrintWriter out = null; - + try { - outToClient = new DataOutputStream( - connectionSocket.getOutputStream()); + outToClient = new DataOutputStream(connectionSocket.getOutputStream()); out = new PrintWriter(outToClient); out.println("HTTP/1.1 200 OK"); out.println("Connection: Close"); @@ -124,78 +121,35 @@ public void start() { ex.printStackTrace(); } } - + InputStream stream = null; - + try { stream = new ByteArrayInputStream(body.getBytes("UTF-8")); } catch (UnsupportedEncodingException ex) { ex.printStackTrace(); } - + JSONObject event; DLNANotifyParser parser = new DLNANotifyParser(); - + try { event = parser.parse(stream); - + if (!event.isNull("TransportState")) { String transportState = event.getString("TransportState"); - PlayStateStatus status = PlayStateStatus - .convertTransportStateToPlayStateStatus(transportState); - - for (URLServiceSubscription sub : subscriptions) { + PlayStateStatus status = PlayStateStatus.convertTransportStateToPlayStateStatus(transportState); + + for (URLServiceSubscription sub: subscriptions) { if (sub.getTarget().equalsIgnoreCase("playState")) { for (int i = 0; i < sub.getListeners().size(); i++) { @SuppressWarnings("unchecked") - ResponseListener listener = (ResponseListener) sub - .getListeners().get(i); + ResponseListener listener = (ResponseListener) sub.getListeners().get(i); Util.postSuccess(listener, status); } } } } - - if (event.has("Volume")) { - Log.d("LG", "Volume " + event.getString("Volume")); - if (event.getString("Volume") != null) { - - int intVolume = event.getInt("Volume"); - float volume = (float) intVolume / 100; - - for (URLServiceSubscription sub : subscriptions) { - if (sub.getTarget().equalsIgnoreCase("volume")) { - for (int i = 0; i < sub.getListeners().size(); i++) { - @SuppressWarnings("unchecked") - ResponseListener listener = (ResponseListener) sub - .getListeners().get(i); - Util.postSuccess(listener, volume); - } - } - } - } - - } - - if (event.has("Mute")) { - Log.d("LG", "Mute " + event.getString("Mute")); - - int intMute = event.getInt("Mute"); - boolean mute = (intMute==1) ? true : false; - - - for (URLServiceSubscription sub : subscriptions) { - if (sub.getTarget().equalsIgnoreCase("mute")) { - for (int i = 0; i < sub.getListeners().size(); i++) { - @SuppressWarnings("unchecked") - ResponseListener listener = (ResponseListener) sub - .getListeners().get(i); - Util.postSuccess(listener, mute); - } - } - } - } - } catch (XmlPullParserException e) { e.printStackTrace(); } catch (IOException e) { @@ -205,11 +159,12 @@ public void start() { } } } - + + public void stop() { if (!running) return; - + if (welcomeSocket != null && !welcomeSocket.isClosed()) { try { welcomeSocket.close(); @@ -217,15 +172,15 @@ public void stop() { ex.printStackTrace(); } } - + welcomeSocket = null; running = false; } - + public int getPort() { return port; } - + public List> getSubscriptions() { return subscriptions; } From d4e83764f6052d774a5a873ea5b112b8a40d38c0 Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Fri, 12 Sep 2014 13:29:59 -0700 Subject: [PATCH 43/76] Recommitted Add DLNA volume and mute control and subscriptions without auto-correction (https://github.com/ConnectSDK/Connect-SDK-Android/commit/f43c8761169edf25f462a13e24e9beb77bf45bbc) --- src/com/connectsdk/service/DLNAService.java | 300 ++++++++++++++++-- .../service/upnp/DLNAHttpServer.java | 36 +++ 2 files changed, 313 insertions(+), 23 deletions(-) diff --git a/src/com/connectsdk/service/DLNAService.java b/src/com/connectsdk/service/DLNAService.java index 5fef38b6..0dbc2162 100644 --- a/src/com/connectsdk/service/DLNAService.java +++ b/src/com/connectsdk/service/DLNAService.java @@ -58,6 +58,7 @@ import com.connectsdk.etc.helper.HttpMessage; import com.connectsdk.service.capability.MediaControl; import com.connectsdk.service.capability.MediaPlayer; +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; @@ -69,7 +70,7 @@ import com.connectsdk.service.sessions.LaunchSession.LaunchSessionType; import com.connectsdk.service.upnp.DLNAHttpServer; -public class DLNAService extends DeviceService implements MediaControl, MediaPlayer { +public class DLNAService extends DeviceService implements MediaControl, MediaPlayer, VolumeControl { public static final String ID = "DLNA"; private static final String SUBSCRIBE = "SUBSCRIBE"; @@ -77,9 +78,12 @@ public class DLNAService extends DeviceService implements MediaControl, MediaPla private static final String DATA = "XMLData"; private static final String ACTION = "SOAPAction"; - private static final String ACTION_CONTENT = "\"urn:schemas-upnp-org:service:AVTransport:1#%s\""; - private static final String AV_TRANSPORT = "AVTransport"; + private static final String AV_TRANSPORT_URN = "\"urn:schemas-upnp-org:service:AVTransport:1#%s\""; + private static final String ACTION_CONTENT = "\"urn:schemas-upnp-org:service:AVTransport:1#%s\""; + private static final String ACTION_CONTENT_RENDER = "\"urn:schemas-upnp-org:service:RenderingControl:1#%s\""; + + private static final String AV_TRANSPORT = "AVTransport"; private static final String CONNECTION_MANAGER = "ConnectionManager"; private static final String RENDERING_CONTROL = "RenderingControl"; @@ -87,7 +91,7 @@ public class DLNAService extends DeviceService implements MediaControl, MediaPla Context context; - String controlURL; + String controlURL, renderURL; HttpClient httpClient; DLNAHttpServer httpServer; @@ -122,7 +126,7 @@ public static JSONObject discoveryParameters() { try { params.put("serviceId", ID); - params.put("filter", "urn:schemas-upnp-org:device:MediaRenderer:1"); + params.put("filter", "urn:schemas-upnp-org:device:MediaRenderer:1"); } catch (JSONException e) { e.printStackTrace(); } @@ -139,6 +143,8 @@ public void setServiceDescription(ServiceDescription serviceDescription) { private void updateControlURL() { StringBuilder sb = new StringBuilder(); + StringBuilder sb1 = new StringBuilder(); + int x =0; List serviceList = serviceDescription.getServiceList(); if (serviceList != null) { @@ -146,10 +152,18 @@ private void updateControlURL() { if (serviceList.get(i).serviceType.contains(AV_TRANSPORT)) { sb.append(serviceList.get(i).baseURL); sb.append(serviceList.get(i).controlURL); - break; + x++; + if (x==2) break; + } + else if (serviceList.get(i).serviceType.contains(RENDERING_CONTROL)) { + sb1.append(serviceList.get(i).baseURL); + sb1.append(serviceList.get(i).controlURL); + x++; + if (x==2) break; } } controlURL = sb.toString(); + renderURL = sb1.toString(); } } @@ -513,7 +527,7 @@ protected JSONObject getSetAVTransportURIBody(String method, String instanceId, JSONObject obj = new JSONObject(); try { obj.put(DATA, sb.toString()); - obj.put(ACTION, String.format(ACTION_CONTENT, method)); + obj.put(ACTION, String.format(AV_TRANSPORT_URN, method)); } catch (JSONException e) { e.printStackTrace(); } @@ -553,13 +567,47 @@ protected JSONObject getMethodBody(String instanceId, String method, Map"); + sb.append(""); + + sb.append(""); + sb.append(""); + sb.append("" + instanceId + ""); + sb.append("" + channel + ""); + if (method.equals("SetVolume")) + sb.append("" + value + ""); + else if (method.equals("SetMute")) + sb.append("" + value + ""); + + sb.append(""); + sb.append(""); + sb.append(""); + + JSONObject obj = new JSONObject(); + try { + obj.put(DATA, sb.toString()); + obj.put(ACTION, String.format(ACTION_CONTENT_RENDER, method)); + } catch (JSONException e) { + e.printStackTrace(); + } + + return obj; + } + + protected JSONObject getRenderingControlMethodBody(String instanceId, String method, String channel) { + return getRenderingControlMethodBody(instanceId, method, channel, null); + } protected String getMetadata(String mediaURL, String mime, String title) { String id = "1000"; @@ -596,39 +644,47 @@ else if (mime.startsWith("audio")) { @Override public void sendCommand(final ServiceCommand mCommand) { Util.runInBackground(new Runnable() { - + @SuppressWarnings("unchecked") @Override public void run() { ServiceCommand> command = (ServiceCommand>) mCommand; - + JSONObject payload = (JSONObject) command.getPayload(); - - HttpPost request = HttpMessage.getDLNAHttpPost(controlURL, command.getTarget()); + + HttpPost request; + + if ((command.getTarget().contains("Volume")) || (command.getTarget().contains("Mute"))) { + request = HttpMessage.getDLNAHttpPostRenderControl(renderURL, command.getTarget()); + } + else { + request = HttpMessage.getDLNAHttpPost(controlURL, command.getTarget()); + } request.setHeader(ACTION, payload.optString(ACTION)); try { - request.setEntity(new StringEntity(payload.optString(DATA).toString())); + request.setEntity(new StringEntity(payload.optString(DATA) + .toString())); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } - - HttpResponse response = null; + HttpResponse response = null; + try { response = httpClient.execute(request); - + int code = response.getStatusLine().getStatusCode(); - if (code == 200) { - HttpEntity entity = response.getEntity(); - String message = EntityUtils.toString(entity, "UTF-8"); - + if (code == 200) { + HttpEntity entity = response.getEntity(); + String message = EntityUtils.toString(entity, "UTF-8"); + Util.postSuccess(command.getResponseListener(), message); - } + } else { Util.postError(command.getResponseListener(), ServiceCommandError.getError(code)); } - + response.getEntity().consumeContent(); } catch (ClientProtocolException e) { e.printStackTrace(); @@ -660,6 +716,14 @@ protected void updateCapabilities() { capabilities.add(PlayState); capabilities.add(PlayState_Subscribe); + 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); } @@ -903,7 +967,7 @@ public void run() { public void unsubscribeEvents() { if (resubscriptionTimer != null) resubscriptionTimer.cancel(); - + Util.runInBackground(new Runnable() { @Override @@ -939,4 +1003,194 @@ public void run() { } }); } + + @Override + public VolumeControl getVolumeControl() { + return this; + } + + @Override + public CapabilityPriorityLevel getVolumeControlCapabilityLevel() { + return CapabilityPriorityLevel.NORMAL; + } + + @Override + public void volumeUp(final ResponseListener 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 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 listener) { + String method = "SetVolume"; + String instanceId = "0"; + String channel = "Master"; + String value = String.valueOf((int)(volume*100)); + + JSONObject payload = getRenderingControlMethodBody(instanceId, method, channel, value); + + ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); + request.send(); + } + + private ServiceCommand getVolumeStatus(Boolean isSubscription, final VolumeListener listener) { + String method = "GetVolume"; + String instanceId = "0"; + String channel = "Master"; + + ServiceCommand request; + + JSONObject payload = getRenderingControlMethodBody(instanceId, method, channel); + + ResponseListener responseListener = new ResponseListener() { + + @Override + public void onSuccess(Object response) { + String currentVolume = parseData((String) response, "CurrentVolume"); + int iVolume = Integer.parseInt(currentVolume); + float fVolume = (float) (iVolume / 100.0); + + Util.postSuccess(listener, fVolume); + } + + @Override + public void onError(ServiceCommandError error) { + Util.postError(listener, error); + } + }; + + if (isSubscription) + request = new URLServiceSubscription(this, method, payload, responseListener); + else + request = new ServiceCommand(this, method, payload, responseListener); + + request.send(); + + return request; + } + + @Override + public void getVolume(VolumeListener listener) { + getVolumeStatus(false, listener); + } + + @Override + public void setMute(boolean isMute, ResponseListener listener) { + String method = "SetMute"; + String instanceId = "0"; + String channel = "Master"; + String value = String.valueOf(isMute); + + JSONObject payload = getRenderingControlMethodBody(instanceId, method, channel, value); + + ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); + request.send(); + } + + private ServiceCommand> getMuteStatus(boolean isSubscription, final MuteListener listener) { + String method = "GetMute"; + String instanceId = "0"; + String channel = "Master"; + + ServiceCommand> request; + + JSONObject payload = getRenderingControlMethodBody(instanceId, method, channel); + + ResponseListener responseListener = new ResponseListener() { + + @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); + } + }; + + if (isSubscription) + request = new URLServiceSubscription>(this, method , payload, responseListener); + else + request = new ServiceCommand>(this, method, payload, responseListener); + + request.send(); + + return request; + } + + @Override + public void getMute(MuteListener listener) { + getMuteStatus(false, listener); + } + + @Override + public ServiceSubscription subscribeVolume(VolumeListener listener) { + URLServiceSubscription request = new URLServiceSubscription(this, "volume", null, null); + request.addListener(listener); + addSubscription(request); + return request; + } + + @Override + public ServiceSubscription subscribeMute(MuteListener listener) { + URLServiceSubscription request = new URLServiceSubscription(this, "mute", null, null); + request.addListener(listener); + addSubscription(request); + return request; + } } diff --git a/src/com/connectsdk/service/upnp/DLNAHttpServer.java b/src/com/connectsdk/service/upnp/DLNAHttpServer.java index cdac1975..5db78745 100644 --- a/src/com/connectsdk/service/upnp/DLNAHttpServer.java +++ b/src/com/connectsdk/service/upnp/DLNAHttpServer.java @@ -150,6 +150,42 @@ public void start() { } } } + + if (event.has("Volume")) { + if (event.getString("Volume") != null) { + System.out.println("[DEBUG] Volume " + event.getString("Volume")); + + int intVolume = event.getInt("Volume"); + float volume = (float) intVolume / 100; + + for (URLServiceSubscription sub : subscriptions) { + if (sub.getTarget().equalsIgnoreCase("volume")) { + for (int i = 0; i < sub.getListeners().size(); i++) { + @SuppressWarnings("unchecked") + ResponseListener listener = (ResponseListener) sub.getListeners().get(i); + Util.postSuccess(listener, volume); + } + } + } + } + } + + if (event.has("Mute")) { + System.out.println("[DEBUG] Mute " + event.getString("Mute")); + + int intMute = event.getInt("Mute"); + boolean mute = (intMute==1) ? true : false; + + for (URLServiceSubscription sub : subscriptions) { + if (sub.getTarget().equalsIgnoreCase("mute")) { + for (int i = 0; i < sub.getListeners().size(); i++) { + @SuppressWarnings("unchecked") + ResponseListener listener = (ResponseListener) sub.getListeners().get(i); + Util.postSuccess(listener, mute); + } + } + } + } } catch (XmlPullParserException e) { e.printStackTrace(); } catch (IOException e) { From 7ee739f3e109863fb5804734e2d536b3ed2a8f9d Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Fri, 12 Sep 2014 13:35:33 -0700 Subject: [PATCH 44/76] Removed comments that will not use --- src/com/connectsdk/service/DLNAService.java | 66 --------------------- 1 file changed, 66 deletions(-) diff --git a/src/com/connectsdk/service/DLNAService.java b/src/com/connectsdk/service/DLNAService.java index 0dbc2162..2abba79b 100644 --- a/src/com/connectsdk/service/DLNAService.java +++ b/src/com/connectsdk/service/DLNAService.java @@ -254,72 +254,6 @@ public void displayImage(MediaInfo mediaInfo, LaunchListener listener) { @Override public void playMedia(String url, String mimeType, String title, String description, String iconSrc, boolean shouldLoop, LaunchListener listener) { displayMedia(url, mimeType, title, description, iconSrc, listener); -// stop(new ResponseListener() { -// -// @Override -// public void onError(ServiceCommandError error) { -// Util.postError(listener, error); -// } -// -// @Override -// public void onSuccess(Object object) { -// 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); -// -// String shareXML = String.format("" + -// "" + -// "" + -// "" + -// "0" + -// "%s" + -// "" + -// "<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:dc="http://purl.org/dc/elements/1.1/"><item id="0" parentID="0" restricted="0"><dc:title>%s</dc:title><dc:description>%s</dc:description><res protocolInfo="http-get:*:%s:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01500000000000000000000000000000">%s</res><upnp:albumArtURI>%s</upnp:albumArtURI><upnp:class>object.item.%sItem</upnp:class></item></DIDL-Lite>" + -// "" + -// "" + -// "" + -// "", -// url, title, description, mMimeType, url, iconSrc, mediaType); -// -// String method = "SetAVTransportURI"; -// JSONObject obj = new JSONObject(); -// try { -// obj.put(ACTION, String.format(ACTION_CONTENT, method)); -// obj.put(DATA, shareXML); -// } catch (JSONException e) { -// e.printStackTrace(); -// } -// -// ResponseListener playResponseListener = new ResponseListener () { -// @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)); -// } -// -// @Override -// public void onError(ServiceCommandError error) { -// if (listener != null) { -// listener.onError(error); -// } -// } -// }; -// -// ServiceCommand> request = new ServiceCommand>(DLNAService.this, method, obj, playResponseListener); -// request.send(); -// } -// }); } @Override From 2ac4ea00efb430f9f63dc7e52df8347feb943d9a Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Fri, 12 Sep 2014 17:02:18 -0700 Subject: [PATCH 45/76] Refactored DLNA Service --- src/com/connectsdk/service/DLNAService.java | 265 ++++++------------ .../service/upnp/DLNAHttpServer.java | 53 ++-- 2 files changed, 117 insertions(+), 201 deletions(-) diff --git a/src/com/connectsdk/service/DLNAService.java b/src/com/connectsdk/service/DLNAService.java index 2abba79b..5d47f575 100644 --- a/src/com/connectsdk/service/DLNAService.java +++ b/src/com/connectsdk/service/DLNAService.java @@ -55,7 +55,6 @@ import com.connectsdk.core.upnp.service.Service; import com.connectsdk.discovery.DiscoveryManager; import com.connectsdk.etc.helper.DeviceServiceReachability; -import com.connectsdk.etc.helper.HttpMessage; import com.connectsdk.service.capability.MediaControl; import com.connectsdk.service.capability.MediaPlayer; import com.connectsdk.service.capability.VolumeControl; @@ -76,12 +75,9 @@ public class DLNAService extends DeviceService implements MediaControl, MediaPla private static final String SUBSCRIBE = "SUBSCRIBE"; private static final String UNSUBSCRIBE = "UNSUBSCRIBE"; - private static final String DATA = "XMLData"; - private static final String ACTION = "SOAPAction"; - - private static final String AV_TRANSPORT_URN = "\"urn:schemas-upnp-org:service:AVTransport:1#%s\""; - private static final String ACTION_CONTENT = "\"urn:schemas-upnp-org:service:AVTransport:1#%s\""; - private static final String ACTION_CONTENT_RENDER = "\"urn:schemas-upnp-org:service:RenderingControl:1#%s\""; + private static final String AV_TRANSPORT_URN = "urn:schemas-upnp-org:service:AVTransport:1"; + private static final String CONNECTION_MANAGER_URN = "urn:schemas-upnp-org:service:ConnectionManager:1"; + private static final String RENDERING_CONTROL_URN = "urn:schemas-upnp-org:service:RenderingControl:1"; private static final String AV_TRANSPORT = "AVTransport"; private static final String CONNECTION_MANAGER = "ConnectionManager"; @@ -91,7 +87,7 @@ public class DLNAService extends DeviceService implements MediaControl, MediaPla Context context; - String controlURL, renderURL; + String avTransportURL, renderingControlURL; HttpClient httpClient; DLNAHttpServer httpServer; @@ -142,28 +138,17 @@ public void setServiceDescription(ServiceDescription serviceDescription) { } private void updateControlURL() { - StringBuilder sb = new StringBuilder(); - StringBuilder sb1 = new StringBuilder(); - int x =0; List serviceList = serviceDescription.getServiceList(); if (serviceList != null) { for (int i = 0; i < serviceList.size(); i++) { if (serviceList.get(i).serviceType.contains(AV_TRANSPORT)) { - sb.append(serviceList.get(i).baseURL); - sb.append(serviceList.get(i).controlURL); - x++; - if (x==2) break; + avTransportURL = String.format("%s%s", serviceList.get(i).baseURL, serviceList.get(i).controlURL); } else if (serviceList.get(i).serviceType.contains(RENDERING_CONTROL)) { - sb1.append(serviceList.get(i).baseURL); - sb1.append(serviceList.get(i).controlURL); - x++; - if (x==2) break; + renderingControlURL = String.format("%s%s", serviceList.get(i).baseURL, serviceList.get(i).controlURL); } } - controlURL = sb.toString(); - renderURL = sb1.toString(); } } @@ -203,8 +188,8 @@ public void onSuccess(Object response) { Map parameters = new HashMap(); parameters.put("Speed", "1"); - JSONObject payload = getMethodBody(instanceId, method, parameters); - + String payload = getMessageXml(AV_TRANSPORT_URN, method, "0", parameters); + ResponseListener playResponseListener = new ResponseListener () { @Override public void onSuccess(Object response) { @@ -232,9 +217,15 @@ public void onError(ServiceCommandError error) { }; String method = "SetAVTransportURI"; - JSONObject httpMessage = getSetAVTransportURIBody(method, instanceId, url, mMimeType, title); - - ServiceCommand> request = new ServiceCommand>(DLNAService.this, method, httpMessage, responseListener); + String metadata = getMetadata(url, mMimeType, title); + + Map params = new HashMap(); + params.put("CurrentURI", url); + params.put("CurrentURIMetaData", metadata); + + String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, params); + + ServiceCommand> request = new ServiceCommand>(DLNAService.this, method, payload, responseListener); request.send(); } @@ -292,7 +283,7 @@ public void play(ResponseListener listener) { Map parameters = new HashMap(); parameters.put("Speed", "1"); - JSONObject payload = getMethodBody(instanceId, method, parameters); + String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, parameters); ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); request.send(); @@ -303,7 +294,7 @@ public void pause(ResponseListener listener) { String method = "Pause"; String instanceId = "0"; - JSONObject payload = getMethodBody(instanceId, method); + String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, null); ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); request.send(); @@ -314,7 +305,7 @@ public void stop(ResponseListener listener) { String method = "Stop"; String instanceId = "0"; - JSONObject payload = getMethodBody(instanceId, method); + String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, null); ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); request.send(); @@ -335,7 +326,7 @@ public void previous(ResponseListener listener) { String method = "Previous"; String instanceId = "0"; - JSONObject payload = getMethodBody(instanceId, method); + String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, null); ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); request.send(); @@ -346,7 +337,7 @@ public void next(ResponseListener listener) { String method = "Next"; String instanceId = "0"; - JSONObject payload = getMethodBody(instanceId, method); + String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, null); ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); request.send(); @@ -367,7 +358,7 @@ public void seek(long position, ResponseListener listener) { parameters.put("Unit", "REL_TIME"); parameters.put("Target", time); - JSONObject payload = getMethodBody(instanceId, method, parameters); + String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, parameters); ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); request.send(); @@ -377,7 +368,7 @@ private void getPositionInfo(final PositionInfoListener listener) { String method = "GetPositionInfo"; String instanceId = "0"; - JSONObject payload = getMethodBody(instanceId, method); + String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, null); ResponseListener responseListener = new ResponseListener() { @@ -439,108 +430,31 @@ public void onGetPositionInfoFailed(ServiceCommandError error) { } }); } - - protected JSONObject getSetAVTransportURIBody(String method, String instanceId, String mediaURL, String mime, String title) { - String action = "SetAVTransportURI"; - String metadata = getMetadata(mediaURL, mime, title); - - StringBuilder sb = new StringBuilder(); + + protected String getMessageXml(String serviceURN, String method, String instanceId, Map params) { + StringBuilder sb = new StringBuilder(); sb.append(""); sb.append(""); sb.append(""); - sb.append(""); + sb.append(""); sb.append("" + instanceId + ""); - sb.append("" + mediaURL + ""); - sb.append(""+ metadata + ""); - - sb.append(""); - sb.append(""); - sb.append(""); - JSONObject obj = new JSONObject(); - try { - obj.put(DATA, sb.toString()); - obj.put(ACTION, String.format(AV_TRANSPORT_URN, method)); - } catch (JSONException e) { - e.printStackTrace(); - } + if (params != null) { + for (Map.Entry entry : params.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); - return obj; - } - - protected JSONObject getMethodBody(String instanceId, String method) { - return getMethodBody(instanceId, method, null); - } - - protected JSONObject getMethodBody(String instanceId, String method, Map parameters) { - StringBuilder sb = new StringBuilder(); - - sb.append(""); - sb.append(""); - - sb.append(""); - sb.append(""); - sb.append("" + instanceId + ""); - - if (parameters != null) { - for (Map.Entry entry : parameters.entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - - sb.append("<" + key + ">"); - sb.append(value); - sb.append(""); - } + String str = String.format("<%s>%s", key, value, key); + sb.append(str); + } } sb.append(""); sb.append(""); sb.append(""); - JSONObject obj = new JSONObject(); - try { - obj.put(DATA, sb.toString()); - obj.put(ACTION, String.format(AV_TRANSPORT_URN, method)); - } catch (JSONException e) { - e.printStackTrace(); - } - - return obj; - } - - protected JSONObject getRenderingControlMethodBody(String instanceId, String method, String channel, String value) { - StringBuilder sb = new StringBuilder(); - - sb.append(""); - sb.append(""); - - sb.append(""); - sb.append(""); - sb.append("" + instanceId + ""); - sb.append("" + channel + ""); - if (method.equals("SetVolume")) - sb.append("" + value + ""); - else if (method.equals("SetMute")) - sb.append("" + value + ""); - - sb.append(""); - sb.append(""); - sb.append(""); - - JSONObject obj = new JSONObject(); - try { - obj.put(DATA, sb.toString()); - obj.put(ACTION, String.format(ACTION_CONTENT_RENDER, method)); - } catch (JSONException e) { - e.printStackTrace(); - } - - return obj; - } - - protected JSONObject getRenderingControlMethodBody(String instanceId, String method, String channel) { - return getRenderingControlMethodBody(instanceId, method, channel, null); + return sb.toString(); } protected String getMetadata(String mediaURL, String mime, String title) { @@ -584,20 +498,27 @@ public void sendCommand(final ServiceCommand mCommand) { public void run() { ServiceCommand> command = (ServiceCommand>) mCommand; - JSONObject payload = (JSONObject) command.getPayload(); + String method = command.getTarget(); + String payload = (String)command.getPayload(); - HttpPost request; + String targetURL = null; + String serviceURN = null; - if ((command.getTarget().contains("Volume")) || (command.getTarget().contains("Mute"))) { - request = HttpMessage.getDLNAHttpPostRenderControl(renderURL, command.getTarget()); + if (payload.contains(AV_TRANSPORT_URN)) { + targetURL = avTransportURL; + serviceURN = AV_TRANSPORT_URN; } - else { - request = HttpMessage.getDLNAHttpPost(controlURL, command.getTarget()); + else if (payload.contains(RENDERING_CONTROL_URN)) { + targetURL = renderingControlURL; + serviceURN = RENDERING_CONTROL_URN; } - request.setHeader(ACTION, payload.optString(ACTION)); + + HttpPost post = new HttpPost(targetURL); + post.setHeader("Content-Type", "text/xml; charset=utf-8"); + post.setHeader("SOAPAction", String.format("\"%s#%s\"", serviceURN, method)); + try { - request.setEntity(new StringEntity(payload.optString(DATA) - .toString())); + post.setEntity(new StringEntity(payload)); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } @@ -605,7 +526,7 @@ public void run() { HttpResponse response = null; try { - response = httpClient.execute(request); + response = httpClient.execute(post); int code = response.getStatusLine().getStatusCode(); @@ -701,7 +622,7 @@ public void getPlayState(final PlayStateListener listener) { String method = "GetTransportInfo"; String instanceId = "0"; - JSONObject payload = getMethodBody(instanceId, method); + String payload = getMessageXml(AV_TRANSPORT_URN, method, instanceId, null); ResponseListener responseListener = new ResponseListener() { @@ -732,12 +653,14 @@ public ServiceSubscription subscribePlayState(PlayStateListen } private void addSubscription(URLServiceSubscription subscription) { - httpServer.getSubscriptions().add(subscription); + if (httpServer != null) + httpServer.getSubscriptions().add(subscription); } @Override public void unsubscribe(URLServiceSubscription subscription) { - httpServer.getSubscriptions().remove(subscription); + if (httpServer != null) + httpServer.getSubscriptions().remove(subscription); } @Override @@ -767,7 +690,7 @@ public void run() { } }); - subscribeEvents(); + subscribeServices(); reportConnected(true); } @@ -788,7 +711,7 @@ public void run() { } }); - unsubscribeEvents(); + unsubscribeServices(); if (httpServer != null) { httpServer.stop(); @@ -805,7 +728,7 @@ public void onLoseReachability(DeviceServiceReachability reachability) { } } - public void subscribeEvents() { + public void subscribeServices() { Util.runInBackground(new Runnable() { @Override @@ -849,10 +772,10 @@ public void run() { } }); - resubscribeEvent(); + resubscribeServices(); } - public void resubscribeEvent() { + public void resubscribeServices() { resubscriptionTimer = new Timer(); resubscriptionTimer.scheduleAtFixedRate(new TimerTask() { @@ -898,7 +821,7 @@ public void run() { }, TIMEOUT/2*1000, TIMEOUT/2*1000); } - public void unsubscribeEvents() { + public void unsubscribeServices() { if (resubscriptionTimer != null) resubscriptionTimer.cancel(); @@ -1012,20 +935,26 @@ public void setVolume(float volume, ResponseListener listener) { String channel = "Master"; String value = String.valueOf((int)(volume*100)); - JSONObject payload = getRenderingControlMethodBody(instanceId, method, channel, value); + Map params = new HashMap(); + params.put("Channel", channel); + params.put("DesiredVolume", value); + + String payload = getMessageXml(RENDERING_CONTROL_URN, method, instanceId, params); ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); request.send(); } - private ServiceCommand getVolumeStatus(Boolean isSubscription, final VolumeListener listener) { + @Override + public void getVolume(final VolumeListener listener) { String method = "GetVolume"; String instanceId = "0"; String channel = "Master"; - ServiceCommand request; - - JSONObject payload = getRenderingControlMethodBody(instanceId, method, channel); + Map params = new HashMap(); + params.put("Channel", channel); + + String payload = getMessageXml(RENDERING_CONTROL_URN, method, instanceId, params); ResponseListener responseListener = new ResponseListener() { @@ -1044,19 +973,8 @@ public void onError(ServiceCommandError error) { } }; - if (isSubscription) - request = new URLServiceSubscription(this, method, payload, responseListener); - else - request = new ServiceCommand(this, method, payload, responseListener); - + ServiceCommand request = new ServiceCommand(this, method, payload, responseListener); request.send(); - - return request; - } - - @Override - public void getVolume(VolumeListener listener) { - getVolumeStatus(false, listener); } @Override @@ -1064,22 +982,28 @@ public void setMute(boolean isMute, ResponseListener listener) { String method = "SetMute"; String instanceId = "0"; String channel = "Master"; - String value = String.valueOf(isMute); + int muteStatus = (isMute) ? 1 : 0; - JSONObject payload = getRenderingControlMethodBody(instanceId, method, channel, value); + Map params = new HashMap(); + params.put("Channel", channel); + params.put("DesiredMute", String.valueOf(muteStatus)); + + String payload = getMessageXml(RENDERING_CONTROL_URN, method, instanceId, params); ServiceCommand> request = new ServiceCommand>(this, method, payload, listener); request.send(); } - private ServiceCommand> getMuteStatus(boolean isSubscription, final MuteListener listener) { + @Override + public void getMute(final MuteListener listener) { String method = "GetMute"; String instanceId = "0"; String channel = "Master"; - ServiceCommand> request; - - JSONObject payload = getRenderingControlMethodBody(instanceId, method, channel); + Map params = new HashMap(); + params.put("Channel", channel); + + String payload = getMessageXml(RENDERING_CONTROL_URN, method, instanceId, params); ResponseListener responseListener = new ResponseListener() { @@ -1097,19 +1021,8 @@ public void onError(ServiceCommandError error) { } }; - if (isSubscription) - request = new URLServiceSubscription>(this, method , payload, responseListener); - else - request = new ServiceCommand>(this, method, payload, responseListener); - + ServiceCommand> request = new ServiceCommand>(this, method, payload, responseListener); request.send(); - - return request; - } - - @Override - public void getMute(MuteListener listener) { - getMuteStatus(false, listener); } @Override diff --git a/src/com/connectsdk/service/upnp/DLNAHttpServer.java b/src/com/connectsdk/service/upnp/DLNAHttpServer.java index 5db78745..656353a7 100644 --- a/src/com/connectsdk/service/upnp/DLNAHttpServer.java +++ b/src/com/connectsdk/service/upnp/DLNAHttpServer.java @@ -12,6 +12,7 @@ import java.net.Socket; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import org.json.JSONException; import org.json.JSONObject; @@ -31,10 +32,10 @@ public class DLNAHttpServer { boolean running = false; - List> subscriptions; + CopyOnWriteArrayList> subscriptions; public DLNAHttpServer() { - subscriptions = new ArrayList>(); + subscriptions = new CopyOnWriteArrayList>(); } public void start() { @@ -136,7 +137,7 @@ public void start() { try { event = parser.parse(stream); - if (!event.isNull("TransportState")) { + if (event.has("TransportState")) { String transportState = event.getString("TransportState"); PlayStateStatus status = PlayStateStatus.convertTransportStateToPlayStateStatus(transportState); @@ -150,32 +151,30 @@ public void start() { } } } - - if (event.has("Volume")) { - if (event.getString("Volume") != null) { - System.out.println("[DEBUG] Volume " + event.getString("Volume")); - - int intVolume = event.getInt("Volume"); - float volume = (float) intVolume / 100; + else if (event.has("Volume")) { + int intVolume = event.getInt("Volume"); + float volume = (float) intVolume / 100; - for (URLServiceSubscription sub : subscriptions) { - if (sub.getTarget().equalsIgnoreCase("volume")) { - for (int i = 0; i < sub.getListeners().size(); i++) { - @SuppressWarnings("unchecked") - ResponseListener listener = (ResponseListener) sub.getListeners().get(i); - Util.postSuccess(listener, volume); - } + for (URLServiceSubscription sub : subscriptions) { + if (sub.getTarget().equalsIgnoreCase("volume")) { + for (int i = 0; i < sub.getListeners().size(); i++) { + @SuppressWarnings("unchecked") + ResponseListener listener = (ResponseListener) sub.getListeners().get(i); + Util.postSuccess(listener, volume); } } } } - - if (event.has("Mute")) { - System.out.println("[DEBUG] Mute " + event.getString("Mute")); - - int intMute = event.getInt("Mute"); - boolean mute = (intMute==1) ? true : false; + else if (event.has("Mute")) { + String muteStatus = event.getString("Mute"); + boolean mute; + try { + mute = (Integer.parseInt(muteStatus) == 1) ? true : false; + } catch(NumberFormatException e) { + mute = Boolean.parseBoolean(muteStatus); + } + for (URLServiceSubscription sub : subscriptions) { if (sub.getTarget().equalsIgnoreCase("mute")) { for (int i = 0; i < sub.getListeners().size(); i++) { @@ -196,11 +195,15 @@ public void start() { } } - public void stop() { if (!running) return; + for (URLServiceSubscription sub: subscriptions) { + sub.unsubscribe(); + } + subscriptions.clear(); + if (welcomeSocket != null && !welcomeSocket.isClosed()) { try { welcomeSocket.close(); @@ -222,6 +225,6 @@ public List> getSubscriptions() { } public void setSubscriptions(List> subscriptions) { - this.subscriptions = subscriptions; + this.subscriptions = new CopyOnWriteArrayList>(subscriptions); } } From d655c0d8ba26699eca44390c174dec944e58c9b6 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Wed, 17 Sep 2014 21:30:07 -0700 Subject: [PATCH 46/76] Reset package structure in submodules to match Connect SDK Core --- modules/google_cast | 2 +- modules/samsung_multiscreen | 2 +- src/com/connectsdk/DefaultPlatform.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/google_cast b/modules/google_cast index 0527302d..76a22e3c 160000 --- a/modules/google_cast +++ b/modules/google_cast @@ -1 +1 @@ -Subproject commit 0527302d094bfa61c75282df51b531b496658442 +Subproject commit 76a22e3c70e66d841c7d2a76474899a98c2acb02 diff --git a/modules/samsung_multiscreen b/modules/samsung_multiscreen index 72bc0b9b..24f862cc 160000 --- a/modules/samsung_multiscreen +++ b/modules/samsung_multiscreen @@ -1 +1 @@ -Subproject commit 72bc0b9b3dfeab1fa4cb9ec8ff5de8cb5d98fbd6 +Subproject commit 24f862ccf61144c9e772dd9f1d78d2d996d0ed96 diff --git a/src/com/connectsdk/DefaultPlatform.java b/src/com/connectsdk/DefaultPlatform.java index 433c1078..7234e313 100644 --- a/src/com/connectsdk/DefaultPlatform.java +++ b/src/com/connectsdk/DefaultPlatform.java @@ -17,9 +17,9 @@ public static HashMap getDeviceServiceMap() { devicesList.put("com.connectsdk.service.DLNAService", "com.connectsdk.discovery.provider.SSDPDiscoveryProvider"); devicesList.put("com.connectsdk.service.DIALService", "com.connectsdk.discovery.provider.SSDPDiscoveryProvider"); devicesList.put("com.connectsdk.service.RokuService", "com.connectsdk.discovery.provider.SSDPDiscoveryProvider"); - devicesList.put("com.connectsdk.androidgooglecast.CastService", "com.connectsdk.androidgooglecast.CastDiscoveryProvider"); + devicesList.put("com.connectsdk.service.CastService", "com.connectsdk.discovery.provider.CastDiscoveryProvider"); devicesList.put("com.connectsdk.service.AirPlayService", "com.connectsdk.discovery.provider.ZeroconfDiscoveryProvider"); - devicesList.put("com.connectsdk.samsungmultiscreen.MultiScreenService", "com.connectsdk.discovery.provider.SSDPDiscoveryProvider"); + devicesList.put("com.connectsdk.service.MultiScreenService", "com.connectsdk.discovery.provider.SSDPDiscoveryProvider"); return devicesList; } From 3937fcf7e3687adaf61ed97fcb546f4014559190 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Wed, 17 Sep 2014 21:31:40 -0700 Subject: [PATCH 47/76] Updated core submodule --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index b25c7f49..62326e2e 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit b25c7f49cf6baf97ff1c69e8ddd5ec529b97fef8 +Subproject commit 62326e2eceed17496a67d99c53291e3cfe0e6f6d From 4c0928131f2e4d71a8792d192ae26cc4a6f24e65 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Wed, 17 Sep 2014 21:36:41 -0700 Subject: [PATCH 48/76] Updated core submodule --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index 62326e2e..19c12627 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 62326e2eceed17496a67d99c53291e3cfe0e6f6d +Subproject commit 19c12627a6859c4b0998ffd73eaa7e143cab2212 From 0638b153c61edcd319c61409f287ba047d6bed4b Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Wed, 17 Sep 2014 21:45:13 -0700 Subject: [PATCH 49/76] Added submodule support to Android Studio, fixes #153 --- Connect-SDK.iml | 35 ++++++++++++++++++++++++++++++----- build.gradle | 11 ++++++++++- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/Connect-SDK.iml b/Connect-SDK.iml index f6c86334..cf961c85 100644 --- a/Connect-SDK.iml +++ b/Connect-SDK.iml @@ -1,5 +1,5 @@ - + @@ -13,6 +13,7 @@ diff --git a/build.gradle b/build.gradle index d8986c9b..760f2b66 100644 --- a/build.gradle +++ b/build.gradle @@ -22,19 +22,18 @@ android { } buildTypes { release { - runProguard false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + minifyEnabled false } } } dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - compile fileTree(dir: 'core/libs', include: ['*.jar']) - compile fileTree(dir: 'modules/google_cast/libs', include: ['*.jar']) - compile fileTree(dir: 'modules/samsung_multiscreen/libs', include: ['*.jar']) + compile files('libs/java_websocket.jar') + compile files('libs/javax.jmdns_3.4.1-patch2.jar') + compile files('modules/samsung_multiscreen/libs/multiscreen-android-1.1.11.jar') - compile 'com.android.support:mediarouter-v7:19.1.0' - compile 'com.android.support:appcompat-v7:19.1.0' + compile 'com.android.support:support-v4:20.0.0' + compile 'com.android.support:mediarouter-v7:20.0.0' + compile 'com.android.support:appcompat-v7:20.0.0' compile 'com.google.android.gms:play-services:5.0.+' } From 0e85bdd655d989979e845c0cef569c2b575b5f3e Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Wed, 5 Nov 2014 22:24:13 -0800 Subject: [PATCH 61/76] Updated to latest Android SDK version --- Connect-SDK.iml | 10 +++++----- build.gradle | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Connect-SDK.iml b/Connect-SDK.iml index 4bf0ac23..52e06d49 100644 --- a/Connect-SDK.iml +++ b/Connect-SDK.iml @@ -80,16 +80,16 @@ - + + + - - - - + + diff --git a/build.gradle b/build.gradle index 760f2b66..dde2c785 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 19 - buildToolsVersion '20.0.0' + compileSdkVersion 21 + buildToolsVersion '21.1.0' sourceSets { main { @@ -32,8 +32,8 @@ dependencies { compile files('libs/javax.jmdns_3.4.1-patch2.jar') compile files('modules/samsung_multiscreen/libs/multiscreen-android-1.1.11.jar') - compile 'com.android.support:support-v4:20.0.0' + compile 'com.android.support:support-v4:21.0.0' compile 'com.android.support:mediarouter-v7:20.0.0' - compile 'com.android.support:appcompat-v7:20.0.0' - compile 'com.google.android.gms:play-services:5.0.+' + compile 'com.android.support:appcompat-v7:21.0.0' + compile 'com.google.android.gms:play-services:6.1.71' } From 072734cec1d69bdda13c1b876d710f362f310590 Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Thu, 6 Nov 2014 16:56:46 +0900 Subject: [PATCH 62/76] Update submodules --- core | 2 +- modules/google_cast | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core b/core index 52b5bbba..c0fb0399 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 52b5bbbadd14afb4f37fc1d56cd594c6ab6eb88e +Subproject commit c0fb039904bbe91b115a274ecf87d2428a2bfe91 diff --git a/modules/google_cast b/modules/google_cast index e4182740..3b16f06c 160000 --- a/modules/google_cast +++ b/modules/google_cast @@ -1 +1 @@ -Subproject commit e41827406e58adcc7a5f868ceee437be97deba83 +Subproject commit 3b16f06c5d1bd3fd48aa4c0372ecdcad081293bb From 621efd854fb5b381ed2ceb42a80662835eb0848a Mon Sep 17 00:00:00 2001 From: Simon Gladkoskok Date: Mon, 10 Nov 2014 10:21:32 -0800 Subject: [PATCH 63/76] update core submodule --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index c0fb0399..f80e2f17 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit c0fb039904bbe91b115a274ecf87d2428a2bfe91 +Subproject commit f80e2f17d6cdc37dcc93c66789c2e3f5df9639aa From c1dd8a0ebdd977e7a7984c9d011092d2d8e145d0 Mon Sep 17 00:00:00 2001 From: Simon Gladkoskok Date: Thu, 13 Nov 2014 16:59:32 -0800 Subject: [PATCH 64/76] Update core submodule Netcast: implement setVolume, volume and mute subscriptions. --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index f80e2f17..87661736 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit f80e2f17d6cdc37dcc93c66789c2e3f5df9639aa +Subproject commit 8766173685f2fe7b34551bd4a0153065595f365f From 2aade92a8c4a39f9ebb28e6e3dc828ee8add9c84 Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Mon, 17 Nov 2014 14:40:32 +0900 Subject: [PATCH 65/76] Update core submodule Fixed crash when decoding web page returns null (https://github.com/ConnectSDK/Connect-SDK-Android-Core/commit/1018932da475a506d07d5c6cb7f0b5c39af035d6) --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index 87661736..1018932d 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 8766173685f2fe7b34551bd4a0153065595f365f +Subproject commit 1018932da475a506d07d5c6cb7f0b5c39af035d6 From d9f809c66f2e6b16a44af171982478c5513cfae2 Mon Sep 17 00:00:00 2001 From: Hyun Kook Khang Date: Wed, 19 Nov 2014 09:58:08 +0900 Subject: [PATCH 66/76] Update core submodule Follow HTTP Redirects. Fixes issue #173 for Dropbox images --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index 1018932d..0e308353 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 1018932da475a506d07d5c6cb7f0b5c39af035d6 +Subproject commit 0e30835320b3464c056daf0366ce605758bad079 From bdf599d564f830d20ac3950214934fe459c71768 Mon Sep 17 00:00:00 2001 From: Oleksii Frolov Date: Mon, 24 Nov 2014 18:46:41 -0800 Subject: [PATCH 67/76] remove multiscreen module from 1.4 --- .gitmodules | 3 --- modules/samsung_multiscreen | 1 - src/com/connectsdk/DefaultPlatform.java | 1 - 3 files changed, 5 deletions(-) delete mode 160000 modules/samsung_multiscreen diff --git a/.gitmodules b/.gitmodules index b3abf065..bcd63ee8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "modules/google_cast"] path = modules/google_cast url = https://github.com/ConnectSDK/Connect-SDK-Android-Google-Cast.git -[submodule "modules/samsung_multiscreen"] - path = modules/samsung_multiscreen - url = https://github.com/ConnectSDK/Connect-SDK-Android-Samsung-MultiScreen.git [submodule "core"] path = core url = https://github.com/ConnectSDK/Connect-SDK-Android-Core.git diff --git a/modules/samsung_multiscreen b/modules/samsung_multiscreen deleted file mode 160000 index b7d8094f..00000000 --- a/modules/samsung_multiscreen +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b7d8094fea7229e5e311e7c4ef99680239a4e4ef diff --git a/src/com/connectsdk/DefaultPlatform.java b/src/com/connectsdk/DefaultPlatform.java index 7234e313..7ce9ecec 100644 --- a/src/com/connectsdk/DefaultPlatform.java +++ b/src/com/connectsdk/DefaultPlatform.java @@ -19,7 +19,6 @@ public static HashMap getDeviceServiceMap() { devicesList.put("com.connectsdk.service.RokuService", "com.connectsdk.discovery.provider.SSDPDiscoveryProvider"); devicesList.put("com.connectsdk.service.CastService", "com.connectsdk.discovery.provider.CastDiscoveryProvider"); devicesList.put("com.connectsdk.service.AirPlayService", "com.connectsdk.discovery.provider.ZeroconfDiscoveryProvider"); - devicesList.put("com.connectsdk.service.MultiScreenService", "com.connectsdk.discovery.provider.SSDPDiscoveryProvider"); return devicesList; } From c6902a4490789394fe1ee6d7cd64783f9c62b113 Mon Sep 17 00:00:00 2001 From: Oleksii Frolov Date: Mon, 24 Nov 2014 19:29:44 -0800 Subject: [PATCH 68/76] Update core submodule --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index 0e308353..a451cd81 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 0e30835320b3464c056daf0366ce605758bad079 +Subproject commit a451cd815ad07d1feec289466f50fdbe0b07d454 From eba8cbed91c1a0cf15f1e7a91ad21f14f08015ae Mon Sep 17 00:00:00 2001 From: Simon Gladkoskok Date: Tue, 25 Nov 2014 11:55:08 -0800 Subject: [PATCH 69/76] Update README.md for 1.4 release Add steps for migrating from 1.3 --- README.md | 59 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 827cee5b..2a76cb2d 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,6 @@ This project has the following dependencies. * [Connect-SDK-Android-Core](https://github.com/ConnectSDK/Connect-SDK-Android-Core) submodule * [Connect-SDK-Android-Google-Cast](https://github.com/ConnectSDK/Connect-SDK-Android-Google-Cast) submodule - Requires [GoogleCast.framework](https://developers.google.com/cast/docs/downloads) -* [Connect-SDK-Android-Samsung-MultiScreen](https://github.com/ConnectSDK/Connect-SDK-Android-Samsung-MultiScreen) submodule - - Requires [SamsungMultiScreen.framework](http://multiscreen.samsung.com/downloads.html) * [Java-WebSocket library](https://github.com/TooTallNate/Java-WebSocket) ##Including Connect SDK in your app @@ -26,20 +24,22 @@ This project has the following dependencies. - `git submodule update` 3. Open Eclipse 4. Click File > Import -5. Select `Existing Android Code Into Workspace` and click Next -6. Browse to the Connect-SDK-Android project folder and click Open -7. Click Finish -8. Do the steps 4-7 for Connect-SDK-Android-Core which is located in `core` folder of the Connect-SDK-Android project -9. Do the steps 4-7 for Connect-SDK-Android-Google-Cast which is located in `modules/google_cast` folder of the Connect-SDK-Android project -10. Do the steps 4-7 for Connect-SDK-Android-Samsung-MultiScreen which is located in `modules/samsung_multiscreen` folder of the Connect-SDK-Android project -11. Follow the setup instructions for each of the service submodules - - [Connect-SDK-Android-Google-Cast](https://github.com/ConnectSDK/Connect-SDK-Android-Google-Cast) - - [Connect-SDK-Android-Samsung-MultiScreen](https://github.com/ConnectSDK/Connect-SDK-Android-Samsung-MultiScreen) -12. Right-click the Connect-SDK-Android-Core project and select Properties, in the Library pane of the Android tab, add Connect-SDK-Android -13. Right-click the Connect-SDK-Android-Google-Cast project and select Properties, in the Library pane of the Android tab, add Connect-SDK-Android-Core -14. Right-click the Connect-SDK-Android-Samsung-MultiScreen project and select Properties, in the Library pane of the Android tab, add Connect-SDK-Android-Core -15. In your project select Properties, in the Library pane of the Android tab, add Connect-SDK-Android-Core, Connect-SDK-Android-Google-Cast, and Connect-SDK-Android-Samsung-MultiScreen -15. Set up your manifest file as per the instructions below +5. Select `Existing Android Code Into Workspace` and click `Next` +6. Browse to the `Connect-SDK-Android` project folder and click `Open` +7. Check all projects and click `Finish` +8. Follow the setup instructions for each of the service submodules + - [Connect-SDK-Android-Google-Cast](https://github.com/ConnectSDK/Connect-SDK-Android-Google-Cast) +9. Right-click the `Connect-SDK-Android-Core` project and select `Properties`, in the `Library` pane of the `Android` tab add + - Connect-SDK-Android +10. Right-click the `Connect-SDK-Android-Google-Cast` project and select `Properties`, in the `Library` pane of the `Android` tab add following libraries + - Connect-SDK-Android-Core + - android-support-v7-appcompat + - android-support-v7-mediarouter + - google-play-services_lib +11. **IN YOUR PROJECT** select `Properties`, in the `Library` pane of the `Android` tab add following libraries + - Connect-SDK-Android-Core + - Connect-SDK-Android-Google-Cast +12. Set up your manifest file as per the instructions below ###Permissions to include in manifest * Required for SSDP & Chromecast/Zeroconf discovery @@ -80,6 +80,33 @@ Add the following line to your proguard configuration file (otherwise `Discovery -keep class com.connectsdk.** { * ; } ``` +##Migrating from 1.3 to 1.4 release + +1. Open terminal and go to your local Connect-SDK-Android repo +2. Pull the latest updates by running command `git pull` in Terminal +3. Set up the submodules by running the following commands in Terminal + - `git submodule init` + - `git submodule update` +4. Open Eclipse +5. Click `File > Import` +6. Select `Existing Android Code Into Workspace` and click `Next` +7. Browse to the `Connect-SDK-Android/core` folder and click `Open` to import core submodule +8. Click `Finish` +9. Do the steps 5-8 for Connect-SDK-Android-Google-Cast which is located in `Connect-SDK-Android/modules/google_cast` folder +10. Right click on `Connect-SDK-Android` project and select `Properties`, in the `Library` pane of the `Android` tab + - remove all libraries references +11. Right-click the `Connect-SDK-Android-Core` project and select `Properties`, in the `Library` pane of the `Android` tab add + - Connect-SDK-Android +12. Right-click the `Connect-SDK-Android-Google-Cast` project and select `Properties`, in the `Library` pane of the `Android` tab add following libraries + - Connect-SDK-Android-Core + - android-support-v7-appcompat + - android-support-v7-mediarouter + - google-play-services_lib +13. **IN YOUR PROJECT** select `Properties`, in the Library pane of the Android tab + - remove Connect-SDK-Android + - add Connect-SDK-Android-Core + - add Connect-SDK-Android-Google-Cast. + ##Contact * Twitter [@ConnectSDK](https://www.twitter.com/ConnectSDK) * Ask a question with the "tv" tag on [Stack Overflow](http://stackoverflow.com/tags/tv) From 1973366a521a892fcc622d4bb67d9fc7dc7794d9 Mon Sep 17 00:00:00 2001 From: Simon Gladkoskok Date: Tue, 25 Nov 2014 11:59:29 -0800 Subject: [PATCH 70/76] Update dependencies --- project.properties | 3 --- 1 file changed, 3 deletions(-) diff --git a/project.properties b/project.properties index 8941fb95..484dab07 100644 --- a/project.properties +++ b/project.properties @@ -13,6 +13,3 @@ # Project target. target=android-17 android.library=true -android.library.reference.1=../../../../Applications/Android Developer Tools/sdk/extras/android/support/v7/appcompat -android.library.reference.2=../../../../Applications/Android Developer Tools/sdk/extras/android/support/v7/mediarouter -android.library.reference.3=../../../../Applications/Android Developer Tools/sdk/extras/google/google_play_services/libproject/google-play-services_lib From 4379225da6750b683bacb1aa7afe0787e41495ae Mon Sep 17 00:00:00 2001 From: Simon Gladkoskok Date: Tue, 25 Nov 2014 15:57:12 -0800 Subject: [PATCH 71/76] Update submodules delete support-v4.jar --- core | 2 +- modules/google_cast | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core b/core index a451cd81..14a50467 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit a451cd815ad07d1feec289466f50fdbe0b07d454 +Subproject commit 14a50467ff24f8d27da625a6bff1f5cf39ddc996 diff --git a/modules/google_cast b/modules/google_cast index 3b16f06c..9bf3ef40 160000 --- a/modules/google_cast +++ b/modules/google_cast @@ -1 +1 @@ -Subproject commit 3b16f06c5d1bd3fd48aa4c0372ecdcad081293bb +Subproject commit 9bf3ef402e3a1acdc78ae8c6bed094555f0a0f9c From b1acb5ae693050a5651220e17789d01a8b387a34 Mon Sep 17 00:00:00 2001 From: Oleksii Frolov Date: Wed, 26 Nov 2014 13:34:45 -0800 Subject: [PATCH 72/76] add build.xml --- build.xml | 92 +++++++++++++++++++++++++++++++++++++++++++++ core | 2 +- modules/google_cast | 2 +- 3 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 build.xml diff --git a/build.xml b/build.xml new file mode 100644 index 00000000..0d0580dd --- /dev/null +++ b/build.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core b/core index 14a50467..d7d1fa51 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 14a50467ff24f8d27da625a6bff1f5cf39ddc996 +Subproject commit d7d1fa518a2cd9df93ddffd0eb3a51199559751e diff --git a/modules/google_cast b/modules/google_cast index 9bf3ef40..da42cde8 160000 --- a/modules/google_cast +++ b/modules/google_cast @@ -1 +1 @@ -Subproject commit 9bf3ef402e3a1acdc78ae8c6bed094555f0a0f9c +Subproject commit da42cde89026c5a3b6da705de6f9629da0dc23e2 From 43da944585660430ae36f68da9f5a630e5aa7dd6 Mon Sep 17 00:00:00 2001 From: Oleksii Frolov Date: Wed, 26 Nov 2014 18:14:03 -0800 Subject: [PATCH 73/76] Update submodules --- core | 2 +- modules/google_cast | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core b/core index d7d1fa51..1102a20e 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit d7d1fa518a2cd9df93ddffd0eb3a51199559751e +Subproject commit 1102a20e7c5210b52f1c668e9b607c4085436084 diff --git a/modules/google_cast b/modules/google_cast index da42cde8..ff7a105f 160000 --- a/modules/google_cast +++ b/modules/google_cast @@ -1 +1 @@ -Subproject commit da42cde89026c5a3b6da705de6f9629da0dc23e2 +Subproject commit ff7a105f72c5932572aca6b218653b34da0097bb From f1c4e02af8cb45f75a75a8a96fc761109780ab7f Mon Sep 17 00:00:00 2001 From: Oleksii Frolov Date: Wed, 26 Nov 2014 19:01:44 -0800 Subject: [PATCH 74/76] Update submodules --- core | 2 +- modules/google_cast | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core b/core index 1102a20e..8fcaa9f4 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 1102a20e7c5210b52f1c668e9b607c4085436084 +Subproject commit 8fcaa9f4af04a3bcc6bb8190bac520057b6a0bc6 diff --git a/modules/google_cast b/modules/google_cast index ff7a105f..5a956a47 160000 --- a/modules/google_cast +++ b/modules/google_cast @@ -1 +1 @@ -Subproject commit ff7a105f72c5932572aca6b218653b34da0097bb +Subproject commit 5a956a47229324cedd90e13e22f06a5f62aa059d From ca94103ce292abbbc7d15dfa251f4d5d271050e4 Mon Sep 17 00:00:00 2001 From: Simon Gladkoskok Date: Tue, 2 Dec 2014 15:41:40 -0800 Subject: [PATCH 75/76] Moved 3rd party libs to core submodule from main project --- core | 2 +- libs/java_websocket.jar | Bin 97655 -> 0 bytes libs/javax.jmdns_3.4.1-patch2.jar | Bin 220532 -> 0 bytes 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 libs/java_websocket.jar delete mode 100644 libs/javax.jmdns_3.4.1-patch2.jar diff --git a/core b/core index 8fcaa9f4..02ebb352 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 8fcaa9f4af04a3bcc6bb8190bac520057b6a0bc6 +Subproject commit 02ebb3520785d348782c16ce2e7681253210306a diff --git a/libs/java_websocket.jar b/libs/java_websocket.jar deleted file mode 100644 index 60d6d84cd5c12694f2946b060420500877f0af5c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 97655 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%0Dm|K!S@qb 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 z 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 diff --git a/libs/javax.jmdns_3.4.1-patch2.jar b/libs/javax.jmdns_3.4.1-patch2.jar deleted file mode 100644 index 977faf3f6e77ec3bc3e625fd3440eb938ce72847..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 220532 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

Z|BYK!b zq^kdX@w(SWRm3=U@BSew#4mwG6lHd>RgSzTu1_4R9yfBqV?U}tMjA)0&vYt>rJ=~7x6h$@ zjLa500m8}*f?@Ayvy#)_LMfD?o6GW`)*_$q)U6D>#vVF$DN|U+F-&$^m3NgwgxW)xo!)e`h7S@ zMOMi~XBZXg1NBr_34}gZYP5qQu_3}BkcVI$!{c(vP3wTd0YDVMF`wCGBG1Ie3@SHv z^G>NcU74-n4fAR~xyyfY$)4>AFl(~gZ+HUAUD6$NT5r7#e8TnC@`B`_?TN#;0Txba6Jb-~|=g~QlF;qPi% z*0Ip@#$)@P`6M2G;oSLBe`31wbD~P#FijIFAEELsL?OAkbktW%N6OzMijvrggZ+DjU zKmKt86t7^Ja#cIbo8bupVB&0yh9?~ce;-haLH80yx15VkT4a(O&;yxZlym9O$;rHDeswIm7M|cC+njQ2_LAup%D+T5h&lX^rM5* z@s=>+tN>yxK~*CI+_kJ?C|>#M9zwHGcb3t6Beilm6D%S1D`knPi%Lt-i&-RszUiPF zgaz+e_J}A-s(fWGIPzn5-a$tr1q7;qcod0#C`7q5j)2u7X42TL!uH;#v^@1;6OG*#^u_A?K?<+7R(va#$!l~%r+eA!BHS0fL3s5%r}YA^Hq7N& z%hXEAS5lOpk%k%7W6q6Ux9&NFC*@*$4Q z*XPba*`4+wkA_>+wNC5GEM;w{+Jg0=fZNao&}DN($l@J`c#{sIdSp?z zu3qQ!qYvfHt4*zcVQrpGitj_dsX#vQ{$0EX{tuwh@I{3gPyS8f`JEgrOn9v(1U68J$FVZs)TAGLuuC_S6TZL ziqftc15AkiCfo+=arjY>5)cDWXa#8I?AN!8;iePje~ zNRx7El!-5Dfy|Obh{#h^vy^?VpwpUQJUm~fDs<3nLXDNht}4PoAWq}grI#SP+SohW zBt!SXCvK2bwScozgTatc)k;^8`DsZ+=Rqt^2`K|sx7rdvxm3Z5Wt7_5i^D*Y0BxwX zXdOd1HfC1c2+`KC_av#?-p5I-$bbh)28ZJ1yW*2yu{Zh)LVm+`i*@Ji)+nKaC80Y~ zV!PxWZNpp!(mPeGuarHJFp%Q^WN$XI~7suvSKee z^7Ii}OgQvp)+@dq_a5gr^3_`4gx%Iw*!_+jw|Q7j}fW2i|+#S za&}^j$3yg%XXO{T{bOM^DZf~Q=KUt za#cDJciHsW*yIb>Sq}iRj~OF8(Q(nuIZOTn#oM7@PbbK?Y!Acs3xKy;GKW6pbcCDc zdl}dFKczP_o8f=d`Bg8Kgy~X`r1I#0cVuo-LekuvE=VYI*Ci7o>ggh7iH;gF5-k*Q z?_+KvSa&a-WC(i8vouG1I*{kCIZwR$=$aX$!$&>;U9xF~7HoH&K1=g=rQPeL=+I)N zE*+wQjctVAZV>qm_op2*;Xx9Glyx>~mGiYVPZseKxL!l#OvW;sL8MLhm|N(sQ0SIR zYy*c1STP#uqDL3O3U1QVukdku3Bn8f034)~gg1~GNIsRo#+P)+Hmx0@3DL?&8P2*7 z$5+itB)$B)6dH0^KS79XY}Opw9Ui_6c?W7&M&l}gTQV&=w1Tp+WSLf+(%BbKQJ4RR zR~z*WN8p-vD+hVPk~#Sq*U!e30J@)mbTkcAp<+}{(4Fn^x%UZ?aBwBv&@3=0MI5B7 zq9o+HW<8>~UAS07*{G_-Tm;i2@YM@Bx^$LrSkIX`?HwaW7D1_!)JK4PY}DuPqV!yq z)c!zt^)rdhfVt1m-RL=*>0Vvr1tfzv$6Jv-i)}+<815+feudnD6~q`9#Hvhr(v?6_ zJG8hPTjQ|VdCU_b(N&+_OSW$Y{_nLKFmSQdx|j-%=mCCg!fkoZGm=#5+gblV&o=e) z&3QgV!B<`+!xzY3+iTCMs~f0gj33RA6~M_>t;YIe5Zx(Uxfd&h%uVxLoox_8motsUi7dhr$X# z9&DdCp$Pq{1&=6VIANk3DZ+^J7YGYSh|%0MAZ{bBInaCNl7e|ky?FB+QJ_{h!ElL$ zK&1}(j^~#i4Q{_6F~)Y$deckV)rc4&N8rJTSv`zwPJ`0`@v4wCoy{AR%k#WhIW{Vr zmD?Dy3WDgFIVNe%p@mRYhICE_+@cDcX}ga%9;T>VW_Pt`3oR`XOFfY*2&dj2r;NQ1 z$M#c19d}kimwM+SuXssp@mO?fJA~xVrn&Xek06Z`(ts1b-J@!(9iM-4{N`uD@V9=C zx^ck$J7ptvax7bSnT{X{b*?1N=dO~ zHFO#9N!_AzFuCkF;yTK7xOAE1y{Dm zQFi8LU!FQnMryRpt;AcWnMwMgHc%6_v6dX!i^vS9q7-|HI1h(oQFaz~<1qoOP@-(z z6<6ymiOP1*-jAh-Yegb~w&dinHkcjF^mfxO`|WK6_?;Me=>Hg^jIURV;>~j1Hm00M zbIf5LFK?GuDw1&B(Qlx1nkicCMwW9vC)q-DEr+G!)7gktOD= zSoh|bph0m-V&*mA zW40Pvo7td-#{#RIZJv~lzd&JzU0TGm={C*TD<%l^w$z-D8$STcQ|ZCXB}cR^<=?3g zs#XUAbt+l^?h#H{D0^mASK$Bf)pLP0887dXWl&Vu!i#K#L@ITbbPuIlWdt`3R3oaljAYH}oS@oWN$1W~3HNLT@Cx=Y|}7)0(PS8#kN{Jd=br zfFeu+daEmxj(xB6FX6eb<^6O0p3*)sW_C;gN+pr^&3Eh#Ng#S;m^%89?*Z!yqPskz znz>?NOkfLI*Sn{fYZ@bC19??kuf+yfWZ8eduN^(ng&K=Nu1*gSOkX@=aB^sNj2wF@ z6+Kyx(63pBxe_BG=QSFq1Z(hj3qyPhyb14}og0?Spc z*avq*gz`WOzEBXy+cy`iWuLZ z?k2mIWa3_{3JNv~O_ujxQDb{g`HLzB$mw@i-AI&~^Vi_-@`7M*O56oIX7A#C((J`K zNEU`uTy6&%>Ufo=+QkImn%SRHm&e;*0w`0a&(N9E*Q)Q*eHvN=wh6Z?Nk-6VqN5)6 zp8)`4oxlm5&D%rOWnvszBlIey()N<+FY4mzl41w<%?JaW1tqdV<5r^98hGc$r4Y-C z-7BcXPB~PC>Zh+omXV88h5WPMldPt{y|}X`VzmOkJchewNpC1?A*3jy0h&)}fs)9B zw-d6G1%&&#E~IhRF;gfAv3rXRCx)?BVVVn=k`r-UQd3WsszZ=y%5qL}QxBZ#hfV1@ zc8*8Shs7mAU6wEymaX$v8Jg~S9cR$YY<6+e>6s(ws8qwdh&PsH-BDqZ4fZIHQB7Ub z8I$BoS4_#`JI4(>wTnsOzgs?d7VB&}CyUmn@CwpBmKlgrhFL<(cPaPr7X9GK_ zg`T)FNS2&Z=yMNh)#viv-IUs@8dRjZP9#e zYUB&=CKwN|&*Fa|U=~`%6ZiviN>Kcncl{PQ^3BjzYA!l=5J@tpz2Z+&u?(D;iB zT@Y5KoFCITq3-n0D|3u1s^?|&|HJha6+bhpoL|tHU683!i7}^eWa>sz`8T~5Z;2}} zQhOaS$dg>mmbrRu?u-4XpCIj9SqfVK${jUBumXtFmo60ux09{b6(sH#!8V`seOJ^= z>RyQ2uupz3O!C;k?DF<5GKl&m5V~_9ropubVZqCtRkn6jlr5fkeMRC{Pwl&`w5q1oo9{l3JJ#IYr+Jk zwdB^dkb&^xNqUXQERHIAR-5~H>Voj% zipXb?Y4@vV^jBObTNam~ursuizmw0T?JN9k7&bT4DjuE+IA(kiEe4AK!^J0Lpw6N& z>Izyljfke$A`)vGgJ|vYUq@*&CYzHyTh%6^Fwm~_-X|$MJ6?!XkyHU9M-5T-17Tta zy_ZDEeEwlrsKaD^Bv;VdkJ}DwZaR-2an4`RV_s9JE1kK2-k{d~T{s#$v940KgVgtO z4_|lq`aLSc@$@Gn6t8vGHG1KckF*)V^m6h1$M@H`cO0m8_M4Hq{Ed>KE6l;RHm{0ben{e?C*hAU{N9bbMf)>2U;PeR9ll3`o#dd^bEDt` ztJ8bK=&d_>0~J}-=b~n0@d^G997DaaYgg3wG$APJe-G~BCC-GtpAPW z(wSQsT0790S=n0BeXF-9I_fzZ89Q0Z>YE!GI1>MFZxoHJ^lVLS?EixiwLrzp18V`r zdnl15mit$ufRrVW9X667v}OPprlpC~dA5=~ko|7IUqB6a%@76+MnpP@sidaHs-yT* z;w+9v({vz6ZhwA3JRLXOm%q(B7;a*xNxe|0;rN{rUgc%F$Kze5!<9!?`wOZs__fTB zr=57b4_Af2%asN!h-?fd0dcTujJgVXTa5v&AKv3*`cOtaQB2kQOa+8A>ll2s>}P** ze*cQR-q)-NcBo*Fg0AHzS(k;rz(CRcm5rr)vr~5aRc`Au5mfbd4ZFg-6l$}Z9%}P) z-Tr)SrS}?1jb`*x8F~eN4NP!kD?R{+;#x7jo~}oA97%H==1zQ-tRiG;>`sg5FeQRq zpDuAmA;@1nYo&98)w{}XT0j@OsR%e>eBI53^b)?oJhNl$}}g! zot4UPF77pGFE~N$bXIJgcB@V3p6(XM;CvNEh8TH?UUavZlLAU~qEt>foqJNR7p3k_ zOZKvSu*!p{E;do3w`o!o#aHEA%UGBp4hVWLh* zqtcI_ChHA}N{?TqeA&Q8Y)TvF1#MELh?v#h7=c&@TChDW-^CbHLv>IL&#!CbMXNo) zw6Y;*Xyw#UdKVjrLU~S?LcqVLpWlMG!Oh6X$u7IJwArjTUSY6QJjIaKAh`!cOli+x zCY7?QuB%)X%oXkX8*CB0?G37_ViqiVzs}7>L&!H%jRI%-OyOO)dxYujr%T!VR`(aB z1&#R~>YL1q#XE1e3DY}h%i!IK^PN^A%B+ymTpfLrow(YZ+8ka@fr=*XMk$F z5TJU)R;bjsbpr?c)o+1GD;chO-RjS!B9_Vi1SYR|ITr;N<3)faX?pXpe5L@EG) zXfK`G@dcczsoWI$KWZ{ai@k+$$-4m$30-v=zBBCwl<$-?mK^N%Jnc|r z$3Aww=Yh%;a2Hsq1*aGibk@X-P$M)iXYI{|<%fmKRi}(2m~%Lnk0-V5j1_oIcuAZm z(a{ir<|9)f#9}oI0l%LhPMSUVtRW)25^sBy<$S1ou1+0%`;H6SJ$4*=r?c#Df>a{A zW|+bp+OupVu2C?eF~+pzDuJJ}m~f#wySMvyuYtD)N@jd^q8 zlfIaiat6USeO+uZI!R@?zN){+4ry68e7~BLVTN6PRw&XcCYfMpFcqE>v+D3`{I^SU!w7}&?wF=&Edt)1?;5l{R;p^fNp;e{M_~~TgIa4)MAdC8PM=_d% zLVBCM(ahdQ&r z?%h2zup@Xy;0eNLw=rkB-S2t{*Z;yL z{{M8Ty#I$T)yP=S$$POFAhe=WS0n1q98n@qPf;87x%4Pgh|$=x<*sG*b6ByRi;0mh)As~VX001+eh*x#GG z&`(hwS1DrN%N{#81>E%OT}bYe9z{mJi=HMP*TKIgJg$=wSIsxtbe%zDfqQS(@7)nc zD7SqxpWZZuaw|1t^w_3Sqyjr49MHnASX}pQDuHHpG^kJkO%^q-cvO;-ou53u6l@Ono_uHP1~tzp3k`D>`DW^{2PLy{~~ zvRjxp%`sWc@SakiKnT}Q?r+o5G}2U4jmOnSnnvUPnx0xu3##n@9UN_JIqeJ{yA$v% zjWtg=KW-vt%0W`R;yO_9p`rh`efB+dHLgq1dp*CrlmjCt`DlKtBfc^mOfaB^C?0*e zknh$_-Ic8CmZGElQjo~R@acE z2mMzIqxa9oDNdH~V?B=t$-I_>rFqlC?$%vLg7Ea~;4c24Xy5TIVEK!;3Cy>j_mNGH z1hHB^~N4_M81r}ooT0W z27USznzsSV(w*Y}8t(Q*y%EiY^wIx)Cn~BYixplnaMd?;6ADvBiToB+zZ{(y42mfK zaf*p;yT^j(P0-Cp^T(Uwk2lF=i$Y^pqOyRBIEIK}vjD)0@g1zo=wr2K>G3LZ@ZHZ* z>N$e^a4sx%>Biof5;B>WPq1*Hihha^B2|y+g*VD(de>zS7H_lvCuq=@0N!Tr+IT_O z$<-t+ALKJ8Kr*23A_*jZ;hy>?-}Z2*V$-^PukY%b_H00C&Nu~v)PP~O+6O%4Nt310 zfExRpx}0$VuYv|v$>+@qxaK%`22f}u0x~v15l=y?{Ipo%X_<*@A!K_A!$!^Tro8Wqk#HFtQrtx~R z8-FCU#XQP1P(X(+l${=CW*Y2Y+?^{CaApM#9C$dEs|MOBYhGhc2q75&CNh6^BKOS zibFfmJ?c;7T0=3aPt3P2i7m3-4%ncBgJm{QQ70N6?2}}q)mqJJG*D_fq3Vjp+muQM ze3&;FmU|Ub(|&g~f9raSs^{jnhSe``PjQ3x5F|}Iy_RBc2xgnvvt;lm(Q0B9H*5QfDlK5Q0*XL@GVdJNN=NUSms+I2SxI#xJ zml)i~WvJ%=sHt8Uiha$D<5yKnmj8w%02FiicwZgkG30X!*%wO!aL7Me@jVm#@zu|vZwgIg2I zh(ZcabRv)32b0M;vwOT&onjm{7nyRDvc))4DW9G!uqCd}=u%#v(%9>?5V1sfBQem) zk@J~hryEPFL%g)ftcOc2z(xFvw{iO^-#e@Ez31_gi{{KZRd#oqH=O@&!^`#qDQrHs zz63!?S`m*qt8Aqq6~%TFJ@F**bnK)s=g!PDkvf$~1sOq|=jH6=?G=XVbNG#KX2^$h zK{r{#M;PT(b(G2ospSWX584H!>-=nL zyQ_vlbglflxWQe&KwP_idiM;0@uh83L3GA$A0fVT`)A2sEBDJwUqeH7rf#EsHEkQ9 zKFcF{HaIjyrcT@v6U0s}YVw~#p_YY?B&g7kEracsp@6g@8kwUQml`YCK9~TktY56gX$gh&d>DHCjBXa z^o8CxcHLjR!WgBoV<^_XPFgb?k3MaaaWLtmfKoM1(lHp@$((e*FDj<`e!6BO;gcb) zs9>1o*u^l;JQ~aV?q6y@52jGeX6-zTAY1-d8mvUsyobEh^5UD>CdpA!oj0Op;LV>L`JMA&ET)(w8T_TLiiHZ>vNgHf zUp4IOJ-zgyU@QQK1#Q-T9Ms)kSo&z#Bqp|Au)ok!hc5-@!NiRAl+}|5pLpor)85NB@oD4c;u4gdJyouk64H3U6uN-9MQuA!n>KX9Oef})| zm8Tl9r{L6e0FZo9J<`xBn!~FBJ;vO>aM{}qnl3Zcf;!}9tL*BJz~VIok9i#_SU+Jt z=@1`We=mRVF{`1)CQps5Q!sS{s))*YPtJTs`qF%aA3M4??2N&qzFm@R?Z)Jl2K^eY z=y67V|7STX(a_=t{5Q=N1;)QCXZ=qA+5fei^{<{~MeLR=56LlWV^B4)h;NW(ItJ?D81-GbDRc4F%D&5VVs@C4CytXN99SfO4O(w z-H7Z+5VjM=JXaSu7+0VZvdpZS5BWqhW`5j!(A(RivDQ-*oLPq3*BA`?xS!9WL_M|| z2);@-rrO^jKgnwBW}q@Zi)!3v1obFOA!`xd5K9qjVcrN&)E91hEFF-}dt%+HUJr28 zcD~nCvL=#SHB!>)ci?}NIYF;aJA1#y4bi_fPyS~T2I2o9CsWdMu=qY3_TRdO|EO{P zx4-|$%ZUHulaiU$f9f688#OJYYTkN+yXg`onIl@UDiBOd+bXdK�`oww;g#zLeDvv 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

##Gtfg5isZD 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
  • DrFac5W%s#cosR%kx5GYHfcc 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�v~}--_d>j`Ob-D*p=;J0L@C%XFKvU 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 Date: Wed, 3 Dec 2014 11:58:14 -0800 Subject: [PATCH 76/76] Update Changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 621a9d05..1dc64db8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Connect SDK Android Changelog +## 1.4.0 -- 3 Dec 2014 + +- Modularized project to allow easy exclusion of modules that have heavy and/or external dependencies +- Improved support for DLNA devices + - DLNA volume control subscriptions + - DLNA play state subscriptions + - DLNA media info +- Unit tests for the discovery services providers +- Miscellaneous bug fixes +- [See commits between 1.3.2 and 1.4.0](https://github.com/ConnectSDK/Connect-SDK-Android/compare/1.3.2...1.4.0) + +[View files at version 1.4.0](https://github.com/ConnectSDK/Connect-SDK-Android/tree/1.4.0) + ## 1.3.2 -- 6 Aug 2014 - Added launchYouTube(String contentId, float startTime, AppLaunchListener listener) method to Launcher capability