/* * 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 android.util.Log; import com.connectsdk.core.AppInfo; import com.connectsdk.core.Util; import com.connectsdk.discovery.DiscoveryFilter; import com.connectsdk.etc.helper.DeviceServiceReachability; import com.connectsdk.etc.helper.HttpConnection; import com.connectsdk.etc.helper.HttpMessage; import com.connectsdk.service.capability.CapabilityMethods; 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; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; public class DIALService extends DeviceService implements Launcher { public static final String ID = "DIAL"; private static final String APP_NETFLIX = "Netflix"; private static List<String> registeredApps = new ArrayList<String>(); static { registeredApps.add("YouTube"); registeredApps.add("Netflix"); registeredApps.add("Amazon"); } public static void registerApp(String appId) { if (!registeredApps.contains(appId)) registeredApps.add(appId); } public DIALService(ServiceDescription serviceDescription, ServiceConfig serviceConfig) { super(serviceDescription, serviceConfig); } @Override public CapabilityPriorityLevel getPriorityLevel(Class<? extends CapabilityMethods> clazz) { if (clazz.equals(Launcher.class)) { return getLauncherCapabilityLevel(); } return CapabilityPriorityLevel.NOT_SUPPORTED; } public static DiscoveryFilter discoveryFilter() { return new DiscoveryFilter(ID, "urn:dial-multiscreen-org:service:dial:1"); } @Override public void setServiceDescription(ServiceDescription serviceDescription) { super.setServiceDescription(serviceDescription); Map<String, List<String>> responseHeaders = this.getServiceDescription().getResponseHeaders(); if (responseHeaders != null) { String commandPath; List<String> 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); } @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<ResponseListener<Object>> command = new ServiceCommand<ResponseListener<Object>>(getCommandProcessor(), requestURL(appInfo.getName()), params, new ResponseListener<Object>() { @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<Object> 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<ResponseListener<Object>> command = new ServiceCommand<ResponseListener<Object>>(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(Locale.US, "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 { params = new JSONObject() {{ put("v", contentId); }}; } catch (JSONException e) { Log.e(Util.T, "Launch Netflix error", e); } } AppInfo appInfo = new AppInfo(APP_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<Object> responseListener = new ResponseListener<Object>() { @Override public void onSuccess(Object response) { String str = (String)response; String[] stateTAG = new String[2]; stateTAG[0] = "<state>"; stateTAG[1] = "</state>"; 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<ResponseListener<Object>> request = new ServiceCommand<ResponseListener<Object>>(getCommandProcessor(), 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<AppInfoListener> subscribeRunningApp(AppInfoListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); return new NotSupportedServiceSubscription<AppInfoListener>(); } @Override public void getAppState(LaunchSession launchSession, AppStateListener listener) { // TODO Auto-generated method stub } @Override public ServiceSubscription<AppStateListener> subscribeAppState( LaunchSession launchSession, com.connectsdk.service.capability.Launcher.AppStateListener listener) { // TODO Auto-generated method stub return null; } @Override public void closeLaunchSession(LaunchSession launchSession, ResponseListener<Object> 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<ResponseListener<Object>> command = (ServiceCommand<ResponseListener<Object>>) mCommand; Object payload = command.getPayload(); try { HttpConnection connection = createHttpConnection(mCommand.getTarget()); if (payload != null || command.getHttpMethod().equalsIgnoreCase(ServiceCommand.TYPE_POST)) { connection.setMethod(HttpConnection.Method.POST); if (payload != null) { connection.setHeader(HttpMessage.CONTENT_TYPE_HEADER, "text/plain; " + "charset=\"utf-8\""); connection.setPayload(payload.toString()); } } else if (command.getHttpMethod().equalsIgnoreCase(ServiceCommand.TYPE_DEL)) { connection.setMethod(HttpConnection.Method.DELETE); } connection.execute(); int code = connection.getResponseCode(); if (code == 200) { Util.postSuccess(command.getResponseListener(), connection.getResponseString()); } else if (code == 201) { Util.postSuccess(command.getResponseListener(), connection.getResponseHeader("Location")); } else { Util.postError(command.getResponseListener(), ServiceCommandError.getError(code)); } } catch (Exception e) { Util.postError(command.getResponseListener(), new ServiceCommandError(0, e.getMessage(), null)); } } }); } HttpConnection createHttpConnection(String target) throws IOException { return HttpConnection.newInstance(URI.create(target)); } 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<String> capabilities = new ArrayList<String>(); capabilities.add(Application); capabilities.add(Application_Params); capabilities.add(Application_Close); capabilities.add(AppState); setCapabilities(capabilities); } private void hasApplication(String appID, ResponseListener<Object> listener) { String uri = requestURL(appID); ServiceCommand<ResponseListener<Object>> command = new ServiceCommand<ResponseListener<Object>>(getCommandProcessor(), uri, null, listener); command.setHttpMethod(ServiceCommand.TYPE_GET); command.send(); } private void probeForAppSupport() { if (serviceDescription.getApplicationURL() == null) { Log.d(Util.T, "unable to check for installed app; no service application url"); return; } for (final String appID : registeredApps) { hasApplication(appID, new ResponseListener<Object>() { @Override public void onError(ServiceCommandError error) { } @Override public void onSuccess(Object object) { addCapability("Launcher." + appID); addCapability("Launcher." + appID + ".Params"); } }); } } }