at.maui.cheapcast.service.CheapCastService.java Source code

Java tutorial

Introduction

Here is the source code for at.maui.cheapcast.service.CheapCastService.java

Source

/*
 * Copyright 2013 Sebastian Mauer
 *
 * 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 at.maui.cheapcast.service;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import at.maui.cheapcast.Const;
import at.maui.cheapcast.Installation;
import at.maui.cheapcast.R;
import at.maui.cheapcast.Utils;
import at.maui.cheapcast.activity.CastActivity;
import at.maui.cheapcast.activity.PreferenceActivity;
import at.maui.cheapcast.chromecast.*;
import at.maui.cheapcast.chromecast.model.AppRegistration;
import at.maui.cheapcast.ssdp.SSDP;
import com.google.analytics.tracking.android.ExceptionReporter;
import com.google.analytics.tracking.android.GAServiceManager;
import com.google.analytics.tracking.android.GoogleAnalytics;
import com.google.analytics.tracking.android.Tracker;
import com.google.gson.Gson;
import org.eclipse.jetty.server.AbstractHttpConnection;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocketHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.NetworkInterface;
import java.util.HashMap;

public class CheapCastService extends Service {

    public static final String LOG_TAG = "CheapCastService";
    private NotificationManager mNotificationManager;
    private HashMap<String, App> mRegisteredApps;

    private WifiManager mWifiManager;
    private NetworkInterface mNetIf;
    private WifiManager.MulticastLock mMulticastLock;

    private SSDP mSsdp;
    private Server mServer;

    private Gson mGson;
    private SharedPreferences mPreferences;
    private Tracker mGaTracker;
    private GoogleAnalytics mGoogleAnalytics;
    private boolean mRunning = false;
    private ICheapCastCallback mCallback;
    private App mLastApp;

    private final ICheapCastService.Stub mBinder = new ICheapCastService.Stub() {
        @Override
        public void addListener(ICheapCastCallback cb) throws RemoteException {
            mCallback = cb;
        }

        @Override
        public void removeListener() throws RemoteException {
            mCallback = null;
        }
    };

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(LOG_TAG, "onCreate()");

        mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
        mNetIf = Utils.getWifiNetworkInterface(mWifiManager);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            mPreferences = getSharedPreferences("cheapcast", MODE_PRIVATE | MODE_MULTI_PROCESS);
        } else {
            mPreferences = getSharedPreferences("cheapcast", MODE_PRIVATE);
        }
        Log.d(LOG_TAG, String.format("Starting up: friendlyName: %s",
                mPreferences.getString("friendly_name", "CheapCasto")));
        mGson = new Gson();

        mGoogleAnalytics = GoogleAnalytics.getInstance(this);
        mGaTracker = mGoogleAnalytics.getTracker(getString(R.string.ga_trackingId));
        mGoogleAnalytics.setAppOptOut(mPreferences.getBoolean("analytics", false));

        Thread.UncaughtExceptionHandler myHandler = new ExceptionReporter(mGaTracker, // Currently used Tracker.
                GAServiceManager.getInstance(), // GAServiceManager singleton.
                Thread.getDefaultUncaughtExceptionHandler(), this); // Current default uncaught exception handler.

        mGaTracker.sendEvent("CheapCastService", "ServiceStart", null, null);

        mRegisteredApps = new HashMap<String, App>();
        registerApp(new App("ChromeCast", "https://www.gstatic.com/cv/receiver.html?$query"));
        registerApp(new App("YouTube", "https://www.youtube.com/tv?$query"));
        registerApp(new App("PlayMovies", "https://play.google.com/video/avi/eureka?$query",
                new String[] { "play-movies", "ramp" }));
        registerApp(new App("GoogleMusic", "https://play.google.com/music/cast/player"));

        registerApp(new App("GoogleCastSampleApp", "http://anzymrcvr.appspot.com/receiver/anzymrcvr.html"));
        registerApp(new App("GoogleCastPlayer", "https://www.gstatic.com/eureka/html/gcp.html"));
        registerApp(new App("Fling", "$query"));
        registerApp(new App("TicTacToe", "http://www.gstatic.com/eureka/sample/tictactoe/tictactoe.html",
                new String[] { "com.google.chromecast.demo.tictactoe" }));
    }

    private void registerApp(App app) {
        mRegisteredApps.put(app.getName(), app);
        Log.d(LOG_TAG, String.format("Registered app: %s", app.getName()));
    }

    public void renderAppStatus(HttpServletResponse httpServletResponse, App app) throws IOException {

        String appDesc = Const.APP_INFO;
        appDesc = appDesc.replaceAll("#name#", app.getName());
        appDesc = appDesc.replaceAll("#connectionSvcURL#", app.getConnectionSvcURL());
        appDesc = appDesc.replaceAll("#protocols#", app.getProtocols());
        appDesc = appDesc.replaceAll("#state#", app.getState());
        appDesc = appDesc.replaceAll("#link#", app.getLink());

        httpServletResponse.setContentType("application/xml;charset=utf-8");
        httpServletResponse.setHeader("Access-Control-Allow-Method", "GET, POST, DELETE, OPTIONS");
        httpServletResponse.setHeader("Access-Control-Expose-Headers", "Location");
        httpServletResponse.setHeader("Cache-control", "no-cache, must-revalidate, no-store");
        httpServletResponse.setStatus(HttpServletResponse.SC_OK);
        httpServletResponse.getWriter().print(appDesc);
    }

    public App getApp(String appName) {
        return mRegisteredApps.get(appName);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(LOG_TAG, "onStartCommand()");

        Notification n = null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            Notification.Builder mBuilder = new Notification.Builder(this).setSmallIcon(R.drawable.ic_service)
                    .setContentTitle("CheapCast").setContentText("Service enabled.").setOngoing(true)
                    .addAction(R.drawable.ic_reload, getString(R.string.restart_service),
                            PendingIntent.getBroadcast(this, 1, new Intent(Const.ACTION_RESTART),
                                    PendingIntent.FLAG_ONE_SHOT))
                    .addAction(R.drawable.ic_stop, getString(R.string.stop_service), PendingIntent
                            .getBroadcast(this, 2, new Intent(Const.ACTION_STOP), PendingIntent.FLAG_ONE_SHOT));

            Intent i = new Intent(this, PreferenceActivity.class);
            i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);

            PendingIntent pi = PendingIntent.getActivity(this, 0, i, 0);
            mBuilder.setContentIntent(pi);
            n = mBuilder.build();
        } else {
            Intent i = new Intent(this, PreferenceActivity.class);
            i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);

            PendingIntent pi = PendingIntent.getActivity(this, 0, i, 0);

            NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this)
                    .setSmallIcon(R.drawable.ic_service).setContentTitle("CheapCast")
                    .setContentText("Service enabled.").setOngoing(true).setContentIntent(pi);
            n = mBuilder.getNotification();
        }

        startForeground(1337, n);

        if (!mRunning)
            initService();

        return START_STICKY;
    }

    private void initService() {

        if (mWifiManager != null) {
            mMulticastLock = mWifiManager.createMulticastLock("SSDP");
            mMulticastLock.acquire();
        }

        try {
            mServer = new Server(8008);
            mServer.setSendDateHeader(true);
            mServer.setSendServerVersion(false);

            mServer.setHandler(mWsHandler);
            mWsHandler.setHandler(mHttpHandler);
            mServer.start();

            Log.d(LOG_TAG, "Initialized HTTP/WS Server");
        } catch (Exception e) {
            e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
        }

        try {
            mSsdp = new SSDP(this);
            mSsdp.start();
            Log.d(LOG_TAG, "Initialized SSDP/DIAL Discovery");
        } catch (IOException e) {
            Log.e(LOG_TAG, "SSDP Init failed", e);
        }

        mRunning = true;
    }

    @Override
    public void onDestroy() {
        Log.d(LOG_TAG, "onDestroy()");

        mGaTracker.sendEvent("CheapCastService", "ServiceStop", null, null);

        mSsdp.shutdown();

        try {
            mServer.stop();
        } catch (Exception e) {
            e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
        }
        stopForeground(true);
    }

    private WebSocketHandler mWsHandler = new WebSocketHandler() {

        @Override
        public WebSocket doWebSocketConnect(HttpServletRequest httpServletRequest, String protocol) {
            Log.d(LOG_TAG, "WS Requested " + httpServletRequest.getPathInfo());

            if (httpServletRequest.getPathInfo().equals("/system/control")) {
                return new SystemControlSocket(CheapCastService.this);
            } else if (httpServletRequest.getPathInfo().equals("/connection")) {
                return new ConnectionSocket(CheapCastService.this);
            } else if (httpServletRequest.getPathInfo().startsWith("/session/")) {
                String appName = httpServletRequest.getPathInfo().replace("/session/", "");
                return new SessionSocket(CheapCastService.this, getApp(appName));
            } else if (httpServletRequest.getPathInfo().startsWith("/receiver/")) {
                String appName = httpServletRequest.getPathInfo().replace("/receiver/", "");
                return new ReceiverSocket(CheapCastService.this, getApp(appName));
            } else {
                Log.e(LOG_TAG, "WS FAIL");
            }

            return null; //To change body of implemented methods use File | Settings | File Templates.
        }
    };

    private Handler mHttpHandler = new AbstractHandler() {
        @Override
        public void handle(String s, Request request, HttpServletRequest httpServletRequest,
                HttpServletResponse httpServletResponse) throws IOException, ServletException {
            String server = Utils.getLocalV4Address(mNetIf).getHostAddress();
            httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");

            if (httpServletRequest.getPathInfo().startsWith("/ssdp/device-desc.xml")
                    && httpServletRequest.getMethod().equals("GET")) {
                Log.d(LOG_TAG, "GET /ssdp/device-desc.xml" + " from " + httpServletRequest.getRemoteAddr() + ", "
                        + httpServletRequest.getHeader("User-Agent"));

                String deviceDesc = Const.DEVICE_DESC;
                deviceDesc = deviceDesc.replaceAll("#uuid#", Installation.id(CheapCastService.this));
                deviceDesc = deviceDesc.replaceAll("#friendlyname#",
                        mPreferences.getString("friendly_name", getString(R.string.cheapcast) + "_" + Build.MODEL));
                deviceDesc = deviceDesc.replaceAll("#base#", "http://" + server + ":8008");

                httpServletResponse.setHeader("Access-Control-Allow-Method", "GET, POST, DELETE, OPTIONS");
                httpServletResponse.setHeader("Access-Control-Expose-Headers", "Location");

                httpServletResponse.setContentType("application/xml");
                httpServletResponse.setStatus(HttpServletResponse.SC_OK);
                httpServletResponse.addHeader("Application-URL", "http://" + server + ":8008/apps");
                httpServletResponse.getWriter().print(deviceDesc);
            } else if (httpServletRequest.getPathInfo().equals("/apps")
                    && httpServletRequest.getMethod().equals("GET")) {

                App activeApp = null;
                for (App app : mRegisteredApps.values()) {
                    if (app.getState().equals("running")) {
                        activeApp = app;
                        break;
                    }
                }

                if (activeApp != null) {
                    Log.d(LOG_TAG, String.format("GET /apps: Redirecting to %s", activeApp.getName()));
                    httpServletResponse.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
                    httpServletResponse.addHeader("Location",
                            String.format("http://%s:8008/apps/%s", server, activeApp.getName()));
                } else {
                    Log.d(LOG_TAG, String.format("GET /apps: SC_NO_CONTENT at /apps"));
                    httpServletResponse.setStatus(HttpServletResponse.SC_NO_CONTENT);
                    httpServletResponse.setContentType("application/xml;charset=utf-8");
                    httpServletResponse.setHeader("Access-Control-Allow-Method", "GET, POST, DELETE, OPTIONS");
                    httpServletResponse.setHeader("Access-Control-Expose-Headers", "Location");
                }
            } else if (httpServletRequest.getPathInfo().startsWith("/apps/")
                    && httpServletRequest.getMethod().equals("GET")) {
                String appName = httpServletRequest.getPathInfo().replace("/apps/", "");
                Log.d(LOG_TAG, String.format("GET /apps/%s", appName));
                App app = mRegisteredApps.get(appName);

                renderAppStatus(httpServletResponse, app);
            } else if (httpServletRequest.getPathInfo().startsWith("/apps/")
                    && httpServletRequest.getMethod().equals("DELETE")) {
                String appName = httpServletRequest.getPathInfo().replace("/apps/", "").replace("/web-1", "");
                App app = mRegisteredApps.get(appName);

                if (app != null) {
                    app.stop();

                    mGaTracker.sendEvent("CheapCastService", "AppStop", appName, null);

                    if (mCallback != null && app.getReceivers().size() == 0)
                        try {
                            mCallback.onAppStopped(appName);
                        } catch (RemoteException e) {
                            e.printStackTrace();
                        }

                    renderAppStatus(httpServletResponse, app);
                }
            } else if (httpServletRequest.getPathInfo().startsWith("/apps/")
                    && httpServletRequest.getMethod().equals("POST")) {
                String appName = httpServletRequest.getPathInfo().replace("/apps/", "");

                Log.d(LOG_TAG, String.format("POST /apps/%s", appName));
                App app = mRegisteredApps.get(appName);

                if (app != null) {

                    // if(mLastApp == null || !mLastApp.equals(app) || !app.getState().equals("running")) {
                    mGaTracker.sendEvent("CheapCastService", "AppStart", appName, null);
                    app.setLink("<link rel='run' href='web-1'/>");
                    app.setConnectionSvcURL(String.format("http://%s:8008/connection/%s", server, appName));
                    app.addProtocol("ramp");
                    app.setState("running");

                    String params = Utils.readerToString(httpServletRequest.getReader());

                    Log.d(LOG_TAG, "Addtl. App params: " + params);
                    String appUrl = app.getReceiverUrl().replace("$query", params);
                    //Intent i = new Intent(Intent.ACTION_VIEW, null);
                    //i.setComponent(ComponentName.unflattenFromString("com.android.chrome/com.android.chrome.Main"));
                    //i.addCategory("android.intent.category.LAUNCHER");
                    Intent i = new Intent(CheapCastService.this, CastActivity.class);
                    i.setData(Uri.parse(appUrl));
                    i.putExtra(Const.APP_EXTRA, app.getName());
                    i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    //if(mCallback == null) {
                    startActivity(i);
                    //}

                    mLastApp = app;

                    httpServletResponse.setContentType("text/html; charset=utf-8");
                    httpServletResponse.setHeader("Location",
                            String.format("http://%s:8008/apps/%s/web-1", server, appName));
                    httpServletResponse.setStatus(HttpServletResponse.SC_CREATED);
                    /* } else {
                    Log.d(LOG_TAG, String.format("App %s already started.", app.getName()));
                        
                    httpServletResponse.setContentType("text/html; charset=utf-8");
                    httpServletResponse.setHeader("Location", String.format("http://%s:8008/apps/%s/web-2", server, appName));
                    httpServletResponse.setStatus(HttpServletResponse.SC_CREATED);
                     }*/

                }
            } else if (httpServletRequest.getPathInfo().startsWith("/connection/")
                    && httpServletRequest.getMethod().equals("POST")) {
                String appName = httpServletRequest.getPathInfo().replace("/connection/", "");
                Log.d(LOG_TAG, String.format("POST /connection/%s", appName));
                App app = mRegisteredApps.get(appName);

                if (app != null) {
                    httpServletResponse.setHeader("Access-Control-Allow-Method", "POST, OPTIONS");
                    httpServletResponse.setHeader("Access-Control-Allow-Headers", "Content-Type");
                    httpServletResponse.setContentType("application/json; charset=utf-8");
                    httpServletResponse.setStatus(HttpServletResponse.SC_OK);
                    httpServletResponse.getWriter()
                            .print(String.format("{\"URL\":\"ws://%s:8008/session/%s?%d\",\"pingInterval\":3}",
                                    server, appName, app.getRemotes().size()));
                }
            } else if (httpServletRequest.getPathInfo().equals("/registerApp")
                    && httpServletRequest.getMethod().equals("POST")) {

                Log.d(LOG_TAG, "POST /registerApp/");

                if (mPreferences.getBoolean("allow_custom_apps", false)) {
                    String rawBody = Utils.readerToString(httpServletRequest.getReader());
                    AppRegistration reg = mGson.fromJson(rawBody, AppRegistration.class);

                    if (reg != null) {
                        mGaTracker.sendEvent("CheapCastService", "RegisterApp", reg.getAppName(), null);
                        registerApp(new App(reg.getAppName(), reg.getAppUrl(), reg.getProtocols()));
                        httpServletResponse.setStatus(HttpServletResponse.SC_OK);
                        httpServletResponse.setContentType("application/json; charset=utf-8");
                        httpServletResponse.getWriter().print("{\"msg\":\"OK\"}");
                    } else {
                        httpServletResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                    }
                } else {
                    httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
                }

            } else {
                Log.d(LOG_TAG, "Requested " + httpServletRequest.getPathInfo());
                Log.d(LOG_TAG, "The princess is in another castle");
                httpServletResponse.setContentType("text/html");
                httpServletResponse.setStatus(HttpServletResponse.SC_OK);
                httpServletResponse.getWriter().println("<h1>This is CheapCast :D</h1>");

                if (mPreferences.getBoolean("allow_custom_apps", false)) {
                    httpServletResponse.getWriter().println("<h3>Registered Apps:</h3>");
                    httpServletResponse.getWriter().print("<ul>");
                    for (App app : mRegisteredApps.values()) {
                        httpServletResponse.getWriter().println(String.format("<li>%s - %s, Protocols: %s</li>",
                                app.getName(), app.getReceiverUrl(), app.getProtocolList()));
                    }
                    httpServletResponse.getWriter().print("</ul>");
                }
            }

            AbstractHttpConnection connection = AbstractHttpConnection.getCurrentConnection();
            String ct = connection.getResponseFields().getStringField("Content-Type");
            if (ct.contains(";")) {
                AbstractHttpConnection.getCurrentConnection().getResponseFields().put("Content-Type",
                        ct.split(";")[0]);
            }

            ((Request) httpServletRequest).setHandled(true);
        }
    };

}