com.connectsdk.service.CastService.java Source code

Java tutorial

Introduction

Here is the source code for com.connectsdk.service.CastService.java

Source

/*
 * CastService
 * Connect SDK
 *
 * Copyright (c) 2014 LG Electronics.
 * Created by Hyun Kook Khang on 23 Feb 2014
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.connectsdk.service;

import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;

import com.connectsdk.core.ImageInfo;
import com.connectsdk.core.MediaInfo;
import com.connectsdk.core.Util;
import com.connectsdk.discovery.DiscoveryFilter;
import com.connectsdk.discovery.DiscoveryManager;
import com.connectsdk.service.capability.CapabilityMethods;
import com.connectsdk.service.capability.MediaControl;
import com.connectsdk.service.capability.MediaPlayer;
import com.connectsdk.service.capability.VolumeControl;
import com.connectsdk.service.capability.WebAppLauncher;
import com.connectsdk.service.capability.listeners.ResponseListener;
import com.connectsdk.service.command.ServiceCommandError;
import com.connectsdk.service.command.ServiceSubscription;
import com.connectsdk.service.command.URLServiceSubscription;
import com.connectsdk.service.config.ServiceConfig;
import com.connectsdk.service.config.ServiceDescription;
import com.connectsdk.service.sessions.CastWebAppSession;
import com.connectsdk.service.sessions.LaunchSession;
import com.connectsdk.service.sessions.LaunchSession.LaunchSessionType;
import com.connectsdk.service.sessions.WebAppSession;
import com.connectsdk.service.sessions.WebAppSession.WebAppPinStatusListener;
import com.google.android.gms.cast.ApplicationMetadata;
import com.google.android.gms.cast.Cast;
import com.google.android.gms.cast.Cast.ApplicationConnectionResult;
import com.google.android.gms.cast.CastDevice;
import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.LaunchOptions;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.cast.MediaTrack;
import com.google.android.gms.cast.RemoteMediaPlayer;
import com.google.android.gms.cast.RemoteMediaPlayer.MediaChannelResult;
import com.google.android.gms.cast.TextTrackStyle;
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 com.google.android.gms.common.images.WebImage;

import org.json.JSONObject;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;

public class CastService extends DeviceService implements MediaPlayer, MediaControl, VolumeControl, WebAppLauncher {
    interface ConnectionListener {
        void onConnected();
    }

    public interface LaunchWebAppListener {
        void onSuccess(WebAppSession webAppSession);

        void onFailure(ServiceCommandError error);
    }

    // @cond INTERNAL

    public static final String ID = "Chromecast";

    public final static String PLAY_STATE = "PlayState";
    public final static String CAST_SERVICE_VOLUME_SUBSCRIPTION_NAME = "volume";
    public final static String CAST_SERVICE_MUTE_SUBSCRIPTION_NAME = "mute";

    // @endcond

    String currentAppId;
    String launchingAppId;

    GoogleApiClient mApiClient;
    CastListener mCastClientListener;
    ConnectionCallbacks mConnectionCallbacks;
    ConnectionFailedListener mConnectionFailedListener;

    CastDevice castDevice;
    RemoteMediaPlayer mMediaPlayer;

    Map<String, CastWebAppSession> sessions;
    List<URLServiceSubscription<?>> subscriptions;

    float currentVolumeLevel;
    boolean currentMuteStatus;
    boolean mWaitingForReconnect;

    static String applicationID = CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID;

    // Queue of commands that should be sent once register is complete
    CopyOnWriteArraySet<ConnectionListener> commandQueue = new CopyOnWriteArraySet<ConnectionListener>();

    public CastService(ServiceDescription serviceDescription, ServiceConfig serviceConfig) {
        super(serviceDescription, serviceConfig);

        mCastClientListener = new CastListener();
        mConnectionCallbacks = new ConnectionCallbacks();
        mConnectionFailedListener = new ConnectionFailedListener();

        sessions = new HashMap<String, CastWebAppSession>();
        subscriptions = new ArrayList<URLServiceSubscription<?>>();

        mWaitingForReconnect = false;
    }

    @Override
    public String getServiceName() {
        return ID;
    }

    public static DiscoveryFilter discoveryFilter() {
        return new DiscoveryFilter(ID, "Chromecast");
    }

    public static void setApplicationID(String id) {
        applicationID = id;
    }

    public static String getApplicationID() {
        return applicationID;
    }

    @Override
    public CapabilityPriorityLevel getPriorityLevel(Class<? extends CapabilityMethods> clazz) {
        if (clazz.equals(MediaPlayer.class)) {
            return getMediaPlayerCapabilityLevel();
        } else if (clazz.equals(MediaControl.class)) {
            return getMediaControlCapabilityLevel();
        } else if (clazz.equals(VolumeControl.class)) {
            return getVolumeControlCapabilityLevel();
        } else if (clazz.equals(WebAppLauncher.class)) {
            return getWebAppLauncherCapabilityLevel();
        }
        return CapabilityPriorityLevel.NOT_SUPPORTED;
    }

    @Override
    public void connect() {
        if (connected && mApiClient != null && mApiClient.isConnecting() && mApiClient.isConnected())
            return;

        if (castDevice == null) {
            this.castDevice = (CastDevice) getServiceDescription().getDevice();
        }

        if (mApiClient == null) {
            mApiClient = createApiClient();
            mApiClient.connect();
        }
    }

    protected GoogleApiClient createApiClient() {
        Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(castDevice, mCastClientListener);

        return new GoogleApiClient.Builder(DiscoveryManager.getInstance().getContext())
                .addApi(Cast.API, apiOptionsBuilder.build()).addConnectionCallbacks(mConnectionCallbacks)
                .addOnConnectionFailedListener(mConnectionFailedListener).build();
    }

    @Override
    public void disconnect() {
        if (!connected)
            return;

        connected = false;
        mWaitingForReconnect = false;

        detachMediaPlayer();

        if (!commandQueue.isEmpty())
            commandQueue.clear();

        if (mApiClient != null && mApiClient.isConnected()) {
            Cast.CastApi.leaveApplication(mApiClient);
            mApiClient.disconnect();
        }
        mApiClient = null;

        Util.runOnUI(new Runnable() {

            @Override
            public void run() {
                if (getListener() != null) {
                    getListener().onDisconnect(CastService.this, null);
                }
            }
        });
    }

    @Override
    public MediaControl getMediaControl() {
        return this;
    }

    @Override
    public CapabilityPriorityLevel getMediaControlCapabilityLevel() {
        return CapabilityPriorityLevel.HIGH;
    }

    @Override
    public void play(final ResponseListener<Object> listener) {
        ConnectionListener connectionListener = new ConnectionListener() {

            @Override
            public void onConnected() {
                try {
                    mMediaPlayer.play(mApiClient);
                    Util.postSuccess(listener, null);
                } catch (Exception e) {
                    Util.postError(listener, new ServiceCommandError(0, "Unable to play", null));
                }
            }
        };

        runCommand(connectionListener);
    }

    @Override
    public void pause(final ResponseListener<Object> listener) {
        ConnectionListener connectionListener = new ConnectionListener() {

            @Override
            public void onConnected() {
                try {
                    mMediaPlayer.pause(mApiClient);

                    Util.postSuccess(listener, null);
                } catch (Exception e) {
                    Util.postError(listener, new ServiceCommandError(0, "Unable to pause", null));
                }
            }
        };

        runCommand(connectionListener);
    }

    @Override
    public void stop(final ResponseListener<Object> listener) {
        ConnectionListener connectionListener = new ConnectionListener() {

            @Override
            public void onConnected() {
                try {
                    mMediaPlayer.stop(mApiClient);

                    Util.postSuccess(listener, null);
                } catch (Exception e) {
                    Util.postError(listener, new ServiceCommandError(0, "Unable to stop", null));
                }
            }
        };

        runCommand(connectionListener);
    }

    @Override
    public void rewind(ResponseListener<Object> listener) {
        Util.postError(listener, ServiceCommandError.notSupported());
    }

    @Override
    public void fastForward(ResponseListener<Object> listener) {
        Util.postError(listener, ServiceCommandError.notSupported());
    }

    @Override
    public void previous(ResponseListener<Object> listener) {
        Util.postError(listener, ServiceCommandError.notSupported());
    }

    @Override
    public void next(ResponseListener<Object> listener) {
        Util.postError(listener, ServiceCommandError.notSupported());
    }

    @Override
    public void seek(final long position, final ResponseListener<Object> listener) {
        if (mMediaPlayer == null || mMediaPlayer.getMediaStatus() == null) {
            Util.postError(listener, new ServiceCommandError(0, "There is no media currently available", null));
            return;
        }

        ConnectionListener connectionListener = new ConnectionListener() {

            @Override
            public void onConnected() {
                try {
                    mMediaPlayer.seek(mApiClient, position, RemoteMediaPlayer.RESUME_STATE_UNCHANGED)
                            .setResultCallback(new ResultCallback<MediaChannelResult>() {

                                @Override
                                public void onResult(MediaChannelResult result) {
                                    Status status = result.getStatus();

                                    if (status.isSuccess()) {
                                        Util.postSuccess(listener, null);
                                    } else {
                                        Util.postError(listener, new ServiceCommandError(status.getStatusCode(),
                                                status.getStatusMessage(), status));
                                    }
                                }
                            });
                } catch (Exception e) {
                    Util.postError(listener, new ServiceCommandError(0, "Unable to seek", null));
                }
            }
        };

        runCommand(connectionListener);
    }

    @Override
    public void getDuration(final DurationListener listener) {
        if (mMediaPlayer != null && mMediaPlayer.getMediaStatus() != null) {
            Util.postSuccess(listener, mMediaPlayer.getStreamDuration());
        } else {
            Util.postError(listener, new ServiceCommandError(0, "There is no media currently available", null));
        }
    }

    @Override
    public void getPosition(final PositionListener listener) {
        if (mMediaPlayer != null && mMediaPlayer.getMediaStatus() != null) {
            Util.postSuccess(listener, mMediaPlayer.getApproximateStreamPosition());
        } else {
            Util.postError(listener, new ServiceCommandError(0, "There is no media currently available", null));
        }
    }

    @Override
    public MediaPlayer getMediaPlayer() {
        return this;
    }

    @Override
    public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() {
        return CapabilityPriorityLevel.HIGH;
    }

    @Override
    public void getMediaInfo(MediaInfoListener listener) {
        if (mMediaPlayer == null)
            return;

        if (mMediaPlayer.getMediaInfo() != null) {
            String url = mMediaPlayer.getMediaInfo().getContentId();
            String mimeType = mMediaPlayer.getMediaInfo().getContentType();

            MediaMetadata metadata = mMediaPlayer.getMediaInfo().getMetadata();
            String title = null;
            String description = null;
            ArrayList<ImageInfo> list = null;

            if (metadata != null) {
                title = metadata.getString(MediaMetadata.KEY_TITLE);
                description = metadata.getString(MediaMetadata.KEY_SUBTITLE);

                if (metadata.getImages() != null && metadata.getImages().size() > 0) {
                    String iconUrl = metadata.getImages().get(0).getUrl().toString();
                    list = new ArrayList<ImageInfo>();
                    list.add(new ImageInfo(iconUrl));
                }
            }

            MediaInfo info = new MediaInfo(url, mimeType, title, description, list);

            Util.postSuccess(listener, info);
        } else {
            Util.postError(listener, new ServiceCommandError(0, "Media Info is null", null));
        }
    }

    @Override
    public ServiceSubscription<MediaInfoListener> subscribeMediaInfo(MediaInfoListener listener) {
        URLServiceSubscription<MediaInfoListener> request = new URLServiceSubscription<MediaInfoListener>(this,
                "info", null, null);
        request.addListener(listener);
        addSubscription(request);

        return request;
    }

    private void attachMediaPlayer() {
        if (mMediaPlayer != null) {
            return;
        }

        mMediaPlayer = createMediaPlayer();
        mMediaPlayer.setOnStatusUpdatedListener(new RemoteMediaPlayer.OnStatusUpdatedListener() {

            @Override
            public void onStatusUpdated() {
                if (subscriptions.size() > 0) {
                    for (URLServiceSubscription<?> subscription : subscriptions) {
                        if (subscription.getTarget().equalsIgnoreCase(PLAY_STATE)) {
                            for (int i = 0; i < subscription.getListeners().size(); i++) {
                                @SuppressWarnings("unchecked")
                                ResponseListener<Object> listener = (ResponseListener<Object>) subscription
                                        .getListeners().get(i);
                                if (mMediaPlayer != null && mMediaPlayer.getMediaStatus() != null) {
                                    PlayStateStatus status = PlayStateStatus.convertPlayerStateToPlayStateStatus(
                                            mMediaPlayer.getMediaStatus().getPlayerState());
                                    Util.postSuccess(listener, status);
                                }
                            }
                        }
                    }
                }
            }
        });

        mMediaPlayer.setOnMetadataUpdatedListener(new RemoteMediaPlayer.OnMetadataUpdatedListener() {
            @Override
            public void onMetadataUpdated() {
                if (subscriptions.size() > 0) {
                    for (URLServiceSubscription<?> subscription : subscriptions) {
                        if (subscription.getTarget().equalsIgnoreCase("info")) {
                            for (int i = 0; i < subscription.getListeners().size(); i++) {
                                MediaInfoListener listener = (MediaInfoListener) subscription.getListeners().get(i);
                                getMediaInfo(listener);
                            }
                        }
                    }
                }
            }
        });

        if (mApiClient != null) {
            try {
                Cast.CastApi.setMessageReceivedCallbacks(mApiClient, mMediaPlayer.getNamespace(), mMediaPlayer);
            } catch (Exception e) {
                Log.w(Util.T, "Exception while creating media channel", e);
            }
        }
    }

    protected RemoteMediaPlayer createMediaPlayer() {
        return new RemoteMediaPlayer();
    }

    private void detachMediaPlayer() {
        if ((mMediaPlayer != null) && (mApiClient != null)) {
            try {
                Cast.CastApi.removeMessageReceivedCallbacks(mApiClient, mMediaPlayer.getNamespace());
            } catch (IOException e) {
                Log.w(Util.T, "Exception while launching application", e);
            }
        }
        mMediaPlayer = null;
    }

    @Override
    public void displayImage(String url, String mimeType, String title, String description, String iconSrc,
            LaunchListener listener) {
        MediaMetadata mMediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_PHOTO);
        mMediaMetadata.putString(MediaMetadata.KEY_TITLE, title);
        mMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, description);

        if (iconSrc != null) {
            Uri iconUri = Uri.parse(iconSrc);
            WebImage image = new WebImage(iconUri, 100, 100);
            mMediaMetadata.addImage(image);
        }

        com.google.android.gms.cast.MediaInfo mediaInformation = new com.google.android.gms.cast.MediaInfo.Builder(
                url).setContentType(mimeType).setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_NONE)
                        .setMetadata(mMediaMetadata).setStreamDuration(0).setCustomData(null).build();

        playMedia(mediaInformation, applicationID, listener);
    }

    @Override
    public void displayImage(MediaInfo mediaInfo, LaunchListener listener) {
        String mediaUrl = null;
        String mimeType = null;
        String title = null;
        String desc = null;
        String iconSrc = null;

        if (mediaInfo != null) {
            mediaUrl = mediaInfo.getUrl();
            mimeType = mediaInfo.getMimeType();
            title = mediaInfo.getTitle();
            desc = mediaInfo.getDescription();

            if (mediaInfo.getImages() != null && mediaInfo.getImages().size() > 0) {
                ImageInfo imageInfo = mediaInfo.getImages().get(0);
                iconSrc = imageInfo.getUrl();
            }
        }

        displayImage(mediaUrl, mimeType, title, desc, iconSrc, listener);
    }

    public void playMedia(String url, String subsUrl, String mimeType, String title, String description,
            String iconSrc, boolean shouldLoop, LaunchListener listener) {
        MediaMetadata mMediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
        mMediaMetadata.putString(MediaMetadata.KEY_TITLE, title);
        mMediaMetadata.putString(MediaMetadata.KEY_SUBTITLE, description);

        if (iconSrc != null) {
            Uri iconUri = Uri.parse(iconSrc);
            WebImage image = new WebImage(iconUri, 100, 100);
            mMediaMetadata.addImage(image);
        }

        List<MediaTrack> mediaTracks = new ArrayList<>();
        if (subsUrl != null) {
            MediaTrack subtitle = new MediaTrack.Builder(1, MediaTrack.TYPE_TEXT).setName("Subtitle")
                    .setSubtype(MediaTrack.SUBTYPE_SUBTITLES).setContentId(subsUrl).setContentType("text/vtt")
                    .setLanguage("en").build();

            mediaTracks.add(subtitle);
        }

        com.google.android.gms.cast.MediaInfo mediaInformation = new com.google.android.gms.cast.MediaInfo.Builder(
                url).setContentType(mimeType)
                        .setStreamType(com.google.android.gms.cast.MediaInfo.STREAM_TYPE_BUFFERED)
                        .setMetadata(mMediaMetadata).setStreamDuration(1000).setCustomData(null)
                        .setMediaTracks(mediaTracks).build();

        playMedia(mediaInformation, applicationID, listener);
    }

    @Override
    public void playMedia(String url, String mimeType, String title, String description, String iconSrc,
            boolean shouldLoop, LaunchListener listener) {
        playMedia(url, null, mimeType, title, description, iconSrc, shouldLoop, listener);
    }

    @Override
    public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, LaunchListener listener) {
        String mediaUrl = null;
        String subsUrl = null;
        String mimeType = null;
        String title = null;
        String desc = null;
        String iconSrc = null;

        if (mediaInfo != null) {
            mediaUrl = mediaInfo.getUrl();
            subsUrl = mediaInfo.getSubsUrl();
            mimeType = mediaInfo.getMimeType();
            title = mediaInfo.getTitle();
            desc = mediaInfo.getDescription();

            if (mediaInfo.getImages() != null && mediaInfo.getImages().size() > 0) {
                ImageInfo imageInfo = mediaInfo.getImages().get(0);
                iconSrc = imageInfo.getUrl();
            }
        }

        playMedia(mediaUrl, subsUrl, mimeType, title, desc, iconSrc, shouldLoop, listener);
    }

    private void playMedia(final com.google.android.gms.cast.MediaInfo mediaInformation, final String mediaAppId,
            final LaunchListener listener) {
        final ApplicationConnectionResultCallback webAppLaunchCallback = new ApplicationConnectionResultCallback(
                new LaunchWebAppListener() {

                    @Override
                    public void onSuccess(final WebAppSession webAppSession) {
                        ConnectionListener connectionListener = new ConnectionListener() {

                            @Override
                            public void onConnected() {
                                try {
                                    mMediaPlayer.load(mApiClient, mediaInformation, true).setResultCallback(
                                            new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {

                                                @Override
                                                public void onResult(MediaChannelResult result) {
                                                    Status status = result.getStatus();

                                                    if (status.isSuccess()) {
                                                        webAppSession.launchSession
                                                                .setSessionType(LaunchSessionType.Media);

                                                        // White text, black outline, no background
                                                        TextTrackStyle textTrackStyle = new TextTrackStyle();
                                                        textTrackStyle
                                                                .setForegroundColor(Color.parseColor("#FFFFFFFF"));
                                                        textTrackStyle
                                                                .setBackgroundColor(Color.parseColor("#01000000"));
                                                        textTrackStyle
                                                                .setWindowType(TextTrackStyle.WINDOW_TYPE_NONE);
                                                        textTrackStyle
                                                                .setEdgeType(TextTrackStyle.EDGE_TYPE_OUTLINE);
                                                        textTrackStyle.setEdgeColor(Color.BLACK);
                                                        textTrackStyle.setFontGenericFamily(
                                                                TextTrackStyle.FONT_FAMILY_SANS_SERIF);

                                                        mMediaPlayer.setTextTrackStyle(mApiClient, textTrackStyle);
                                                        mMediaPlayer.setActiveMediaTracks(mApiClient,
                                                                new long[] { 1 });

                                                        Util.postSuccess(listener, new MediaLaunchObject(
                                                                webAppSession.launchSession, CastService.this));
                                                    } else {
                                                        Util.postError(listener,
                                                                new ServiceCommandError(status.getStatusCode(),
                                                                        status.getStatusMessage(), status));
                                                    }
                                                }
                                            });
                                } catch (Exception e) {
                                    Util.postError(listener, new ServiceCommandError(0, "Unable to load", null));
                                }
                            }
                        };

                        runCommand(connectionListener);
                    }

                    @Override
                    public void onFailure(ServiceCommandError error) {
                        Util.postError(listener, error);
                    }
                });

        launchingAppId = mediaAppId;

        ConnectionListener connectionListener = new ConnectionListener() {

            @Override
            public void onConnected() {
                boolean relaunchIfRunning = false;

                try {
                    if (Cast.CastApi.getApplicationStatus(mApiClient) == null
                            || (!mediaAppId.equals(currentAppId))) {
                        relaunchIfRunning = true;
                    }

                    LaunchOptions options = new LaunchOptions();
                    options.setRelaunchIfRunning(relaunchIfRunning);
                    Cast.CastApi.launchApplication(mApiClient, mediaAppId, options)
                            .setResultCallback(webAppLaunchCallback);
                } catch (Exception e) {
                    Util.postError(listener, new ServiceCommandError(0, "Unable to launch", null));
                }
            }
        };

        runCommand(connectionListener);
    }

    @Override
    public void closeMedia(final LaunchSession launchSession, final ResponseListener<Object> listener) {
        ConnectionListener connectionListener = new ConnectionListener() {

            @Override
            public void onConnected() {
                try {
                    Cast.CastApi.stopApplication(mApiClient, launchSession.getSessionId())
                            .setResultCallback(new ResultCallback<Status>() {

                                @Override
                                public void onResult(Status result) {
                                    if (result.isSuccess()) {
                                        Util.postSuccess(listener, result);
                                    } else {
                                        Util.postError(listener, new ServiceCommandError(result.getStatusCode(),
                                                result.getStatusMessage(), result));
                                    }
                                }
                            });
                } catch (Exception e) {
                    Util.postError(listener, new ServiceCommandError(0, "Unable to stop", null));
                }
            }
        };

        runCommand(connectionListener);
    }

    @Override
    public WebAppLauncher getWebAppLauncher() {
        return this;
    }

    @Override
    public CapabilityPriorityLevel getWebAppLauncherCapabilityLevel() {
        return CapabilityPriorityLevel.HIGH;
    }

    @Override
    public void launchWebApp(String webAppId, WebAppSession.LaunchListener listener) {
        launchWebApp(webAppId, true, listener);
    }

    @Override
    public void launchWebApp(final String webAppId, final boolean relaunchIfRunning,
            final WebAppSession.LaunchListener listener) {
        launchingAppId = webAppId;

        final LaunchWebAppListener launchWebAppListener = new LaunchWebAppListener() {
            @Override
            public void onSuccess(WebAppSession webAppSession) {
                Util.postSuccess(listener, webAppSession);
            }

            @Override
            public void onFailure(ServiceCommandError error) {
                Util.postError(listener, error);
            }
        };

        ConnectionListener connectionListener = new ConnectionListener() {

            @Override
            public void onConnected() {
                // TODO Workaround, for some reason, if relaunchIfRunning is false, launchApplication returns 2005 error and cannot launch.
                try {
                    if (relaunchIfRunning == false) {
                        Cast.CastApi.joinApplication(mApiClient)
                                .setResultCallback(new ResultCallback<Cast.ApplicationConnectionResult>() {

                                    @Override
                                    public void onResult(ApplicationConnectionResult result) {
                                        if (result.getStatus().isSuccess()
                                                && result.getApplicationMetadata() != null
                                                && result.getApplicationMetadata().getName() != null
                                                && result.getApplicationMetadata().getApplicationId()
                                                        .equals(webAppId)) {
                                            ApplicationMetadata applicationMetadata = result
                                                    .getApplicationMetadata();
                                            currentAppId = applicationMetadata.getApplicationId();

                                            LaunchSession launchSession = LaunchSession
                                                    .launchSessionForAppId(applicationMetadata.getApplicationId());
                                            launchSession.setAppName(applicationMetadata.getName());
                                            launchSession.setSessionId(result.getSessionId());
                                            launchSession.setSessionType(LaunchSessionType.WebApp);
                                            launchSession.setService(CastService.this);

                                            CastWebAppSession webAppSession = new CastWebAppSession(launchSession,
                                                    CastService.this);
                                            webAppSession.setMetadata(applicationMetadata);

                                            sessions.put(applicationMetadata.getApplicationId(), webAppSession);

                                            Util.postSuccess(listener, webAppSession);
                                        } else {
                                            LaunchOptions options = new LaunchOptions();
                                            options.setRelaunchIfRunning(true);

                                            try {
                                                Cast.CastApi.launchApplication(mApiClient, webAppId, options)
                                                        .setResultCallback(new ApplicationConnectionResultCallback(
                                                                launchWebAppListener));
                                            } catch (Exception e) {
                                                Util.postError(listener,
                                                        new ServiceCommandError(0, "Unable to launch", null));
                                            }
                                        }
                                    }
                                });
                    } else {
                        LaunchOptions options = new LaunchOptions();
                        options.setRelaunchIfRunning(relaunchIfRunning);

                        Cast.CastApi.launchApplication(mApiClient, webAppId, options)
                                .setResultCallback(new ApplicationConnectionResultCallback(launchWebAppListener));
                    }
                } catch (Exception e) {
                    Util.postError(listener, new ServiceCommandError(0, "Unable to launch", null));
                }
            }
        };

        runCommand(connectionListener);
    }

    @Override
    public void launchWebApp(String webAppId, JSONObject params, WebAppSession.LaunchListener listener) {
        Util.postError(listener, ServiceCommandError.notSupported());
    }

    @Override
    public void launchWebApp(String webAppId, JSONObject params, boolean relaunchIfRunning,
            WebAppSession.LaunchListener listener) {
        Util.postError(listener, ServiceCommandError.notSupported());
    }

    public void requestStatus(final ResponseListener<Object> listener) {
        try {
            mMediaPlayer.requestStatus(mApiClient)
                    .setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {

                        @Override
                        public void onResult(MediaChannelResult result) {
                            if (result.getStatus().isSuccess()) {
                                Util.postSuccess(listener, result);
                            } else {
                                Util.postError(listener,
                                        new ServiceCommandError(0, "Failed to request status", result));
                            }
                        }
                    });
        } catch (Exception e) {
            Util.postError(listener, new ServiceCommandError(0, "There is no media currently available", null));
        }
    }

    public void joinApplication(final ResponseListener<Object> listener) {
        ConnectionListener connectionListener = new ConnectionListener() {

            @Override
            public void onConnected() {
                try {
                    Cast.CastApi.joinApplication(mApiClient)
                            .setResultCallback(new ResultCallback<Cast.ApplicationConnectionResult>() {

                                @Override
                                public void onResult(ApplicationConnectionResult result) {
                                    if (result.getStatus().isSuccess()) {
                                        // TODO: Maybe there is better way to check current cast device is showing backdrop, but for now, if chromecast is showing backdrop, then requestStatus would never response.
                                        if (result.getApplicationMetadata() != null
                                                && result.getApplicationMetadata().getName() != null
                                                && !result.getApplicationMetadata().getName().equals("Backdrop")
                                                && mMediaPlayer != null && mApiClient != null) {

                                            mMediaPlayer.requestStatus(mApiClient).setResultCallback(
                                                    new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {

                                                        @Override
                                                        public void onResult(MediaChannelResult result) {
                                                            Util.postSuccess(listener, result);
                                                        }
                                                    });
                                        } else {
                                            Util.postSuccess(listener, result);
                                        }
                                    } else {
                                        Util.postError(listener,
                                                new ServiceCommandError(0, "Failed to join application", result));
                                    }
                                }
                            });
                } catch (Exception e) {
                    Util.postError(listener, new ServiceCommandError(0, "Unable to join", null));
                }
            }
        };

        runCommand(connectionListener);
    }

    @Override
    public void joinWebApp(final LaunchSession webAppLaunchSession, final WebAppSession.LaunchListener listener) {
        final ApplicationConnectionResultCallback webAppLaunchCallback = new ApplicationConnectionResultCallback(
                new LaunchWebAppListener() {

                    @Override
                    public void onSuccess(final WebAppSession webAppSession) {
                        webAppSession.connect(new ResponseListener<Object>() {

                            @Override
                            public void onSuccess(Object object) {
                                requestStatus(new ResponseListener<Object>() {
                                    @Override
                                    public void onSuccess(Object object) {
                                        Util.postSuccess(listener, webAppSession);
                                    }

                                    @Override
                                    public void onError(ServiceCommandError error) {
                                        // we sent success, because join is already succeeded.
                                        Util.postSuccess(listener, webAppSession);
                                    }
                                });
                            }

                            @Override
                            public void onError(ServiceCommandError error) {
                                Util.postError(listener, error);
                            }
                        });
                    }

                    @Override
                    public void onFailure(ServiceCommandError error) {
                        Util.postError(listener, error);
                    }
                });

        launchingAppId = webAppLaunchSession.getAppId();

        ConnectionListener connectionListener = new ConnectionListener() {

            @Override
            public void onConnected() {
                try {
                    Cast.CastApi.joinApplication(mApiClient, webAppLaunchSession.getAppId())
                            .setResultCallback(webAppLaunchCallback);
                } catch (Exception e) {
                    Util.postError(listener, new ServiceCommandError(0, "Unable to join", null));
                }
            }
        };

        runCommand(connectionListener);
    }

    @Override
    public void joinWebApp(String webAppId, WebAppSession.LaunchListener listener) {
        LaunchSession launchSession = LaunchSession.launchSessionForAppId(webAppId);
        launchSession.setSessionType(LaunchSessionType.WebApp);
        launchSession.setService(this);

        joinWebApp(launchSession, listener);
    }

    @Override
    public void closeWebApp(LaunchSession launchSession, final ResponseListener<Object> listener) {
        ConnectionListener connectionListener = new ConnectionListener() {

            @Override
            public void onConnected() {
                try {
                    Cast.CastApi.stopApplication(mApiClient).setResultCallback(new ResultCallback<Status>() {

                        @Override
                        public void onResult(Status status) {
                            if (status.isSuccess()) {
                                Util.postSuccess(listener, null);
                            } else {
                                Util.postError(listener, new ServiceCommandError(status.getStatusCode(),
                                        status.getStatusMessage(), status));
                            }
                        }
                    });
                } catch (Exception e) {
                    Util.postError(listener, new ServiceCommandError(0, "Unable to stop", null));
                }
            }
        };

        runCommand(connectionListener);
    }

    @Override
    public void pinWebApp(String webAppId, ResponseListener<Object> listener) {
        Util.postError(listener, ServiceCommandError.notSupported());
    }

    @Override
    public void unPinWebApp(String webAppId, ResponseListener<Object> listener) {
        Util.postError(listener, ServiceCommandError.notSupported());
    }

    @Override
    public void isWebAppPinned(String webAppId, WebAppPinStatusListener listener) {
        Util.postError(listener, ServiceCommandError.notSupported());
    }

    @Override
    public ServiceSubscription<WebAppPinStatusListener> subscribeIsWebAppPinned(String webAppId,
            WebAppPinStatusListener listener) {
        Util.postError(listener, ServiceCommandError.notSupported());
        return null;
    }

    @Override
    public VolumeControl getVolumeControl() {
        return this;
    }

    @Override
    public CapabilityPriorityLevel getVolumeControlCapabilityLevel() {
        return CapabilityPriorityLevel.HIGH;
    }

    @Override
    public void volumeUp(final ResponseListener<Object> listener) {
        getVolume(new VolumeListener() {

            @Override
            public void onSuccess(final Float volume) {
                if (volume >= 1.0) {
                    Util.postSuccess(listener, null);
                } else {
                    float newVolume = (float) (volume + 0.01);

                    if (newVolume > 1.0)
                        newVolume = (float) 1.0;

                    setVolume(newVolume, listener);

                    Util.postSuccess(listener, null);
                }
            }

            @Override
            public void onError(ServiceCommandError error) {
                Util.postError(listener, error);
            }
        });
    }

    @Override
    public void volumeDown(final ResponseListener<Object> listener) {
        getVolume(new VolumeListener() {

            @Override
            public void onSuccess(final Float volume) {
                if (volume <= 0.0) {
                    Util.postSuccess(listener, null);
                } else {
                    float newVolume = (float) (volume - 0.01);

                    if (newVolume < 0.0)
                        newVolume = (float) 0.0;

                    setVolume(newVolume, listener);

                    Util.postSuccess(listener, null);
                }
            }

            @Override
            public void onError(ServiceCommandError error) {
                Util.postError(listener, error);
            }
        });
    }

    @Override
    public void setVolume(final float volume, final ResponseListener<Object> listener) {
        ConnectionListener connectionListener = new ConnectionListener() {

            @Override
            public void onConnected() {
                try {
                    Cast.CastApi.setVolume(mApiClient, volume);
                    Util.postSuccess(listener, null);
                } catch (Exception e) {
                    Util.postError(listener, new ServiceCommandError(0, "setting volume level failed", null));
                }
            }
        };

        runCommand(connectionListener);
    }

    @Override
    public void getVolume(VolumeListener listener) {
        Util.postSuccess(listener, currentVolumeLevel);
    }

    @Override
    public void setMute(final boolean isMute, final ResponseListener<Object> listener) {
        ConnectionListener connectionListener = new ConnectionListener() {

            @Override
            public void onConnected() {
                try {
                    Cast.CastApi.setMute(mApiClient, isMute);
                    Util.postSuccess(listener, null);
                } catch (Exception e) {
                    Util.postError(listener, new ServiceCommandError(0, "setting mute status failed", null));
                }
            }
        };

        runCommand(connectionListener);
    }

    @Override
    public void getMute(final MuteListener listener) {
        Util.postSuccess(listener, currentMuteStatus);
    }

    @Override
    public ServiceSubscription<VolumeListener> subscribeVolume(VolumeListener listener) {
        URLServiceSubscription<VolumeListener> request = new URLServiceSubscription<VolumeListener>(this,
                CAST_SERVICE_VOLUME_SUBSCRIPTION_NAME, null, null);
        request.addListener(listener);
        addSubscription(request);

        return request;
    }

    @Override
    public ServiceSubscription<MuteListener> subscribeMute(MuteListener listener) {
        URLServiceSubscription<MuteListener> request = new URLServiceSubscription<MuteListener>(this,
                CAST_SERVICE_MUTE_SUBSCRIPTION_NAME, null, null);
        request.addListener(listener);
        addSubscription(request);

        return request;
    }

    @Override
    protected void updateCapabilities() {
        List<String> capabilities = new ArrayList<String>();

        Collections.addAll(capabilities, MediaPlayer.Capabilities);
        Collections.addAll(capabilities, VolumeControl.Capabilities);

        capabilities.add(Play);
        capabilities.add(Pause);
        capabilities.add(Stop);
        capabilities.add(Duration);
        capabilities.add(Seek);
        capabilities.add(Position);
        capabilities.add(PlayState);
        capabilities.add(Subtitles_Vtt);
        capabilities.add(PlayState_Subscribe);

        capabilities.add(WebAppLauncher.Launch);
        capabilities.add(Message_Send);
        capabilities.add(Message_Receive);
        capabilities.add(Message_Send_JSON);
        capabilities.add(Message_Receive_JSON);
        capabilities.add(WebAppLauncher.Connect);
        capabilities.add(WebAppLauncher.Disconnect);
        capabilities.add(WebAppLauncher.Join);
        capabilities.add(WebAppLauncher.Close);

        setCapabilities(capabilities);
    }

    private class CastListener extends Cast.Listener {
        @Override
        public void onApplicationDisconnected(int statusCode) {
            Log.d(Util.T, "Cast.Listener.onApplicationDisconnected: " + statusCode);

            if (currentAppId == null)
                return;

            CastWebAppSession webAppSession = sessions.get(currentAppId);

            if (webAppSession == null)
                return;

            webAppSession.handleAppClose();

            currentAppId = null;
        }

        @Override
        public void onApplicationStatusChanged() {
            ConnectionListener connectionListener = new ConnectionListener() {

                @Override
                public void onConnected() {
                    if (mApiClient != null) {
                        ApplicationMetadata applicationMetadata = Cast.CastApi.getApplicationMetadata(mApiClient);

                        if (applicationMetadata != null)
                            currentAppId = applicationMetadata.getApplicationId();
                    }
                }
            };

            runCommand(connectionListener);
        }

        @Override
        public void onVolumeChanged() {
            ConnectionListener connectionListener = new ConnectionListener() {

                @Override
                public void onConnected() {
                    try {
                        currentVolumeLevel = (float) Cast.CastApi.getVolume(mApiClient);
                        currentMuteStatus = Cast.CastApi.isMute(mApiClient);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                    if (subscriptions.size() > 0) {
                        for (URLServiceSubscription<?> subscription : subscriptions) {
                            if (subscription.getTarget().equals(CAST_SERVICE_VOLUME_SUBSCRIPTION_NAME)) {
                                for (int i = 0; i < subscription.getListeners().size(); i++) {
                                    @SuppressWarnings("unchecked")
                                    ResponseListener<Object> listener = (ResponseListener<Object>) subscription
                                            .getListeners().get(i);

                                    Util.postSuccess(listener, currentVolumeLevel);
                                }
                            } else if (subscription.getTarget().equals(CAST_SERVICE_MUTE_SUBSCRIPTION_NAME)) {
                                for (int i = 0; i < subscription.getListeners().size(); i++) {
                                    @SuppressWarnings("unchecked")
                                    ResponseListener<Object> listener = (ResponseListener<Object>) subscription
                                            .getListeners().get(i);

                                    Util.postSuccess(listener, currentMuteStatus);
                                }
                            }
                        }
                    }
                }
            };

            runCommand(connectionListener);
        }
    }

    private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks {
        @Override
        public void onConnectionSuspended(final int cause) {
            Log.d(Util.T, "ConnectionCallbacks.onConnectionSuspended");

            mWaitingForReconnect = true;
            detachMediaPlayer();
        }

        @Override
        public void onConnected(Bundle connectionHint) {
            Log.d(Util.T, "ConnectionCallbacks.onConnected, wasWaitingForReconnect: " + mWaitingForReconnect);

            attachMediaPlayer();

            if (mApiClient != null) {
                Cast.CastApi.joinApplication(mApiClient)
                        .setResultCallback(new ResultCallback<Cast.ApplicationConnectionResult>() {

                            @Override
                            public void onResult(ApplicationConnectionResult result) {
                                if (result.getStatus().isSuccess()) {
                                    // TODO: Maybe there is better way to check current cast device is showing backdrop, but for now, if chromecast is showing backdrop, then requestStatus would never response.
                                    if (result.getApplicationMetadata() != null
                                            && result.getApplicationMetadata().getName() != null
                                            && !result.getApplicationMetadata().getName().equals("Backdrop")
                                            && mMediaPlayer != null && mApiClient != null) {

                                        mMediaPlayer.requestStatus(mApiClient).setResultCallback(
                                                new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {

                                                    @Override
                                                    public void onResult(MediaChannelResult result) {
                                                        joinFinished();
                                                    }
                                                });
                                    } else {
                                        joinFinished();
                                    }
                                } else {
                                    joinFinished();
                                }
                            }
                        });
            }
        }

        private void joinFinished() {
            if (mWaitingForReconnect) {
                mWaitingForReconnect = false;
            } else {
                connected = true;

                reportConnected(true);
            }

            if (!commandQueue.isEmpty()) {
                for (ConnectionListener listener : commandQueue) {
                    listener.onConnected();
                    commandQueue.remove(listener);
                }
            }
        }
    }

    private class ConnectionFailedListener implements GoogleApiClient.OnConnectionFailedListener {
        @Override
        public void onConnectionFailed(final ConnectionResult result) {
            Log.d(Util.T, "ConnectionFailedListener.onConnectionFailed " + (result != null ? result : ""));

            detachMediaPlayer();
            connected = false;
            mWaitingForReconnect = false;
            mApiClient = null;

            Util.runOnUI(new Runnable() {

                @Override
                public void run() {
                    if (listener != null) {
                        ServiceCommandError error = new ServiceCommandError(result.getErrorCode(),
                                "Failed to connect to Google Cast device", result);

                        listener.onConnectionFailure(CastService.this, error);
                    }
                }
            });
        }
    }

    private class ApplicationConnectionResultCallback implements ResultCallback<Cast.ApplicationConnectionResult> {
        LaunchWebAppListener listener;

        public ApplicationConnectionResultCallback(LaunchWebAppListener listener) {
            this.listener = listener;
        }

        @Override
        public void onResult(ApplicationConnectionResult result) {
            Status status = result.getStatus();

            if (status.isSuccess()) {
                ApplicationMetadata applicationMetadata = result.getApplicationMetadata();
                currentAppId = applicationMetadata.getApplicationId();

                LaunchSession launchSession = LaunchSession
                        .launchSessionForAppId(applicationMetadata.getApplicationId());
                launchSession.setAppName(applicationMetadata.getName());
                launchSession.setSessionId(result.getSessionId());
                launchSession.setSessionType(LaunchSessionType.WebApp);
                launchSession.setService(CastService.this);

                CastWebAppSession webAppSession = new CastWebAppSession(launchSession, CastService.this);
                webAppSession.setMetadata(applicationMetadata);

                sessions.put(applicationMetadata.getApplicationId(), webAppSession);

                if (listener != null) {
                    listener.onSuccess(webAppSession);
                }

                launchingAppId = null;
            } else {
                if (listener != null) {
                    listener.onFailure(
                            new ServiceCommandError(status.getStatusCode(), status.getStatusMessage(), status));
                }
            }
        }
    }

    @Override
    public void getPlayState(PlayStateListener listener) {
        if (mMediaPlayer != null && mMediaPlayer.getMediaStatus() != null) {
            PlayStateStatus status = PlayStateStatus
                    .convertPlayerStateToPlayStateStatus(mMediaPlayer.getMediaStatus().getPlayerState());
            Util.postSuccess(listener, status);
        } else {
            Util.postError(listener, new ServiceCommandError(0, "There is no media currently available", null));
        }
    }

    public GoogleApiClient getApiClient() {
        return mApiClient;
    }

    //////////////////////////////////////////////////
    //      Device Service Methods
    //////////////////////////////////////////////////
    @Override
    public boolean isConnectable() {
        return true;
    }

    @Override
    public boolean isConnected() {
        return connected;
    }

    @Override
    public ServiceSubscription<PlayStateListener> subscribePlayState(PlayStateListener listener) {
        URLServiceSubscription<PlayStateListener> request = new URLServiceSubscription<PlayStateListener>(this,
                PLAY_STATE, null, null);
        request.addListener(listener);
        addSubscription(request);

        return request;
    }

    private void addSubscription(URLServiceSubscription<?> subscription) {
        subscriptions.add(subscription);
    }

    @Override
    public void unsubscribe(URLServiceSubscription<?> subscription) {
        subscriptions.remove(subscription);
    }

    public List<URLServiceSubscription<?>> getSubscriptions() {
        return subscriptions;
    }

    public void setSubscriptions(List<URLServiceSubscription<?>> subscriptions) {
        this.subscriptions = subscriptions;
    }

    private void runCommand(ConnectionListener connectionListener) {
        if (mApiClient != null && mApiClient.isConnected()) {
            connectionListener.onConnected();
        } else {
            connect();
            commandQueue.add(connectionListener);
        }
    }

}