com.fastbootmobile.encore.cast.CastModule.java Source code

Java tutorial

Introduction

Here is the source code for com.fastbootmobile.encore.cast.CastModule.java

Source

/*
 * Copyright (C) 2014 Fastboot Mobile, LLC.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
 * the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with this program;
 * if not, see <http://www.gnu.org/licenses>.
 */

package com.fastbootmobile.encore.cast;

import android.content.Context;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.support.v7.media.MediaRouteSelector;
import android.support.v7.media.MediaRouter;
import android.text.format.Formatter;
import android.util.Log;

import com.fastbootmobile.encore.framework.PlaybackProxy;
import com.fastbootmobile.encore.model.Artist;
import com.fastbootmobile.encore.model.Song;
import com.fastbootmobile.encore.providers.ProviderAggregator;
import com.fastbootmobile.encore.service.BasePlaybackCallback;
import com.fastbootmobile.encore.service.IPlaybackCallback;
import com.google.android.gms.cast.Cast;
import com.google.android.gms.cast.CastDevice;
import com.google.android.gms.cast.CastMediaControlIntent;
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 org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;

/**
 * Module allowing casting to Chromecast and other MediaRouter-enabled receivers
 */
public class CastModule extends MediaRouter.Callback {
    private static final String TAG = "CastModule";
    private static final String CAST_APP_ID = "FB626268";

    private static final String JSON_KEY_COMMAND = "command";
    private static final String COMMAND_CONNECT = "connect";
    private static final String COMMAND_EVT_SONGSTARTED = "songstarted";
    private static final String COMMAND_EVT_PAUSED = "paused";
    private static final String COMMAND_EVT_RESUMED = "resumed";

    private Context mContext;
    private Handler mHandler;

    private MediaRouter mMediaRouter;
    private MediaRouteSelector mSelector;
    private CastDevice mSelectedDevice;
    private GoogleApiClient mApiClient;
    private CastListener mCastListener;
    private ConnectionCallbacks mConnectionCallbacks;
    private ConnectionFailedListener mConnectionFailedListener;
    private boolean mShouldStart;
    private CastChannel mCastChannel;
    private boolean mIsAppUp;

    private IPlaybackCallback.Stub mPlaybackCallback = new BasePlaybackCallback() {
        @Override
        public void onSongStarted(boolean buffering, Song s) throws RemoteException {
            if (mIsAppUp) {
                final ProviderAggregator aggregator = ProviderAggregator.getDefault();
                Artist a = aggregator.retrieveArtist(s.getArtist(), s.getProvider());

                try {
                    JSONObject msg = new JSONObject();
                    msg.put(JSON_KEY_COMMAND, COMMAND_EVT_SONGSTARTED);
                    msg.put("title", s.getTitle());
                    msg.put("artist", a != null ? a.getName() : "<Not loaded>");
                    msg.put("duration", s.getDuration());

                    mCastChannel.sendMessage(mApiClient, msg.toString());
                } catch (JSONException e) {
                    Log.e(TAG, "Cannot create JSON to notify Cast of new song", e);
                }
            }
        }

        @Override
        public void onPlaybackPause() throws RemoteException {
            if (mIsAppUp) {
                try {
                    JSONObject msg = new JSONObject();
                    msg.put(JSON_KEY_COMMAND, COMMAND_EVT_PAUSED);
                    mCastChannel.sendMessage(mApiClient, msg.toString());
                } catch (JSONException e) {
                    Log.e(TAG, "Cannot create JSON to notify Cast of pause event", e);
                }
            }
        }

        @Override
        public void onPlaybackResume() throws RemoteException {
            if (mIsAppUp) {
                try {
                    JSONObject msg = new JSONObject();
                    msg.put(JSON_KEY_COMMAND, COMMAND_EVT_RESUMED);
                    mCastChannel.sendMessage(mApiClient, msg.toString());
                } catch (JSONException e) {
                    Log.e(TAG, "Cannot create JSON to notify Cast of resume event", e);
                }
            }
        }
    };

    /**
     * Default constructor
     * @param ctx The host context of the module
     */
    public CastModule(Context ctx) {
        mContext = ctx;
        mHandler = new Handler();
        mConnectionCallbacks = new ConnectionCallbacks();
        mConnectionFailedListener = new ConnectionFailedListener();

        mMediaRouter = MediaRouter.getInstance(ctx);
        mSelector = new MediaRouteSelector.Builder()
                .addControlCategory(CastMediaControlIntent.categoryForCast(CAST_APP_ID)).build();

        mCastListener = new CastListener();
        mCastChannel = new CastChannel();
    }

    /**
     * Called when the main activity starts
     */
    public void onStart() {
        mMediaRouter.addCallback(mSelector, this, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
        PlaybackProxy.addCallback(mPlaybackCallback);
    }

    /**
     * Called when the main activity stops or pauses
     */
    public void onStop() {
        mMediaRouter.removeCallback(this);
        PlaybackProxy.removeCallback(mPlaybackCallback);
    }

    public MediaRouteSelector getSelector() {
        return mSelector;
    }

    @Override
    public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
        Log.d(TAG, "onRouteSelected: route=" + route);

        // secondary output device
        mSelectedDevice = CastDevice.getFromBundle(route.getExtras());
        updateCast();
    }

    @Override
    public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route) {
        Log.d(TAG, "onRouteUnselected: route=" + route);

        // secondary output device
        mSelectedDevice = null;
        updateCast();
        PlaybackProxy.setPhonePlayerMuted(false);
    }

    private void updateCast() {
        if (mSelectedDevice == null) {
            if ((mApiClient != null) && mApiClient.isConnected()) {
                mApiClient.disconnect();
            }
        } else {
            Log.d(TAG, "Acquiring controller for " + mSelectedDevice);
            try {
                Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions
                        .builder(mSelectedDevice, mCastListener).setVerboseLoggingEnabled(true);

                mApiClient = new GoogleApiClient.Builder(mContext).addApi(Cast.API, apiOptionsBuilder.build())
                        .addConnectionCallbacks(mConnectionCallbacks)
                        .addOnConnectionFailedListener(mConnectionFailedListener).build();

                mApiClient.connect();
            } catch (IllegalStateException e) {
                Log.w(TAG, "Error while creating a device controller", e);
            }
        }
    }

    private String getWiFiIpAddress() {
        WifiManager wifiMgr = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
        WifiInfo wifiInfo = wifiMgr.getConnectionInfo();
        int ip = wifiInfo.getIpAddress();
        return Formatter.formatIpAddress(ip);
    }

    private void attachMediaPlayer() {
        // Get the device's Wi-Fi IP address (Wi-Fi is obviously enabled for Chromecast)
        JSONObject object = new JSONObject();
        try {
            object.put(JSON_KEY_COMMAND, COMMAND_CONNECT);
            object.put("address", getWiFiIpAddress());
        } catch (JSONException e) {
            Log.e(TAG, "Cannot build JSON object!", e);
        }

        mCastChannel.sendMessage(mApiClient, object.toString());
        mIsAppUp = true;
    }

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

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

            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (!mApiClient.isConnected()) {
                        // We got disconnected while this runnable was pending execution.
                        return;
                    }

                    try {
                        mShouldStart = true;
                        Cast.CastApi.requestStatus(mApiClient);
                    } catch (IOException e) {
                        Log.d(TAG, "error requesting status", e);
                    }
                }
            });
        }
    }

    private class ConnectionFailedListener implements GoogleApiClient.OnConnectionFailedListener {
        @Override
        public void onConnectionFailed(ConnectionResult result) {
            Log.d(TAG, "onConnectionFailed");
        }
    }

    private class CastListener extends Cast.Listener {
        @Override
        public void onVolumeChanged() {
            /*refreshDeviceVolume(Cast.CastApi.getVolume(mApiClient),
                Cast.CastApi.isMute(mApiClient));*/
        }

        @Override
        public void onApplicationStatusChanged() {
            try {
                String status = Cast.CastApi.getApplicationStatus(mApiClient);
                Log.d(TAG, "onApplicationStatusChanged; status=" + status);

                if (mShouldStart && mApiClient.isConnected()) {
                    Cast.CastApi.launchApplication(mApiClient, CAST_APP_ID, true)
                            .setResultCallback(new ApplicationConnectionResultCallback("LaunchApp"));
                    mShouldStart = false;
                    PlaybackProxy.setPhonePlayerMuted(true);
                } else {
                    mIsAppUp = false;
                    PlaybackProxy.setPhonePlayerMuted(false);
                }
            } catch (IllegalStateException e) {
                // Not connected to a device
                mIsAppUp = false;
                PlaybackProxy.setPhonePlayerMuted(false);
            }
        }

        @Override
        public void onApplicationDisconnected(int statusCode) {
            Log.d(TAG, "onApplicationDisconnected: statusCode=" + statusCode);
            mIsAppUp = false;
            PlaybackProxy.setPhonePlayerMuted(false);
        }
    }

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

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

        @Override
        public void onResult(Cast.ApplicationConnectionResult result) {
            Status status = result.getStatus();
            Log.d(mClassTag, "ApplicationConnectionResultCallback.onResult: " + status);

            if (status.isSuccess()) {
                // Our app is launched on the Chromecast
                try {
                    Cast.CastApi.setMessageReceivedCallbacks(mApiClient, mCastChannel.getNamespace(), mCastChannel);
                    Log.d(TAG, "Registered callback for " + mCastChannel.getNamespace());
                } catch (IOException e) {
                    Log.w(TAG, "Exception while launching application", e);
                }

                // Ask the device to connect to this sender's websocket
                attachMediaPlayer();

                // Mute phone audio
                PlaybackProxy.setPhonePlayerMuted(true);
            } else {
                Log.e(TAG, "App launch error");
            }
        }
    }
}