com.sesamtv.cordova.chromecast.ChromeCast.java Source code

Java tutorial

Introduction

Here is the source code for com.sesamtv.cordova.chromecast.ChromeCast.java

Source

/**
 *  ChromeCast cordova plugin (Android)
 *
 *  @author Hongbo LU
 *  @see http://sesamtv.com
 *  @license MIT <http://szanata.com/MIT.txt>
 *  @license GNU <http://szanata.com/GNU.txt>
 *
 *  chromecast plugin for cordova
 *
 */
package com.sesamtv.cordova.chromecast;

import java.io.IOException;
import java.util.List;
import java.util.HashMap;

import android.support.v7.app.MediaRouteActionProvider;
import com.google.android.gms.cast.*;
import com.google.android.gms.common.images.WebImage;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.os.Bundle;
import android.support.v7.media.MediaRouteSelector;
import android.support.v7.media.MediaRouter;
import android.support.v7.media.MediaRouter.RouteInfo;
import android.util.Log;
import android.net.Uri;
import android.widget.Toast;

import com.google.android.gms.cast.Cast.ApplicationConnectionResult;
import com.google.android.gms.cast.Cast.MessageReceivedCallback;
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 java.util.ArrayList;
import java.util.Map;

public class ChromeCast extends CordovaPlugin {
    //private final String APP_ID = "31a76198-2182-481a-a4a6-27d351872026";
    private String APP_ID = null;
    private static Boolean DEBUG_MODE = true;
    private static final String MSG_PREFIX = "urn:x-cast:";
    private String PACKAGE_NAME = "com.sesamtv.chromecast";
    private static final String TAG = "ChromeCastPlugin";

    private MediaRouter mMediaRouter;
    private MediaRouteSelector mMediaRouteSelector;
    private MediaRouter.Callback mMediaRouterCallback;
    private CastDevice mSelectedDevice;
    private GoogleApiClient mApiClient;
    private Cast.Listener mCastListener;
    private ConnectionCallbacks mConnectionCallbacks;
    private ConnectionFailedListener mConnectionFailedListener;
    private boolean mApplicationStarted;
    private boolean mWaitingForReconnect;
    private com.sesamtv.cordova.chromecast.MediaPlayer mMediaPlayer;
    private HashMap<String, CustomChannel> mChannels = new HashMap<String, CustomChannel>();
    private HashMap<String, CallbackContext> mChannelsQueue = new HashMap<String, CallbackContext>();
    private CallbackContext receiverCallback;
    private CallbackContext mediaStatusCallback;
    private CallbackContext setReceiverCallback;
    private CallbackContext onEndedCallback;
    private CallbackContext onSessionCreatedCallback;

    private enum Actions {
        startReceiverListener, onMessage, sendMessage, setReceiver, loadMedia, stopMedia, pauseMedia, playMedia, seekMedia, seekMediaBy, setDeviceVolume, setDeviceVolumeBy, setMuted, toggleMuted, getMediaStatus, stopApplication, startStatusListener, startOnEndedListener, startSessionListener
    }

    public String getRawNSName(String ns) {
        return MSG_PREFIX + PACKAGE_NAME + "." + ns;
    }

    public String getNSName(String rawNs) {
        return rawNs.replace(MSG_PREFIX + PACKAGE_NAME + ".", "");
    }
    /*public ChromecastPlugin() {
        
    }*/

    public void initialize(final CordovaInterface cordova, CordovaWebView webView) {
        super.initialize(cordova, webView);

        // Configure Cast device discovery

    }

    private void initRouters(CallbackContext callbackContext) {
        Log.d(TAG, "init routers...");

        mMediaRouter = MediaRouter.getInstance(cordova.getActivity().getApplicationContext());
        mMediaRouteSelector = new MediaRouteSelector.Builder()
                .addControlCategory(CastMediaControlIntent.categoryForCast(APP_ID)).build();
        mMediaRouterCallback = new MyMediaRouterCallback();

        mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback,
                MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);

        if (callbackContext != null) {
            callbackContext.success();
        }
    }

    @Override
    public boolean execute(final String action, final JSONArray args, final CallbackContext callbackContext)
            throws JSONException {
        Log.d(TAG, "APP ID: " + APP_ID);
        Log.d(TAG, "execute action : " + action);

        if (action.equals("setAppId")) {
            APP_ID = args.getString(0);
            if (args.length() > 1) {
                PACKAGE_NAME = args.getString(1);
            }
            cordova.getActivity().runOnUiThread(new Runnable() {
                public void run() {
                    try {
                        initRouters(callbackContext);
                    } catch (Exception e) {
                        callbackContext.error(e.getMessage());
                    }
                }
            });
        } else if (APP_ID == null) {
            Log.w(TAG, "app id not found");
            callbackContext.error("APP_ID_NOT_FOUND");
        } else {
            if (action.equals("startListener")) {
                this.startListener(args.getString(0), callbackContext);
            } else if (action.equals("onMessage")) {
                this.onCastMessage(args.getString(0), callbackContext);
            } else if (action.equals("setReceiver")) {
                this.setReceiverAction(args, callbackContext);
            } else {
                executeActions(action, args, callbackContext);
            }

        }
        return true;
    }

    private void startListener(String type, CallbackContext callbackContext) {
        Log.d(TAG, "startListener for " + type);
        PluginResult pluginResult = new PluginResult(PluginResult.Status.NO_RESULT);
        if (type.equals("session")) {
            onSessionCreatedCallback = callbackContext;
            pluginResult.setKeepCallback(true);
            callbackContext.sendPluginResult(pluginResult);
        } else if (type.equals("receivers")) {
            receiverCallback = callbackContext;
            onReceiverListChanged();
        } else if (type.equals("sessionEnded")) {
            onEndedCallback = callbackContext;
            pluginResult.setKeepCallback(true);
            callbackContext.sendPluginResult(pluginResult);
        } else if (type.equals("status")) {
            mediaStatusCallback = callbackContext;
            mediaStatusCallback.sendPluginResult(getMediaStatus());
        } else {
            callbackContext.error("LISTENER_TYPE_NOT_FOUND");
        }
    }

    private boolean executeActions(final String action, final JSONArray args, final CallbackContext callbackContext)
            throws JSONException {
        switch (Actions.valueOf(action)) {
        case sendMessage:
            cordova.getThreadPool().execute(new Runnable() {
                public void run() {
                    try {
                        String channelName = args.getString(0);
                        String msg = args.getString(1);
                        sendMessage(channelName, msg, callbackContext);
                    } catch (Exception e) {
                        callbackContext.error(e.getMessage());
                    }
                }
            });
            break;

        case loadMedia:
            loadMediaAction(args.getJSONObject(0), callbackContext);
            break;
        case pauseMedia:
            pauseMediaAction(callbackContext);
            break;
        case stopMedia:
            stopMediaAction(callbackContext, args);
            break;
        case playMedia:
            playMediaAction(callbackContext);
            break;
        case seekMediaBy:
            try {
                seekMediaByAction(args.getLong(0));
                callbackContext.success();
            } catch (IOException e) {
                callbackContext.error(e.getMessage());
            }
            break;
        case seekMedia:
            try {
                seekMediaAction(args.getLong(0));
                callbackContext.success();
            } catch (IOException e) {
                callbackContext.error(e.getMessage());
            }
            break;
        case setDeviceVolume:
            final double vol = args.getDouble(0);
            Log.d(TAG, "setVolume " + vol);
            cordova.getThreadPool().execute(new Runnable() {
                public void run() {
                    setVolumeAction(vol, callbackContext);
                }
            });
            break;
        case setDeviceVolumeBy:
            cordova.getThreadPool().execute(new Runnable() {
                public void run() {
                    try {
                        setVolumeByAction(args.getDouble(0), callbackContext);
                    } catch (JSONException e) {
                        callbackContext.error(e.getMessage());
                    }
                }
            });
            break;
        case setMuted:
            final boolean muted = args.getBoolean(0);
            Log.d(TAG, "setMuted " + muted);
            cordova.getThreadPool().execute(new Runnable() {
                public void run() {
                    setDeviceMutedAction(muted, callbackContext);
                }
            });
            break;
        case toggleMuted:
            Log.d(TAG, "toggleMuted ");
            cordova.getThreadPool().execute(new Runnable() {
                public void run() {
                    toggleDeviceMutedAction(callbackContext);
                }
            });
            break;
        case getMediaStatus:
            Log.d(TAG, "getMediaStatus");
            callbackContext.sendPluginResult(getMediaStatus());
            break;
        case stopApplication:
            try {
                Log.d(TAG, "stopCast");
                stopApplication();
                callbackContext.success();
            } catch (Exception e) {
                callbackContext.error("stop cast failed :" + e.getMessage());
                return false;
            }
            break;

        default:
            callbackContext.error("Invalid action: " + action);

        }
        return true;
    }

    private void setReceiverAction(JSONArray args, final CallbackContext callbackContext) throws JSONException {

        final int index = args.getInt(0);
        setReceiverCallback = callbackContext;

        cordova.getActivity().runOnUiThread(new Runnable() {
            public void run() {
                final RouteInfo route = mMediaRouter.getRoutes().get(index);

                Log.d(TAG, "route :" + index + " " + route.getId() + " selected " + route.isSelected()
                        + " playback type: " + route.getPlaybackType());

                try {
                    if (route.isSelected()) {
                        Log.d(TAG, "found selected route");
                        //mMediaRouter.selectRoute(mMediaRouter.getDefaultRoute());
                        //mMediaRouter.selectRoute(route);

                        mSelectedDevice = CastDevice.getFromBundle(route.getExtras());
                        onReceiverListChanged();
                        launchReceiver();

                    } else {
                        mMediaRouter.selectRoute(route);
                    }
                } catch (Exception e) {
                    callbackContext.error(e.getMessage());
                }
            }
        });

    }

    private void loadMediaAction(final JSONObject opt, final CallbackContext callbackContext) throws JSONException {
        try {
            Log.d(TAG, "load Media...");
            cordova.getThreadPool().execute(new Runnable() {
                public void run() {
                    try {
                        mMediaPlayer.loadMedia(opt, callbackContext);
                    } catch (Exception e) {
                        callbackContext.error(e.getMessage());
                    }
                }
            });
        } catch (Exception e) {
            callbackContext.error("cast failed :" + e.getMessage());
        }
    }

    private void pauseMediaAction(final CallbackContext callbackContext) {
        cordova.getThreadPool().execute(new Runnable() {
            public void run() {
                try {
                    mMediaPlayer.pauseMedia(callbackContext);
                } catch (Exception e) {
                    callbackContext.error(e.getMessage());
                }
            }
        });
    }

    private void stopMediaAction(final CallbackContext callbackContext, final JSONArray args) {

        cordova.getThreadPool().execute(new Runnable() {
            public void run() {
                JSONObject customData = null;
                try {
                    if (args.length() > 0) {
                        customData = args.getJSONObject(0);
                    }
                } catch (JSONException e) {
                    callbackContext.error(e.getMessage());
                }
                mMediaPlayer.stopMedia(callbackContext, customData);
            }
        });
    }

    private void playMediaAction(final CallbackContext callbackContext) throws JSONException {
        Log.d(TAG, "play ");
        cordova.getThreadPool().execute(new Runnable() {
            public void run() {
                mMediaPlayer.playMedia(callbackContext);
            }
        });
    }

    private void seekMediaByAction(final long value) throws IOException {
        Log.d(TAG, "seekBy " + value);
        if (ensurePlayer()) {
            cordova.getThreadPool().execute(new Runnable() {
                public void run() {
                    try {
                        mMediaPlayer.seekMediaBy(value);
                    } catch (Exception e) {
                        Log.d(TAG, "seekBy Error " + e.getMessage());
                    }
                }
            });
        }
    }

    private void seekMediaAction(final long value) throws IOException {
        Log.d(TAG, "seekmedia " + value);
        if (ensurePlayer()) {
            cordova.getThreadPool().execute(new Runnable() {
                public void run() {
                    try {
                        mMediaPlayer.seekMedia(value);
                    } catch (Exception e) {
                        Log.d(TAG, "seekBy Error " + e.getMessage());
                    }
                }
            });
        }
    }

    private void setVolumeAction(double vol, CallbackContext callbackContext) {
        if (ensurePlayer(callbackContext)) {
            mMediaPlayer.setDeciveVolume(vol);
            callbackContext.success();
        }
    }

    private void setVolumeByAction(double value, CallbackContext callbackContext) {
        if (ensurePlayer(callbackContext)) {
            mMediaPlayer.setDeviceVolumeBy(value);
            callbackContext.success();
        }
    }

    private void setDeviceMutedAction(boolean muted, CallbackContext callbackContext) {
        if (ensurePlayer(callbackContext)) {
            mMediaPlayer.setDeviceMuted(muted);
            callbackContext.success();
        }
    }

    private void toggleDeviceMutedAction(CallbackContext callbackContext) {
        if (ensurePlayer(callbackContext)) {
            mMediaPlayer.toggleDeviceMuted();
            callbackContext.success();
        }
    }

    private boolean ensurePlayer(CallbackContext... params) {
        int l = params.length;
        CallbackContext callbackContext = null;
        if (l == 1) {
            callbackContext = params[0];
        }
        boolean flag = true;
        String reason = null;
        if (mMediaPlayer == null) {
            reason = "MEDIAPLAYER_NOT_FOUND";
            flag = false;
        }
        if (!flag && callbackContext != null) {
            callbackContext.error(reason);
        }
        return flag;
    }

    private void onCastMessage(final String channelName, final CallbackContext callbackContext) {
        if (mApplicationStarted) {
            mChannels.put(channelName, new CustomChannel(channelName, callbackContext));
        } else {
            mChannelsQueue.put(channelName, callbackContext);
            Log.w(TAG, "add cast message failed: application is not yet ready, add to queue");
        }
    }

    private void clearChannelsQueue() {
        Log.d(TAG, "clear channels queue");
        if (mChannelsQueue.size() > 0) {
            for (Map.Entry<String, CallbackContext> entry : mChannelsQueue.entrySet()) {
                String key = entry.getKey();
                CallbackContext value = entry.getValue();

                this.onCastMessage(key, value);
            }
            mChannelsQueue.clear();
        }

    }

    private void addChannels() {
        if (hasValidConnection()) {

            for (Map.Entry<String, CustomChannel> entry : mChannels.entrySet()) {
                //String key = entry.getKey();
                CustomChannel value = entry.getValue();
                try {
                    Cast.CastApi.setMessageReceivedCallbacks(mApiClient, value.getNamespace(), value);
                } catch (IOException e) {
                    Log.e(TAG, e.getMessage());
                }
            }
        }
    }

    private void removeChannels() {
        if (hasValidConnection()) {
            for (Map.Entry<String, CustomChannel> entry : mChannels.entrySet()) {
                CustomChannel value = entry.getValue();
                try {
                    Cast.CastApi.removeMessageReceivedCallbacks(mApiClient, value.getNamespace());
                } catch (IOException e) {
                    Log.e(TAG, e.getMessage());
                }
            }
            mChannels.clear();
        }
    }

    @Override
    public void onResume(boolean multitasking) {
        super.onResume(multitasking);
        Log.d(TAG, "resuming a session");
        // Start media router discovery
        mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback,
                MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);

    }

    @Override
    public void onPause(boolean multitasking) {
        Log.d(TAG, "on pause");
        if (cordova.getActivity().isFinishing()) {
            // End media router discovery
            mMediaRouter.removeCallback(mMediaRouterCallback);
        }

        super.onPause(multitasking);
    }

    /**
     * when webview page reloaded
     */
    @Override
    public void onReset() {
        Log.d(TAG, "onreset");
        teardown();
        super.onReset();
    }

    @Override
    public void onDestroy() {
        Log.d(TAG, "on destroy");
        teardown();
        super.onDestroy();
    }

    public void onMediaStatusCallback(JSONObject status) {
        if (mediaStatusCallback != null) {
            PluginResult result = new PluginResult(PluginResult.Status.OK, status);
            result.setKeepCallback(true);
            mediaStatusCallback.sendPluginResult(result);
        }
    }

    private void onReceiverListChanged() {
        cordova.getActivity().runOnUiThread(new Runnable() {
            public void run() {
                Log.d(TAG, "onReceiverListChanged " + (receiverCallback != null));
                if (receiverCallback != null) {
                    JSONArray jsonRoute = getRoutes();
                    PluginResult result = new PluginResult(PluginResult.Status.OK, jsonRoute);
                    result.setKeepCallback(true);
                    Log.d(TAG, "onReceiverListChanged " + jsonRoute.toString());
                    receiverCallback.sendPluginResult(result);
                }
            }
        });

    }

    private void onSessionStarted(ApplicationConnectionResult result) {
        if (setReceiverCallback != null) {
            setReceiverCallback.success();
            setReceiverCallback = null;
        }
        if (onSessionCreatedCallback != null) {
            try {
                JSONObject info = new JSONObject();
                info.put("appId", APP_ID);
                info.put("sessionId", result.getSessionId());
                info.put("displayName", result.getApplicationMetadata().getName());
                info.put("statusCode", result.getStatus().getStatusCode());
                info.put("wasLaunched", result.getWasLaunched());

                ArrayList<JSONObject> media = new ArrayList<JSONObject>();
                if (mMediaPlayer != null) {
                    JSONObject mediaStatus = mMediaPlayer.getMediaStatus();
                    if (mediaStatus.has("contentId")) {
                        media.add(mediaStatus);
                    }
                }
                info.put("media", media);
                PluginResult res = new PluginResult(PluginResult.Status.OK, info);
                res.setKeepCallback(true);
                onSessionCreatedCallback.sendPluginResult(res);
            } catch (JSONException e) {
                Log.e(TAG, e.getMessage());
            }
        }
    }

    private void onSessionEnded(JSONObject info) {
        if (info == null) {
            info = new JSONObject();
        }
        if (onEndedCallback != null) {
            PluginResult result = new PluginResult(PluginResult.Status.OK, info);
            result.setKeepCallback(true);
            onEndedCallback.sendPluginResult(result);
        }
    }

    private JSONArray getRoutes() {
        JSONArray routeList = new JSONArray();
        List<RouteInfo> routesInMedia = mMediaRouter.getRoutes();
        final int l = routesInMedia.size();
        if (l == 0) {
            return routeList;
        }
        try {
            for (int i = 0; i < l; i++) {
                RouteInfo rInfo = routesInMedia.get(i);
                CastDevice dInfo = getDevice(rInfo);
                JSONObject jsonRoute = new JSONObject();
                jsonRoute.put("id", rInfo.getId());
                jsonRoute.put("name", rInfo.getName());
                jsonRoute.put("description", rInfo.getDescription());
                jsonRoute.put("isSelected", rInfo.isSelected());
                jsonRoute.put("index", i);
                jsonRoute.put("volume", rInfo.getVolume());
                if (dInfo != null) {
                    jsonRoute.put("ipAddress", dInfo.getIpAddress());
                    List<WebImage> icons = dInfo.getIcons();
                    if ((icons != null) && !icons.isEmpty()) {
                        WebImage icon = icons.get(0);
                        jsonRoute.put("icon", icon.getUrl());
                    }
                }

                if (rInfo.isSelected()) {
                    jsonRoute.put("isConnected", mSelectedDevice != null);
                }
                /*if (mSelectedDevice != null) {
                jsonRoute.put("ipAddress", mSelectedDevice.getIpAddress());
                }*/
                routeList.put(jsonRoute);
            }
        } catch (JSONException e) {
            Log.d(TAG, "failed to get routes list");
        }
        return routeList;
    }

    public PluginResult getMediaStatus() {
        JSONObject status = new JSONObject();
        try {
            if (mMediaPlayer != null) {
                status = mMediaPlayer.getMediaStatus();
            } else {
                status.put("state", "");
                status.put("position", 0.0);
                status.put("duration", 0.0);
            }

        } catch (JSONException e) {
            e.printStackTrace();
        }
        PluginResult result = new PluginResult(PluginResult.Status.OK, status);
        result.setKeepCallback(true);
        return result;
    }

    /*@Override
    public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.main, menu);
    MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item);
    MediaRouteActionProvider mediaRouteActionProvider = (MediaRouteActionProvider) MenuItemCompat
            .getActionProvider(mediaRouteMenuItem);
    // Set the MediaRouteActionProvider selector for device discovery.
    mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector);
    return true;
    }*/

    /**
     * Callback for MediaRouter events
     */
    private class MyMediaRouterCallback extends MediaRouter.Callback {
        @Override
        public void onRouteAdded(MediaRouter router, RouteInfo route) {
            super.onRouteAdded(router, route);
            Log.d(TAG, "on route added");
            Log.d(TAG, "route added :" + route.getId() + ":" + route.getName() + ":" + route.getDescription());
            onReceiverListChanged();
        }

        @Override
        public void onRouteRemoved(MediaRouter router, RouteInfo route) {
            super.onRouteRemoved(router, route);
            Log.d(TAG, "on route removed");
            onReceiverListChanged();
        }

        @Override
        public void onRouteSelected(MediaRouter router, RouteInfo info) {
            Log.d(TAG, "onRouteSelected");
            // Handle the user route selection.
            mSelectedDevice = getDevice(info);
            onReceiverListChanged();
            launchReceiver();
        }

        @Override
        public void onRouteUnselected(MediaRouter router, RouteInfo info) {
            Log.d(TAG, "onRouteUnselected: info=" + info);
            onReceiverListChanged();
            teardown();
            mSelectedDevice = null;
        }

    }

    private CastDevice getDevice(RouteInfo info) {
        return CastDevice.getFromBundle(info.getExtras());
    }

    private boolean hasValidConnection() {
        return mApiClient != null && mApiClient.isConnected();
    }

    /**
     * Start the receiver app
     */
    private void launchReceiver() {
        try {
            mCastListener = new Cast.Listener() {
                @Override
                public void onApplicationStatusChanged() {
                    if (hasValidConnection()) {
                        Log.d(TAG, "onApplicationStatusChanged: " + Cast.CastApi.getApplicationStatus(mApiClient));
                    }
                }

                @Override
                public void onVolumeChanged() {
                    if (hasValidConnection()) {
                        Log.d(TAG, "onVolumeChanged: " + Cast.CastApi.getVolume(mApiClient));
                    }
                }

                @Override
                public void onApplicationDisconnected(int errorCode) {
                    Log.d(TAG, "application has stopped");
                    teardown();
                }

            };
            // Connect to Google Play services
            mConnectionCallbacks = new ConnectionCallbacks();
            mConnectionFailedListener = new ConnectionFailedListener();
            Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(mSelectedDevice, mCastListener);
            if (DEBUG_MODE) {
                apiOptionsBuilder.setDebuggingEnabled();
            }
            mApiClient = new GoogleApiClient.Builder(cordova.getActivity().getApplicationContext())
                    .addApi(Cast.API, apiOptionsBuilder.build()).addConnectionCallbacks(mConnectionCallbacks)
                    .addOnConnectionFailedListener(mConnectionFailedListener).build();

            mApiClient.connect();

        } catch (Exception e) {
            Log.e(TAG, "Failed launchReceiver", e);
        }
    }

    private void launchApplication() {
        if (mApiClient == null) {
            return;
        }
        Log.d(TAG, "launch application");
        Cast.CastApi.launchApplication(mApiClient, APP_ID, false)
                .setResultCallback(new ApplicationConnectionResultCallback("LaunchApp"));
    }

    private void joinApplication() {

        if (mApiClient == null) {
            return;
        }
        Log.d(TAG, "join application");
        Cast.CastApi.joinApplication(mApiClient, APP_ID)
                .setResultCallback(new ApplicationConnectionResultCallback("JoinApplication"));
    }

    private void leaveApplication() {
        if (hasValidConnection()) {
            return;
        }
        Log.d(TAG, "leave application");
        Cast.CastApi.leaveApplication(mApiClient).setResultCallback(new ResultCallback<Status>() {
            @Override
            public void onResult(Status result) {
                if (result.isSuccess()) {
                    mMediaPlayer.detachMediaPlayer();
                    onSessionEnded(null);
                } else {
                    Log.w(TAG, "leave application failed");
                }
            }
        });
    }

    private void stopApplication() {
        if (mApiClient == null) {
            return;
        }
        Log.d(TAG, "stop application");
        Cast.CastApi.stopApplication(mApiClient).setResultCallback(new ResultCallback<Status>() {
            @Override
            public void onResult(Status result) {
                if (result.isSuccess()) {
                    mMediaPlayer.detachMediaPlayer();
                    onSessionEnded(null);
                } else {
                    Log.w(TAG, "stop application failed");
                }
            }
        });
    }

    /**
     * Google Play services callbacks
     */
    private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks {

        @Override
        public void onConnected(Bundle connectionHint) {
            Log.d(TAG, "onConnected");

            if (mApiClient == null) {
                // We got disconnected while this runnable was pending
                // execution.
                return;
            }

            try {
                if (mWaitingForReconnect) {
                    mWaitingForReconnect = false;

                    // Check if the receiver app is still running
                    if ((connectionHint != null) && connectionHint.getBoolean(Cast.EXTRA_APP_NO_LONGER_RUNNING)) {
                        Log.d(TAG, "App  is no longer running");
                        teardown();
                    } else {
                        // Re-create the custom message channel
                        addChannels();
                        joinApplication();
                        if (mMediaPlayer != null) {
                            mMediaPlayer.reattachMediaPlayer();
                        }
                    }
                } else {
                    // Launch the receiver app
                    //if (mMediaRouter.getDefaultRoute() != mMediaRouter.getSelectedRoute()) {
                    launchApplication();
                    //}

                }
            } catch (Exception e) {
                Log.e(TAG, "Failed to launch application", e);
            }
        }

        @Override
        public void onConnectionSuspended(int cause) {
            Log.d(TAG, "onConnectionSuspended");
            mWaitingForReconnect = true;
            if (mMediaPlayer != null) {
                mMediaPlayer.detachMediaPlayer();
            }
        }
    }

    /**
     * Google Play services callbacks
     */
    private class ConnectionFailedListener implements GoogleApiClient.OnConnectionFailedListener {

        @Override
        public void onConnectionFailed(ConnectionResult result) {
            Log.e(TAG, "onConnectionFailed ");
            teardown();
        }
    }

    /**
     * Tear down the connection to the receiver
     */
    private void teardown() {
        onSessionEnded(null);

        if (mMediaPlayer != null) {
            mMediaPlayer.detachMediaPlayer();
            mMediaPlayer = null;
        }
        if (mApiClient != null) {
            if (mApplicationStarted) {
                Cast.CastApi.stopApplication(mApiClient);
                removeChannels();
                mApplicationStarted = false;
            }
            if (mApiClient.isConnected()) {
                mApiClient.disconnect();
            }
            mApiClient = null;
        }
        mSelectedDevice = null;
        mWaitingForReconnect = false;
    }

    /**
     * Send a message to the receiver
     */
    private void sendMessage(String channelName, String message, final CallbackContext callbackContext) {
        if (hasValidConnection() && mChannels.containsKey(channelName)) {
            try {
                Cast.CastApi.sendMessage(mApiClient, mChannels.get(channelName).getNamespace(), message)
                        .setResultCallback(new ResultCallback<Status>() {
                            @Override
                            public void onResult(Status result) {
                                if (!result.isSuccess()) {
                                    Log.e(TAG, "Sending message failed");
                                    callbackContext.error("Sending message failed");
                                } else {
                                    callbackContext.success();
                                }
                            }
                        });
            } catch (Exception e) {
                Log.e(TAG, "Exception while sending message", e);
                callbackContext.error("Exception while sending message");
            }
        } else {
            Toast.makeText(cordova.getActivity(), message, Toast.LENGTH_SHORT).show();
        }
    }

    /**
     * Custom message channel
     */
    class CustomChannel implements MessageReceivedCallback {
        private String CHANNEL_NAME;
        private CallbackContext callbackContext;

        public CustomChannel(String ns, CallbackContext callback) {
            CHANNEL_NAME = ns;
            callbackContext = callback;
            PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
            result.setKeepCallback(true);
            callbackContext.sendPluginResult(result);
            try {
                Cast.CastApi.setMessageReceivedCallbacks(mApiClient, this.getNamespace(), this);
            } catch (IOException e) {
                Log.e(TAG, "Exception while creating channel", e);
            }
        }

        public void destroy() {
            callbackContext = null;
            try {
                Cast.CastApi.removeMessageReceivedCallbacks(mApiClient, this.getNamespace());
            } catch (IOException e) {
                Log.e(TAG, e.getMessage());
            }
        }

        /**
         * @return custom namespace
         */
        public String getNamespace() {
            return getRawNSName(CHANNEL_NAME);
        }

        /*
         * Receive message from the receiver app
         */
        @Override
        public void onMessageReceived(CastDevice castDevice, String namespace, String message) {
            Log.d(TAG, "onMessageReceived: " + message);

            PluginResult result = new PluginResult(PluginResult.Status.OK, message);
            result.setKeepCallback(true);
            callbackContext.sendPluginResult(result);

        }

    }

    private final class ApplicationConnectionResultCallback
            implements ResultCallback<Cast.ApplicationConnectionResult> {
        private final String mClassTag;

        public ApplicationConnectionResultCallback(String suffix) {
            mClassTag = TAG + "_" + suffix;
        }

        @Override
        public void onResult(ApplicationConnectionResult result) {
            Status status = result.getStatus();
            Log.d(mClassTag, "ApplicationConnectionResultCallback.onResult: statusCode" + status.getStatusCode());
            if (status.isSuccess()) {
                ApplicationMetadata applicationMetadata = result.getApplicationMetadata();
                String sessionId = result.getSessionId();
                String applicationStatus = result.getApplicationStatus();
                boolean wasLaunched = result.getWasLaunched();
                Log.d(mClassTag, "application name: " + applicationMetadata.getName() + ", status: "
                        + applicationStatus + ", sessionId: " + sessionId + ", wasLaunched: " + wasLaunched);
                mApplicationStarted = true;

                mMediaPlayer = new com.sesamtv.cordova.chromecast.MediaPlayer(mApiClient, ChromeCast.this);
                mMediaPlayer.attachMediaPlayer();

                onSessionStarted(result);
                clearChannelsQueue();
                // set the initial instructions
                // on the receiver
                //sendMessage(getString(R.string.instructions));
            } else {
                Log.e(mClassTag, "application could not launch");
                teardown();
            }
        }
    }

}