/* * 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"); } } } }