Java tutorial
/* * 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.android.dial; import java.io.Reader; import java.io.StringReader; import java.net.InetAddress; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; 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.message.BasicNameValuePair; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.protocol.HttpContext; import org.apache.http.util.EntityUtils; import org.json.JSONException; import org.json.JSONObject; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserFactory; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.widget.Toast; import com.codebutler.android_websockets.WebSocketClient; /** * DIAL protocol client: * http://www.dial-multiscreen.org/dial-protocol-specification * * @author leon_nicholls * */ public class MainActivity extends Activity { private static final String LOG_TAG = "MainActivity"; public static final String PREFS_NAME = "preferences"; private static final String YOU_TUBE = "YouTube"; private static final String FLING = "Fling"; private static final String NETFLIX = "Netflix"; private static final String CHROME_CAST = "ChromeCast"; private static final String PLAY_MOVIES = "PlayMovies"; private static final String TIC_TAC_TOE = "TicTacToe"; 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"; /** * Request code used by this activity. */ protected static final int CODE_SWITCH_SERVER = 1; private DialServer target; private LinkedHashMap<InetAddress, DialServer> recentlyConnected = new LinkedHashMap<InetAddress, DialServer>(); private WebSocketClient client; private String connectionServiceUrl; private String state; private String protocol; private String response; /* * @see android.app.Activity#onCreate(android.os.Bundle) */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); startActivityForResult(ServerFinder.createConnectIntent(this, target, getRecentlyConnected()), CODE_SWITCH_SERVER); } /* * @see android.app.Activity#onCreateOptionsMenu(android.view.Menu) */ @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.activity_main, menu); return true; } /* * The user has selected a DIAL server to connect to * * @see android.app.Activity#onActivityResult(int, int, * android.content.Intent) */ @Override protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) { if (requestCode == CODE_SWITCH_SERVER) { if (resultCode == RESULT_OK && data != null) { final DialServer dialServer = data.getParcelableExtra(ServerFinder.EXTRA_DIAL_SERVER); if (dialServer != null) { Toast.makeText(MainActivity.this, getString(R.string.finder_connected, dialServer.toString()), Toast.LENGTH_LONG).show(); new Thread(new Runnable() { public void run() { 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; String app = YOU_TUBE; 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(MainActivity.this, 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(MainActivity.this, 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); // Set variable values as the body of the POST; // v is the YouTube video id. httpPost.setEntity(new StringEntity( "pairingCode=eac4ae42-8b54-4441-9be3-d8a9abb5c481&v=cKG5HDyTW8o&t=0")); // http://www.youtube.com/watch?v=cKG5HDyTW8o 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(MainActivity.this, 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\":\"ChromeCast\", \"senderId\":\"7v3zqrpliq3i\"}}")); 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()); } JSONObject jObject; try { jObject = new JSONObject(response); // {"URL":"ws://192.168.0.17:8008/session?33","pingInterval":0} webSocketAddress = jObject.getString("URL"); Log.d(LOG_TAG, "webSocketAddress: " + webSocketAddress); int pingInterval = jObject.optInt("pingInterval"); // TODO } catch (JSONException e) { Log.e(LOG_TAG, "JSON", e); } } } else { Log.i(LOG_TAG, "no post response"); return; } // Make a web socket connection for doing RAMP // to control media playback if (webSocketAddress != null) { // https://github.com/koush/android-websockets List<BasicNameValuePair> extraHeaders = Arrays.asList( new BasicNameValuePair(HEADER_ORIGN, HEADER_ORIGIN_VALUE), new BasicNameValuePair("Pragma", "no-cache"), new BasicNameValuePair("Cache-Control", "no-cache"), new BasicNameValuePair(HEADER_USER_AGENT, HEADER_USER_AGENT_VALUE)); client = new WebSocketClient(URI.create(webSocketAddress), new WebSocketClient.Listener() { // ws://192.168.0.17:8008/session?26 @Override public void onConnect() { Log.d(LOG_TAG, "Websocket Connected!"); // TODO RAMP commands } @Override public void onMessage(String message) { Log.d(LOG_TAG, String.format("Websocket Got string message! %s", message)); } @Override public void onMessage(byte[] data) { Log.d(LOG_TAG, String.format("Websocket Got binary message! %s", data)); } @Override public void onDisconnect(int code, String reason) { Log.d(LOG_TAG, String.format( "Websocket Disconnected! Code: %d Reason: %s", code, reason)); } @Override public void onError(Exception error) { Log.e(LOG_TAG, "Websocket Error!", error); } }, extraHeaders); client.connect(); } else { Log.i(LOG_TAG, "webSocketAddress is null"); } } catch (Exception e) { Log.e(LOG_TAG, "run", e); } } }).start(); } } } } /** * 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; } /** * Parse the App status description XML * * @param context * @param reader */ private void parseXml(Context context, Reader reader) { try { XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); factory.setNamespaceAware(true); XmlPullParser parser = factory.newPullParser(); parser.setInput(reader); int eventType = parser.getEventType(); String lastTagName = null; while (eventType != XmlPullParser.END_DOCUMENT) { switch (eventType) { case XmlPullParser.START_DOCUMENT: break; case XmlPullParser.START_TAG: String tagName = parser.getName(); lastTagName = tagName; break; case XmlPullParser.TEXT: if (lastTagName != null) { if ("connectionSvcURL".equals(lastTagName)) { connectionServiceUrl = parser.getText(); } else if ("state".equals(lastTagName)) { state = parser.getText(); } else if ("protocol".equals(lastTagName)) { protocol = parser.getText(); } } break; case XmlPullParser.END_TAG: tagName = parser.getName(); lastTagName = null; break; } eventType = parser.next(); } } catch (Exception e) { Log.e(LOG_TAG, "parseXml", e); } } /** * @return list of recently connected devices */ public ArrayList<DialServer> getRecentlyConnected() { ArrayList<DialServer> devices = new ArrayList<DialServer>(recentlyConnected.values()); Collections.reverse(devices); return devices; } /* * Menu selection * * @see android.app.Activity#onOptionsItemSelected(android.view.MenuItem) */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_switch: startActivityForResult(ServerFinder.createConnectIntent(this, target, getRecentlyConnected()), CODE_SWITCH_SERVER); return true; default: return super.onOptionsItemSelected(item); } } /** * 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; } } }