com.entertailion.java.fling.RampClient.java Source code

Java tutorial

Introduction

Here is the source code for com.entertailion.java.fling.RampClient.java

Source

/*
 * Copyright (C) 2013 ENTERTAILION, LLC.
 *
 * 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.entertailion.java.fling;

import java.io.Reader;
import java.io.StringReader;
import java.net.URI;
import java.util.UUID;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolException;
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.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.DefaultRedirectHandler;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.java_websocket.handshake.ServerHandshake;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;

/*
 * Manage RAMP protocol 
 * 
 * @author leon_nicholls
 */
public class RampClient implements RampWebSocketListener {

    private static final String LOG_TAG = "RampClient";

    private static final String STATE_RUNNING = "running";
    private static final String STATE_STOPPED = "stopped";

    private static final String HEADER_CONNECTION = "Connection";
    private static final String HEADER_CONNECTION_VALUE = "keep-alive";
    private static final String HEADER_ORIGN = "Origin";
    private static final String HEADER_ORIGIN_VALUE = "chrome-extension://boadgeojelhgndaghljhdicfkmllpafd";
    private static final String HEADER_USER_AGENT = "User-Agent";
    private static final String HEADER_USER_AGENT_VALUE = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.71 Safari/537.36";
    private static final String HEADER_DNT = "DNT";
    private static final String HEADER_DNT_VALUE = "1";
    private static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
    private static final String HEADER_ACCEPT_ENCODING_VALUE = "gzip,deflate,sdch";
    private static final String HEADER_ACCEPT = "Accept";
    private static final String HEADER_ACCEPT_VALUE = "*/*";
    private static final String HEADER_ACCEPT_LANGUAGE = "Accept-Language";
    private static final String HEADER_ACCEPT_LANGUAGE_VALUE = "en-US,en;q=0.8";
    private static final String HEADER_CONTENT_TYPE = "Content-Type";
    private static final String HEADER_CONTENT_TYPE_JSON_VALUE = "application/json";
    private static final String HEADER_CONTENT_TYPE_TEXT_VALUE = "text/plain";

    private String connectionServiceUrl;
    private String state;
    private String protocol;
    private String response;
    private boolean started;
    private boolean closed;

    private RampWebSocketClient rampWebSocketClient;
    private int commandId;
    private String app;
    private String activityId;
    private String senderId;

    private Thread pongThread;
    private DialServer dialServer;
    private FlingFrame flingFrame;

    public RampClient(FlingFrame flingFrame) {
        this.flingFrame = flingFrame;
        this.senderId = UUID.randomUUID().toString();
    }

    public void launchApp(String app, DialServer dialServer) {
        this.app = app;
        this.dialServer = dialServer;
        this.activityId = UUID.randomUUID().toString();
        try {
            String device = "http://" + dialServer.getIpAddress().getHostAddress() + ":" + dialServer.getPort();
            Log.d(LOG_TAG, "device=" + device);
            Log.d(LOG_TAG, "apps url=" + dialServer.getAppsUrl());

            // application instance url
            String location = null;

            DefaultHttpClient defaultHttpClient = HttpRequestHelper.createHttpClient();
            CustomRedirectHandler handler = new CustomRedirectHandler();
            defaultHttpClient.setRedirectHandler(handler);
            BasicHttpContext localContext = new BasicHttpContext();

            // check if any app is running
            HttpGet httpGet = new HttpGet(dialServer.getAppsUrl());
            httpGet.setHeader(HEADER_CONNECTION, HEADER_CONNECTION_VALUE);
            httpGet.setHeader(HEADER_USER_AGENT, HEADER_USER_AGENT_VALUE);
            httpGet.setHeader(HEADER_ACCEPT, HEADER_ACCEPT_VALUE);
            httpGet.setHeader(HEADER_DNT, HEADER_DNT_VALUE);
            httpGet.setHeader(HEADER_ACCEPT_ENCODING, HEADER_ACCEPT_ENCODING_VALUE);
            httpGet.setHeader(HEADER_ACCEPT_LANGUAGE, HEADER_ACCEPT_LANGUAGE_VALUE);
            HttpResponse httpResponse = defaultHttpClient.execute(httpGet);
            if (httpResponse != null) {
                int responseCode = httpResponse.getStatusLine().getStatusCode();
                Log.d(LOG_TAG, "get response code=" + httpResponse.getStatusLine().getStatusCode());
                if (responseCode == 204) {
                    // nothing is running
                } else if (responseCode == 200) {
                    // app is running

                    // Need to get real URL after a redirect
                    // http://stackoverflow.com/a/10286025/594751
                    String lastUrl = dialServer.getAppsUrl();
                    if (handler.lastRedirectedUri != null) {
                        lastUrl = handler.lastRedirectedUri.toString();
                        Log.d(LOG_TAG, "lastUrl=" + lastUrl);
                    }

                    String response = EntityUtils.toString(httpResponse.getEntity());
                    Log.d(LOG_TAG, "get response=" + response);
                    parseXml(new StringReader(response));

                    Header[] headers = httpResponse.getAllHeaders();
                    for (int i = 0; i < headers.length; i++) {
                        Log.d(LOG_TAG, headers[i].getName() + "=" + headers[i].getValue());
                    }

                    // stop the app instance
                    HttpDelete httpDelete = new HttpDelete(lastUrl);
                    httpResponse = defaultHttpClient.execute(httpDelete);
                    if (httpResponse != null) {
                        Log.d(LOG_TAG, "delete response code=" + httpResponse.getStatusLine().getStatusCode());
                        response = EntityUtils.toString(httpResponse.getEntity());
                        Log.d(LOG_TAG, "delete response=" + response);
                    } else {
                        Log.d(LOG_TAG, "no delete response");
                    }
                }

            } else {
                Log.i(LOG_TAG, "no get response");
                return;
            }

            // Check if app is installed on device
            int responseCode = getAppStatus(defaultHttpClient, dialServer.getAppsUrl() + app);
            if (responseCode != 200) {
                return;
            }
            parseXml(new StringReader(response));
            Log.d(LOG_TAG, "state=" + state);

            // start the app with POST
            HttpPost httpPost = new HttpPost(dialServer.getAppsUrl() + app);
            httpPost.setHeader(HEADER_CONNECTION, HEADER_CONNECTION_VALUE);
            httpPost.setHeader(HEADER_ORIGN, HEADER_ORIGIN_VALUE);
            httpPost.setHeader(HEADER_USER_AGENT, HEADER_USER_AGENT_VALUE);
            httpPost.setHeader(HEADER_DNT, HEADER_DNT_VALUE);
            httpPost.setHeader(HEADER_ACCEPT_ENCODING, HEADER_ACCEPT_ENCODING_VALUE);
            httpPost.setHeader(HEADER_ACCEPT, HEADER_ACCEPT_VALUE);
            httpPost.setHeader(HEADER_ACCEPT_LANGUAGE, HEADER_ACCEPT_LANGUAGE_VALUE);
            httpPost.setHeader(HEADER_CONTENT_TYPE, HEADER_CONTENT_TYPE_TEXT_VALUE);
            if (app.equals(FlingFrame.CHROMECAST)) {
                httpPost.setEntity(new StringEntity(
                        "v=release-d4fa0a24f89ec5ba83f7bf3324282c8d046bf612&id=local%3A1&idle=windowclose"));
            }

            httpResponse = defaultHttpClient.execute(httpPost, localContext);
            if (httpResponse != null) {
                Log.d(LOG_TAG, "post response code=" + httpResponse.getStatusLine().getStatusCode());
                response = EntityUtils.toString(httpResponse.getEntity());
                Log.d(LOG_TAG, "post response=" + response);
                Header[] headers = httpResponse.getHeaders("LOCATION");
                if (headers.length > 0) {
                    location = headers[0].getValue();
                    Log.d(LOG_TAG, "post response location=" + location);
                }

                headers = httpResponse.getAllHeaders();
                for (int i = 0; i < headers.length; i++) {
                    Log.d(LOG_TAG, headers[i].getName() + "=" + headers[i].getValue());
                }
            } else {
                Log.i(LOG_TAG, "no post response");
                return;
            }

            // Keep trying to get the app status until the
            // connection service URL is available
            state = STATE_STOPPED;
            do {
                responseCode = getAppStatus(defaultHttpClient, dialServer.getAppsUrl() + app);
                if (responseCode != 200) {
                    break;
                }
                parseXml(new StringReader(response));
                Log.d(LOG_TAG, "state=" + state);
                Log.d(LOG_TAG, "connectionServiceUrl=" + connectionServiceUrl);
                Log.d(LOG_TAG, "protocol=" + protocol);
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                }
            } while (state.equals(STATE_RUNNING) && connectionServiceUrl == null);

            if (connectionServiceUrl == null) {
                Log.i(LOG_TAG, "connectionServiceUrl is null");
                return; // oops, something went wrong
            }

            // get the websocket URL
            String webSocketAddress = null;
            httpPost = new HttpPost(connectionServiceUrl); // "http://192.168.0.17:8008/connection/YouTube"
            httpPost.setHeader(HEADER_CONNECTION, HEADER_CONNECTION_VALUE);
            httpPost.setHeader(HEADER_ORIGN, HEADER_ORIGIN_VALUE);
            httpPost.setHeader(HEADER_USER_AGENT, HEADER_USER_AGENT_VALUE);
            httpPost.setHeader(HEADER_DNT, HEADER_DNT_VALUE);
            httpPost.setHeader(HEADER_ACCEPT_ENCODING, HEADER_ACCEPT_ENCODING_VALUE);
            httpPost.setHeader(HEADER_ACCEPT, HEADER_ACCEPT_VALUE);
            httpPost.setHeader(HEADER_ACCEPT_LANGUAGE, HEADER_ACCEPT_LANGUAGE_VALUE);
            httpPost.setHeader(HEADER_CONTENT_TYPE, HEADER_CONTENT_TYPE_JSON_VALUE);
            httpPost.setEntity(new StringEntity("{\"channel\":0,\"senderId\":{\"appName\":\"" + app
                    + "\", \"senderId\":\"" + senderId + "\"}}"));

            httpResponse = defaultHttpClient.execute(httpPost, localContext);
            if (httpResponse != null) {
                responseCode = httpResponse.getStatusLine().getStatusCode();
                Log.d(LOG_TAG, "post response code=" + responseCode);
                if (responseCode == 200) {
                    // should return JSON payload
                    response = EntityUtils.toString(httpResponse.getEntity());
                    Log.d(LOG_TAG, "post response=" + response);
                    Header[] headers = httpResponse.getAllHeaders();
                    for (int i = 0; i < headers.length; i++) {
                        Log.d(LOG_TAG, headers[i].getName() + "=" + headers[i].getValue());
                    }

                    // http://code.google.com/p/json-simple/
                    JSONParser parser = new JSONParser();
                    try {
                        Object obj = parser.parse(new StringReader(response)); // {"URL":"ws://192.168.0.17:8008/session?33","pingInterval":0}
                        JSONObject jsonObject = (JSONObject) obj;
                        webSocketAddress = (String) jsonObject.get("URL");
                        Log.d(LOG_TAG, "webSocketAddress: " + webSocketAddress);
                        long pingInterval = (Long) jsonObject.get("pingInterval"); // TODO
                    } catch (Exception e) {
                        Log.e(LOG_TAG, "parse JSON", e);
                    }
                }
            } else {
                Log.i(LOG_TAG, "no post response");
                return;
            }

            // Make a web socket connection for doing RAMP
            // to control media playback
            this.started = false;
            this.closed = false;
            if (webSocketAddress != null) {
                // https://github.com/TooTallNate/Java-WebSocket
                URI uri = URI.create(webSocketAddress);

                rampWebSocketClient = new RampWebSocketClient(uri, this);

                new Thread(new Runnable() {
                    public void run() {
                        Thread t = new Thread(rampWebSocketClient);
                        t.start();
                        try {
                            t.join();
                        } catch (InterruptedException e1) {
                            e1.printStackTrace();
                        } finally {
                            rampWebSocketClient.close();
                        }
                    }
                }).start();
            } else {
                Log.i(LOG_TAG, "webSocketAddress is null");
            }

        } catch (Exception e) {
            Log.e(LOG_TAG, "launchApp", e);
        }
    }

    public void closeCurrentApp() {
        if (dialServer != null) {
            try {
                DefaultHttpClient defaultHttpClient = HttpRequestHelper.createHttpClient();
                CustomRedirectHandler handler = new CustomRedirectHandler();
                defaultHttpClient.setRedirectHandler(handler);
                BasicHttpContext localContext = new BasicHttpContext();

                // check if any app is running
                HttpGet httpGet = new HttpGet(dialServer.getAppsUrl());
                httpGet.setHeader(HEADER_CONNECTION, HEADER_CONNECTION_VALUE);
                httpGet.setHeader(HEADER_USER_AGENT, HEADER_USER_AGENT_VALUE);
                httpGet.setHeader(HEADER_ACCEPT, HEADER_ACCEPT_VALUE);
                httpGet.setHeader(HEADER_DNT, HEADER_DNT_VALUE);
                httpGet.setHeader(HEADER_ACCEPT_ENCODING, HEADER_ACCEPT_ENCODING_VALUE);
                httpGet.setHeader(HEADER_ACCEPT_LANGUAGE, HEADER_ACCEPT_LANGUAGE_VALUE);
                HttpResponse httpResponse = defaultHttpClient.execute(httpGet);
                if (httpResponse != null) {
                    int responseCode = httpResponse.getStatusLine().getStatusCode();
                    Log.d(LOG_TAG, "get response code=" + httpResponse.getStatusLine().getStatusCode());
                    if (responseCode == 204) {
                        // nothing is running
                    } else if (responseCode == 200) {
                        // app is running

                        // Need to get real URL after a redirect
                        // http://stackoverflow.com/a/10286025/594751
                        String lastUrl = dialServer.getAppsUrl();
                        if (handler.lastRedirectedUri != null) {
                            lastUrl = handler.lastRedirectedUri.toString();
                            Log.d(LOG_TAG, "lastUrl=" + lastUrl);
                        }

                        String response = EntityUtils.toString(httpResponse.getEntity());
                        Log.d(LOG_TAG, "get response=" + response);
                        parseXml(new StringReader(response));

                        Header[] headers = httpResponse.getAllHeaders();
                        for (int i = 0; i < headers.length; i++) {
                            Log.d(LOG_TAG, headers[i].getName() + "=" + headers[i].getValue());
                        }

                        // stop the app instance
                        HttpDelete httpDelete = new HttpDelete(lastUrl);
                        httpResponse = defaultHttpClient.execute(httpDelete);
                        if (httpResponse != null) {
                            Log.d(LOG_TAG, "delete response code=" + httpResponse.getStatusLine().getStatusCode());
                            response = EntityUtils.toString(httpResponse.getEntity());
                            Log.d(LOG_TAG, "delete response=" + response);
                        } else {
                            Log.d(LOG_TAG, "no delete response");
                        }
                    }

                } else {
                    Log.i(LOG_TAG, "no get response");
                    return;
                }
            } catch (Exception e) {
                Log.e(LOG_TAG, "closeCurrentApp", e);
            }
        }
    }

    /**
     * Do HTTP GET for app status to determine response code and response body
     * 
     * @param defaultHttpClient
     * @param url
     * @return
     */
    private int getAppStatus(DefaultHttpClient defaultHttpClient, String url) {
        int responseCode = 200;
        try {
            HttpGet httpGet = new HttpGet(url);
            HttpResponse httpResponse = defaultHttpClient.execute(httpGet);
            if (httpResponse != null) {
                responseCode = httpResponse.getStatusLine().getStatusCode();
                Log.d(LOG_TAG, "get response code=" + responseCode);
                response = EntityUtils.toString(httpResponse.getEntity());
                Log.d(LOG_TAG, "get response=" + response);
            } else {
                Log.i(LOG_TAG, "no get response");
            }
        } catch (Exception e) {
            Log.e(LOG_TAG, "getAppStatus", e);
        }
        return responseCode;
    }

    private void parseXml(Reader reader) {
        try {
            InputSource inStream = new org.xml.sax.InputSource();
            inStream.setCharacterStream(reader);
            SAXParserFactory spf = SAXParserFactory.newInstance();
            SAXParser sp = spf.newSAXParser();
            XMLReader xr = sp.getXMLReader();
            AppHandler appHandler = new AppHandler();
            xr.setContentHandler(appHandler);
            xr.parse(inStream);

            connectionServiceUrl = appHandler.getConnectionServiceUrl();
            state = appHandler.getState();
            protocol = appHandler.getProtocol();
        } catch (Exception e) {
            Log.e(LOG_TAG, "parse device description", e);
        }
    }

    /**
     * Custom HTTP redirection handler to keep track of the redirected URL
     * ChromeCast web server will redirect "/apps" to "/apps/YouTube" if that is
     * the active/last app
     * 
     */
    public class CustomRedirectHandler extends DefaultRedirectHandler {

        public URI lastRedirectedUri;

        @Override
        public boolean isRedirectRequested(HttpResponse response, HttpContext context) {

            return super.isRedirectRequested(response, context);
        }

        @Override
        public URI getLocationURI(HttpResponse response, HttpContext context) throws ProtocolException {

            lastRedirectedUri = super.getLocationURI(response, context);

            return lastRedirectedUri;
        }

    }

    // RampWebSocketListener callbacks
    public void onMessage(String message) {
        Log.d(LOG_TAG, "onMessage: message" + message);

        // TODO only respond based on interval
        // rampWebSocketClient.send("[\"cm\",{\"type\":\"pong\"}]");

        // ["cv",{"type":"activity","message":{"type":"timeupdate","activityId":"d82cede3-ec23-4f73-8abc-343dd9ca6dbb","state":{"mediaUrl":"http://192.168.0.50:8087/cast.webm","videoUrl":"http://192.168.0.50:8087/cast.webm","currentTime":20.985000610351562,"duration":null,"pause":false,"muted":false,"volume":1,"paused":false}}}]
        // Should really parse JSON, but only need the current time
        int start = message.indexOf("\"currentTime\":");
        if (start > 0) {
            int end = message.indexOf(",", start);
            String time = message.substring(start + 14, end);
            Log.d(LOG_TAG, "currentTime=" + time);
            flingFrame.updateTime((int) Float.parseFloat(time));
        }
    }

    public void onError(Exception ex) {
        Log.d(LOG_TAG, "onError: ex" + ex);
        ex.printStackTrace();

        started = false;
        closed = true;

        pongThread.interrupt();
    }

    public void onOpen(ServerHandshake handshake) {
        Log.d(LOG_TAG, "onOpen: handshake" + handshake);

        started = true;
        closed = false;

        if (pongThread != null) {
            pongThread.interrupt();
        }

        pongThread = new Thread(new Runnable() {
            public void run() {
                while (started && !closed) {
                    try {
                        rampWebSocketClient.send("[\"cm\",{\"type\":\"pong\"}]");
                        try {
                            Thread.sleep(3000); // pong every 3 seconds to keep
                            // the app alive
                        } catch (InterruptedException e) {
                        }
                    } catch (Exception e) {
                        Log.e(LOG_TAG, "pongThread", e);
                    }
                }
            }
        });
        pongThread.start();
    }

    public void onClose(int code, String reason, boolean remote) {
        Log.d(LOG_TAG, "onClose: code" + code + ", reason=" + reason + ", remote=" + remote);

        closed = true;
        started = false;

        pongThread.interrupt();

        flingFrame.updateTime(0);
    }

    // Media playback controls
    public void play() {
        if (rampWebSocketClient != null) {
            rampWebSocketClient.send("[\"ramp\",{\"type\":\"PLAY\", \"cmd_id\":" + commandId + "}]");
            commandId++;
        }
    }

    public void pause() {
        if (rampWebSocketClient != null) {
            rampWebSocketClient.send("[\"ramp\",{\"type\":\"STOP\", \"cmd_id\":" + commandId + "}]");
            commandId++;
        }
    }

    public void stop() {
        // ChromeCast app stop behaves like pause
        /*
         * if (rampWebSocketClient != null) {
         * rampWebSocketClient.send("[\"ramp\",{\"type\":\"STOP\", \"cmd_id\":"
         * + commandId + "}]"); commandId++; }
         */
        // Close the current app
        closeCurrentApp();
    }

    // Load media
    public void load(String url) {
        if (rampWebSocketClient != null) {
            if (app.equals(FlingFrame.CHROMECAST)) {
                rampWebSocketClient.send(
                        "[\"cv\",{\"type\":\"launch_service\",\"message\":{\"action\":\"launch\",\"activityType\":\"video_playback\",\"activityId\":\""
                                + activityId + "\",\"senderId\":\"" + senderId
                                + "\",\"receiverId\":\"local:1\",\"disconnectPolicy\":\"continue\",\"initParams\":{\"mediaUrl\":\""
                                + url + "\",\"videoUrl\":\"" + url
                                + "\",\"currentTime\":0,\"duration\":0,\"pause\":false,\"muted\":false,\"volume\":1}}}]");
            } else {
                rampWebSocketClient.send("[\"ramp\",{\"title\":\"Video\",\"src\":\"" + url
                        + "\",\"type\":\"LOAD\",\"cmd_id\":" + commandId + ",\"autoplay\":true}]");
            }
            commandId++;
        }
    }

    // Web socket status
    public boolean isStarted() {
        return started;
    }

    public boolean isClosed() {
        return closed;
    }
}