Home | History | Annotate | Download | only in chromoting
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 package org.chromium.chromoting;
      6 
      7 import android.app.Activity;
      8 import android.content.Context;
      9 import android.os.Bundle;
     10 import android.support.v4.view.MenuItemCompat;
     11 import android.support.v7.app.MediaRouteActionProvider;
     12 import android.support.v7.media.MediaRouteSelector;
     13 import android.support.v7.media.MediaRouter;
     14 import android.support.v7.media.MediaRouter.RouteInfo;
     15 import android.util.Log;
     16 import android.view.Menu;
     17 import android.view.MenuItem;
     18 import android.widget.Toast;
     19 
     20 import com.google.android.gms.cast.Cast;
     21 import com.google.android.gms.cast.Cast.Listener;
     22 import com.google.android.gms.cast.CastDevice;
     23 import com.google.android.gms.cast.CastMediaControlIntent;
     24 import com.google.android.gms.cast.CastStatusCodes;
     25 import com.google.android.gms.common.ConnectionResult;
     26 import com.google.android.gms.common.api.GoogleApiClient;
     27 import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
     28 import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;
     29 import com.google.android.gms.common.api.ResultCallback;
     30 import com.google.android.gms.common.api.Status;
     31 
     32 import org.chromium.chromoting.jni.JniInterface;
     33 
     34 import java.io.IOException;
     35 import java.util.ArrayList;
     36 import java.util.List;
     37 
     38 /**
     39  * A handler that interacts with the Cast Extension of the Chromoting host using extension messages.
     40  * It uses the Cast Android Sender API to start our registered Cast Receiver App on a nearby Cast
     41  * device, if the user chooses to do so.
     42  */
     43 public class CastExtensionHandler implements ClientExtension, ActivityLifecycleListener {
     44 
     45     /** Extension messages of this type will be handled by the CastExtensionHandler. */
     46     public static final String EXTENSION_MSG_TYPE = "cast_message";
     47 
     48     /** Tag used for logging. */
     49     private static final String TAG = "CastExtensionHandler";
     50 
     51     /** Application Id of the Cast Receiver App that will be run on the Cast device. */
     52     private static final String RECEIVER_APP_ID = "8A1211E3";
     53 
     54     /**
     55      * Custom namespace that will be used to communicate with the Cast device.
     56      * TODO(aiguha): Use com.google.chromeremotedesktop for official builds.
     57      */
     58     private static final String CHROMOTOCAST_NAMESPACE = "urn:x-cast:com.chromoting.cast.all";
     59 
     60     /** Context that wil be used to initialize the MediaRouter and the GoogleApiClient. */
     61     private Context mContext = null;
     62 
     63     /** True if the application has been launched on the Cast device. */
     64     private boolean mApplicationStarted;
     65 
     66     /** True if the client is temporarily in a disconnected state. */
     67     private boolean mWaitingForReconnect;
     68 
     69     /** Object that allows routing of media to external devices including Google Cast devices. */
     70     private MediaRouter mMediaRouter;
     71 
     72     /** Describes the capabilities of routes that the application might want to use. */
     73     private MediaRouteSelector mMediaRouteSelector;
     74 
     75     /** Cast device selected by the user. */
     76     private CastDevice mSelectedDevice;
     77 
     78     /** Object to receive callbacks about media routing changes. */
     79     private MediaRouter.Callback mMediaRouterCallback;
     80 
     81     /** Listener for events related to the connected Cast device.*/
     82     private Listener mCastClientListener;
     83 
     84     /** Object that handles Google Play Services integration. */
     85     private GoogleApiClient mApiClient;
     86 
     87     /** Callback objects for connection changes with Google Play Services. */
     88     private ConnectionCallbacks mConnectionCallbacks;
     89     private OnConnectionFailedListener mConnectionFailedListener;
     90 
     91     /** Channel for receiving messages from the Cast device. */
     92     private ChromotocastChannel mChromotocastChannel;
     93 
     94     /** Current session ID, if there is one. */
     95     private String mSessionId;
     96 
     97     /** Queue of messages that are yet to be delivered to the Receiver App. */
     98     private List<String> mChromotocastMessageQueue;
     99 
    100     /** Current status of the application, if any. */
    101     private String mApplicationStatus;
    102 
    103     /**
    104      * A callback class for receiving events about media routing.
    105      */
    106     private class CustomMediaRouterCallback extends MediaRouter.Callback {
    107         @Override
    108         public void onRouteSelected(MediaRouter router, RouteInfo info) {
    109             mSelectedDevice = CastDevice.getFromBundle(info.getExtras());
    110             connectApiClient();
    111         }
    112 
    113         @Override
    114         public void onRouteUnselected(MediaRouter router, RouteInfo info) {
    115             tearDown();
    116             mSelectedDevice = null;
    117         }
    118     }
    119 
    120     /**
    121      * A callback class for receiving the result of launching an application on the user-selected
    122      * Google Cast device.
    123      */
    124     private class ApplicationConnectionResultCallback implements
    125             ResultCallback<Cast.ApplicationConnectionResult> {
    126         @Override
    127         public void onResult(Cast.ApplicationConnectionResult result) {
    128             Status status = result.getStatus();
    129             if (!status.isSuccess()) {
    130                 tearDown();
    131                 return;
    132             }
    133 
    134             mSessionId = result.getSessionId();
    135             mApplicationStatus = result.getApplicationStatus();
    136             mApplicationStarted = result.getWasLaunched();
    137             mChromotocastChannel = new ChromotocastChannel();
    138 
    139             try {
    140                 Cast.CastApi.setMessageReceivedCallbacks(mApiClient,
    141                         mChromotocastChannel.getNamespace(), mChromotocastChannel);
    142                 sendPendingMessagesToCastDevice();
    143             } catch (IOException e) {
    144                 showToast(R.string.connection_to_cast_failed, Toast.LENGTH_SHORT);
    145                 tearDown();
    146             } catch (IllegalStateException e) {
    147                 showToast(R.string.connection_to_cast_failed, Toast.LENGTH_SHORT);
    148                 tearDown();
    149             }
    150         }
    151     }
    152 
    153     /**
    154      * A callback class for receiving events about client connections and disconnections from
    155      * Google Play Services.
    156      */
    157     private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks {
    158         @Override
    159         public void onConnected(Bundle connectionHint) {
    160             if (mWaitingForReconnect) {
    161                 mWaitingForReconnect = false;
    162                 reconnectChannels();
    163                 return;
    164             }
    165             Cast.CastApi.launchApplication(mApiClient, RECEIVER_APP_ID, false).setResultCallback(
    166                     new ApplicationConnectionResultCallback());
    167         }
    168 
    169         @Override
    170         public void onConnectionSuspended(int cause) {
    171             mWaitingForReconnect = true;
    172         }
    173     }
    174 
    175     /**
    176      * A listener for failures to connect with Google Play Services.
    177      */
    178     private class ConnectionFailedListener implements GoogleApiClient.OnConnectionFailedListener {
    179         @Override
    180         public void onConnectionFailed(ConnectionResult result) {
    181             Log.e(TAG, String.format("Google Play Service connection failed: %s", result));
    182 
    183             tearDown();
    184         }
    185 
    186     }
    187 
    188     /**
    189      * A channel for communication with the Cast device on the CHROMOTOCAST_NAMESPACE.
    190      */
    191     private class ChromotocastChannel implements Cast.MessageReceivedCallback {
    192 
    193         /**
    194          * Returns the namespace associated with this channel.
    195          */
    196         public String getNamespace() {
    197             return CHROMOTOCAST_NAMESPACE;
    198         }
    199 
    200         @Override
    201         public void onMessageReceived(CastDevice castDevice, String namespace, String message) {
    202             if (namespace.equals(CHROMOTOCAST_NAMESPACE)) {
    203                 sendMessageToHost(message);
    204             }
    205         }
    206     }
    207 
    208     /**
    209      * A listener for changes when connected to a Google Cast device.
    210      */
    211     private class CastClientListener extends Cast.Listener {
    212         @Override
    213         public void onApplicationStatusChanged() {
    214             try {
    215                 if (mApiClient != null) {
    216                     mApplicationStatus = Cast.CastApi.getApplicationStatus(mApiClient);
    217                 }
    218             } catch (IllegalStateException e) {
    219                 showToast(R.string.connection_to_cast_failed, Toast.LENGTH_SHORT);
    220                 tearDown();
    221             }
    222         }
    223 
    224         @Override
    225         public void onVolumeChanged() {}  // Changes in volume do not affect us.
    226 
    227         @Override
    228         public void onApplicationDisconnected(int errorCode) {
    229             if (errorCode != CastStatusCodes.SUCCESS) {
    230                 Log.e(TAG, String.format("Application disconnected with: %d", errorCode));
    231             }
    232             tearDown();
    233         }
    234     }
    235 
    236     /**
    237      * Constructs a CastExtensionHandler with an empty message queue.
    238      */
    239     public CastExtensionHandler() {
    240         mChromotocastMessageQueue = new ArrayList<String>();
    241     }
    242 
    243     //
    244     // ClientExtension implementation.
    245     //
    246 
    247     @Override
    248     public String getCapability() {
    249         return Capabilities.CAST_CAPABILITY;
    250     }
    251 
    252     @Override
    253     public boolean onExtensionMessage(String type, String data) {
    254         if (type.equals(EXTENSION_MSG_TYPE)) {
    255             mChromotocastMessageQueue.add(data);
    256             if (mApplicationStarted) {
    257                 sendPendingMessagesToCastDevice();
    258             }
    259             return true;
    260         }
    261         return false;
    262     }
    263 
    264     @Override
    265     public ActivityLifecycleListener onActivityAcceptingListener(Activity activity) {
    266         return this;
    267     }
    268 
    269     //
    270     // ActivityLifecycleListener implementation.
    271     //
    272 
    273     /** Initializes the MediaRouter and related objects using the provided activity Context. */
    274     @Override
    275     public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    276         if (activity == null) {
    277             return;
    278         }
    279         mContext = activity;
    280         mMediaRouter = MediaRouter.getInstance(activity);
    281         mMediaRouteSelector = new MediaRouteSelector.Builder()
    282                 .addControlCategory(CastMediaControlIntent.categoryForCast(RECEIVER_APP_ID))
    283                 .build();
    284         mMediaRouterCallback = new CustomMediaRouterCallback();
    285     }
    286 
    287     @Override
    288     public void onActivityDestroyed(Activity activity) {
    289         tearDown();
    290     }
    291 
    292     @Override
    293     public void onActivityPaused(Activity activity) {
    294         removeMediaRouterCallback();
    295     }
    296 
    297     @Override
    298     public void onActivityResumed(Activity activity) {
    299         addMediaRouterCallback();
    300     }
    301 
    302     @Override
    303     public void onActivitySaveInstanceState (Activity activity, Bundle outState) {}
    304 
    305     @Override
    306     public void onActivityStarted(Activity activity) {
    307         addMediaRouterCallback();
    308     }
    309 
    310     @Override
    311     public void onActivityStopped(Activity activity) {
    312         removeMediaRouterCallback();
    313     }
    314 
    315     @Override
    316     public boolean onActivityCreatedOptionsMenu(Activity activity, Menu menu) {
    317         // Find the cast button in the menu.
    318         MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item);
    319         if (mediaRouteMenuItem == null) {
    320             return false;
    321         }
    322 
    323         // Setup a MediaRouteActionProvider using the button.
    324         MediaRouteActionProvider mediaRouteActionProvider =
    325                 (MediaRouteActionProvider) MenuItemCompat.getActionProvider(mediaRouteMenuItem);
    326         mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector);
    327 
    328         return true;
    329     }
    330 
    331     @Override
    332     public boolean onActivityOptionsItemSelected(Activity activity, MenuItem item) {
    333         if (item.getItemId() == R.id.actionbar_disconnect) {
    334             removeMediaRouterCallback();
    335             showToast(R.string.connection_to_cast_closed, Toast.LENGTH_SHORT);
    336             tearDown();
    337             return true;
    338         }
    339         return false;
    340     }
    341 
    342     //
    343     // Extension Message Handling logic
    344     //
    345 
    346     /** Sends a message to the Chromoting host. */
    347     private void sendMessageToHost(String data) {
    348         JniInterface.sendExtensionMessage(EXTENSION_MSG_TYPE, data);
    349     }
    350 
    351     /** Sends any messages in the message queue to the Cast device. */
    352     private void sendPendingMessagesToCastDevice() {
    353         for (String msg : mChromotocastMessageQueue) {
    354             sendMessageToCastDevice(msg);
    355         }
    356         mChromotocastMessageQueue.clear();
    357     }
    358 
    359     //
    360     // Cast Sender API logic
    361     //
    362 
    363     /**
    364      * Initializes and connects to Google Play Services.
    365      */
    366     private void connectApiClient() {
    367         if (mContext == null) {
    368             return;
    369         }
    370         mCastClientListener = new CastClientListener();
    371         mConnectionCallbacks = new ConnectionCallbacks();
    372         mConnectionFailedListener = new ConnectionFailedListener();
    373 
    374         Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions
    375                 .builder(mSelectedDevice, mCastClientListener)
    376                 .setVerboseLoggingEnabled(true);
    377 
    378         mApiClient = new GoogleApiClient.Builder(mContext)
    379                 .addApi(Cast.API, apiOptionsBuilder.build())
    380                 .addConnectionCallbacks(mConnectionCallbacks)
    381                 .addOnConnectionFailedListener(mConnectionFailedListener)
    382                 .build();
    383         mApiClient.connect();
    384     }
    385 
    386     /**
    387      * Adds the callback object to the MediaRouter. Called when the owning activity starts/resumes.
    388      */
    389     private void addMediaRouterCallback() {
    390         if (mMediaRouter != null && mMediaRouteSelector != null && mMediaRouterCallback != null) {
    391             mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback,
    392                     MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
    393         }
    394     }
    395 
    396     /**
    397      * Removes the callback object from the MediaRouter. Called when the owning activity
    398      * stops/pauses.
    399      */
    400     private void removeMediaRouterCallback() {
    401         if (mMediaRouter != null && mMediaRouterCallback != null) {
    402             mMediaRouter.removeCallback(mMediaRouterCallback);
    403         }
    404     }
    405 
    406     /**
    407      * Sends a message to the Cast device on the CHROMOTOCAST_NAMESPACE.
    408      */
    409     private void sendMessageToCastDevice(String message) {
    410         if (mApiClient == null || mChromotocastChannel == null) {
    411             return;
    412         }
    413         Cast.CastApi.sendMessage(mApiClient, mChromotocastChannel.getNamespace(), message)
    414                 .setResultCallback(new ResultCallback<Status>() {
    415                     @Override
    416                     public void onResult(Status result) {
    417                         if (!result.isSuccess()) {
    418                             Log.e(TAG, "Failed to send message to cast device.");
    419                         }
    420                     }
    421                 });
    422 
    423     }
    424 
    425     /**
    426      * Restablishes the chromotocast message channel, so we can continue communicating with the
    427      * Google Cast device. This must be called when resuming a connection.
    428      */
    429     private void reconnectChannels() {
    430         if (mApiClient == null && mChromotocastChannel == null) {
    431             return;
    432         }
    433         try {
    434             Cast.CastApi.setMessageReceivedCallbacks(
    435                     mApiClient, mChromotocastChannel.getNamespace(), mChromotocastChannel);
    436             sendPendingMessagesToCastDevice();
    437         } catch (IOException e) {
    438             showToast(R.string.connection_to_cast_failed, Toast.LENGTH_SHORT);
    439         } catch (IllegalStateException e) {
    440             showToast(R.string.connection_to_cast_failed, Toast.LENGTH_SHORT);
    441         }
    442     }
    443 
    444     /**
    445      * Stops the running application on the Google Cast device and performs the required tearDown
    446      * sequence.
    447      */
    448     private void tearDown() {
    449         if (mApiClient != null && mApplicationStarted && mApiClient.isConnected()) {
    450             Cast.CastApi.stopApplication(mApiClient, mSessionId);
    451             if (mChromotocastChannel != null) {
    452                 try {
    453                     Cast.CastApi.removeMessageReceivedCallbacks(
    454                             mApiClient, mChromotocastChannel.getNamespace());
    455                 } catch (IOException e) {
    456                     Log.e(TAG, "Failed to remove chromotocast channel.");
    457                 }
    458             }
    459             mApiClient.disconnect();
    460         }
    461         mChromotocastChannel = null;
    462         mApplicationStarted = false;
    463         mApiClient = null;
    464         mSelectedDevice = null;
    465         mWaitingForReconnect  = false;
    466         mSessionId = null;
    467     }
    468 
    469     /**
    470      * Makes a toast using the given message and duration.
    471      */
    472     private void showToast(int messageId, int duration) {
    473         if (mContext != null) {
    474             Toast.makeText(mContext, mContext.getString(messageId), duration).show();
    475         }
    476     }
    477 }
    478