From dbd692767b578bad0ce1d295918ab439822525fe Mon Sep 17 00:00:00 2001 From: avayvod Date: Mon, 30 Nov 2015 14:10:19 -0800 Subject: [PATCH] [Cast, Presentation API, Android] Reply to the STOP command if the application is being stopped already. BUG=562672 Sometimes the client sends the STOP command while the Cast application has disconnected but Chrome is still updating its state. In this case, respond to the client with the success message and matching sequence number. Review URL: https://codereview.chromium.org/1479303002 Cr-Commit-Position: refs/heads/master@{#362233} --- .../router/cast/CastRouteController.java | 145 +++++++++++++----- 1 file changed, 109 insertions(+), 36 deletions(-) diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/router/cast/CastRouteController.java b/chrome/android/java/src/org/chromium/chrome/browser/media/router/cast/CastRouteController.java index a472afb420313..46d7967cb0ec5 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/media/router/cast/CastRouteController.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/media/router/cast/CastRouteController.java @@ -6,7 +6,8 @@ import android.content.Context; import android.os.Handler; -import android.util.SparseIntArray; +import android.support.v4.util.ArrayMap; +import android.util.SparseArray; import com.google.android.gms.cast.ApplicationMetadata; import com.google.android.gms.cast.Cast; @@ -86,6 +87,16 @@ public class CastRouteController implements RouteController, MediaNotificationLi // The value is borrowed from the Android Cast SDK code to match their behavior. private static final double MIN_VOLUME_LEVEL_DELTA = 1e-7; + private static class RequestRecord { + public final String clientId; + public final int sequenceNumber; + + public RequestRecord(String clientId, int sequenceNumber) { + this.clientId = clientId; + this.sequenceNumber = sequenceNumber; + } + } + private static class CastMessagingChannel implements Cast.MessageReceivedCallback { private final CastRouteController mSession; @@ -98,23 +109,23 @@ public void onMessageReceived(CastDevice castDevice, String namespace, String me Log.d(TAG, "Received message from Cast device: namespace=\"" + namespace + "\" message=\"" + message + "\""); - int sequenceNumber = INVALID_SEQUENCE_NUMBER; + RequestRecord request = null; try { JSONObject jsonMessage = new JSONObject(message); int requestId = jsonMessage.getInt("requestId"); if (mSession.mRequests.indexOfKey(requestId) >= 0) { - sequenceNumber = mSession.mRequests.get(requestId); + request = mSession.mRequests.get(requestId); mSession.mRequests.delete(requestId); } } catch (JSONException e) { } if (MEDIA_NAMESPACE.equals(namespace)) { - mSession.onMediaMessage(message, sequenceNumber); + mSession.onMediaMessage(message, request); return; } - mSession.onAppMessage(message, namespace, sequenceNumber); + mSession.onAppMessage(message, namespace, request); } } @@ -161,8 +172,9 @@ private static void removeNullFields(Object object) throws JSONException { private MediaNotificationInfo.Builder mNotificationBuilder; private RemoteMediaPlayer mMediaPlayer; - private SparseIntArray mRequests; - private Queue mVolumeRequestSequenceNumbers = new ArrayDeque(); + private SparseArray mRequests; + private Queue mVolumeRequests; + private ArrayMap> mStopRequests; private Handler mHandler; @@ -197,7 +209,9 @@ public CastRouteController( mApplicationMetadata = metadata; mApplicationStatus = applicationStatus; mCastDevice = castDevice; - mRequests = new SparseIntArray(); + mRequests = new SparseArray(); + mVolumeRequests = new ArrayDeque(); + mStopRequests = new ArrayMap>(); mHandler = new Handler(); mMessageChannel = new CastMessagingChannel(this); @@ -266,7 +280,7 @@ public String getSessionId() { @Override public void close() { - stopApplication(INVALID_SEQUENCE_NUMBER); + stopApplication(); } @Override @@ -326,42 +340,68 @@ public void onPause(int actionSource) { @Override public void onStop(int actionSource) { - stopApplication(INVALID_SEQUENCE_NUMBER); + stopApplication(); } /** * Forwards the media message to the page via the media router. + * The MEDIA_STATUS message needs to be sent to all the clients. * @param message The media that's being send by the receiver. - * @param sequenceNumber The sequence number of the message this one is responding to. + * @param request The information about the client and the sequence number to respond with. */ - public void onMediaMessage(String message, int sequenceNumber) { + public void onMediaMessage(String message, RequestRecord request) { if (mMediaPlayer != null) { mMediaPlayer.onMessageReceived(mCastDevice, MEDIA_NAMESPACE, message); } - sendMessageToClients("v2_message", message, sequenceNumber); + if (isMediaStatusMessage(message)) { + // MEDIA_STATUS needs to be sent to all the clients. + for (String clientId : mClients) { + if (request != null && clientId.equals(request.clientId)) continue; + + sendClientMessageTo( + clientId, "v2_message", message, INVALID_SEQUENCE_NUMBER); + } + } + if (request != null) { + sendClientMessageTo(request.clientId, "v2_message", message, request.sequenceNumber); + } } /** * Forwards the application specific message to the page via the media router. * @param message The message within the namespace that's being send by the receiver. * @param namespace The application specific namespace this message belongs to. - * @param sequenceNumber The sequence number of the message this one is responding to. + * @param request The information about the client and the sequence number to respond with. */ - public void onAppMessage(String message, String namespace, int sequenceNumber) { + public void onAppMessage(String message, String namespace, RequestRecord request) { try { JSONObject jsonMessage = new JSONObject(); jsonMessage.put("sessionId", mSessionId); jsonMessage.put("namespaceName", namespace); jsonMessage.put("message", message); - sendMessageToClients("app_message", jsonMessage.toString(), sequenceNumber); + if (request != null) { + sendClientMessageTo(request.clientId, "app_message", + jsonMessage.toString(), request.sequenceNumber); + } else { + broadcastClientMessage("app_message", jsonMessage.toString()); + } } catch (JSONException e) { Log.e(TAG, "Failed to create the message wrapper", e); } } - private void stopApplication(final int sequenceNumber) { + private boolean isMediaStatusMessage(String message) { + try { + JSONObject jsonMessage = new JSONObject(message); + return "MEDIA_STATUS".equals(jsonMessage.getString("type")); + } catch (JSONException e) { + return false; + } + } + + private void stopApplication() { if (mStoppingApplication) return; if (isApiClientInvalid()) return; @@ -371,7 +411,20 @@ private void stopApplication(final int sequenceNumber) { .setResultCallback(new ResultCallback() { @Override public void onResult(Status status) { - sendMessageToClients("remove_session", mSessionId, sequenceNumber); + for (String clientId : mClients) { + Queue sequenceNumbersForClient = mStopRequests.get(clientId); + if (sequenceNumbersForClient == null) { + sendClientMessageTo(clientId, "remove_session", mSessionId, + INVALID_SEQUENCE_NUMBER); + continue; + } + + for (int sequenceNumber : sequenceNumbersForClient) { + sendClientMessageTo( + clientId, "remove_session", mSessionId, sequenceNumber); + } + mStopRequests.remove(clientId); + } // TODO(avayvod): handle a failure to stop the application. // https://crbug.com/535577 @@ -517,12 +570,13 @@ private boolean handleCastV2Message(JSONObject jsonMessage) int sequenceNumber = jsonMessage.optInt("sequenceNumber", INVALID_SEQUENCE_NUMBER); if ("STOP".equals(messageType)) { - stopApplication(sequenceNumber); + handleStopMessage(clientId, sequenceNumber); return true; } if ("SET_VOLUME".equals(messageType)) { - return handleVolumeMessage(jsonCastMessage.getJSONObject("volume"), sequenceNumber); + return handleVolumeMessage( + jsonCastMessage.getJSONObject("volume"), clientId, sequenceNumber); } if (Arrays.asList(MEDIA_MESSAGE_TYPES).contains(messageType)) { @@ -530,12 +584,23 @@ private boolean handleCastV2Message(JSONObject jsonMessage) messageType = sMediaOverloadedMessageTypes.get(messageType); jsonCastMessage.put("type", messageType); } - return sendCastMessage(jsonCastMessage, MEDIA_NAMESPACE, sequenceNumber); + return sendCastMessage(jsonCastMessage, MEDIA_NAMESPACE, clientId, sequenceNumber); } return true; } + private void handleStopMessage(String clientId, int sequenceNumber) { + Queue sequenceNumbersForClient = mStopRequests.get(clientId); + if (sequenceNumbersForClient == null) { + sequenceNumbersForClient = new ArrayDeque(); + mStopRequests.put(clientId, sequenceNumbersForClient); + } + sequenceNumbersForClient.add(sequenceNumber); + + stopApplication(); + } + // SET_VOLUME messages have a |level| and |muted| properties. One of them is // |null| and the other one isn't. |muted| is expected to be a boolean while // |level| is a float from 0.0 to 1.0. @@ -546,7 +611,8 @@ private boolean handleCastV2Message(JSONObject jsonMessage) // "muted": null // } // } - private boolean handleVolumeMessage(JSONObject volume, final int sequenceNumber) + private boolean handleVolumeMessage( + JSONObject volume, final String clientId, final int sequenceNumber) throws JSONException { if (volume == null) return false; @@ -584,14 +650,14 @@ private boolean handleVolumeMessage(JSONObject volume, final int sequenceNumber) // Android SDK when the status update is received so we respond to the volume message // immediately. if (waitForVolumeChange) { - mVolumeRequestSequenceNumbers.add(sequenceNumber); + mVolumeRequests.add(new RequestRecord(clientId, sequenceNumber)); } else { // It's usually bad to have request and response on the same call stack so post the // response to the Android message loop. mHandler.post(new Runnable() { @Override public void run() { - sendMessageToClients("v2_message", null, sequenceNumber); + sendClientMessageTo(clientId, "v2_message", null, sequenceNumber); } }); } @@ -630,12 +696,13 @@ private boolean handleAppMessage(JSONObject jsonMessage) throws JSONException { if (!mNamespaces.contains(namespaceName)) addNamespace(namespaceName); int sequenceNumber = jsonMessage.optInt("sequenceNumber", INVALID_SEQUENCE_NUMBER); - return sendCastMessage(actualMessage, namespaceName, sequenceNumber); + return sendCastMessage(actualMessage, namespaceName, clientId, sequenceNumber); } private boolean sendCastMessage( JSONObject message, final String namespace, + final String clientId, final int sequenceNumber) throws JSONException { if (isApiClientInvalid()) return false; @@ -651,7 +718,7 @@ private boolean sendCastMessage( requestId = CastRequestIdGenerator.getNextRequestId(); message.put("requestId", requestId); } - mRequests.append(requestId, sequenceNumber); + mRequests.append(requestId, new RequestRecord(clientId, sequenceNumber)); } Log.d(TAG, "Sending message to Cast device in namespace %s: %s", namespace, message); @@ -674,7 +741,8 @@ public void onResult(Status result) { // App messages wait for the empty message with the sequence // number. - sendMessageToClients("app_message", null, sequenceNumber); + sendClientMessageTo( + clientId, "app_message", null, sequenceNumber); } }); } catch (Exception e) { @@ -745,7 +813,7 @@ public void updateSessionStatus() { mApplicationStatus = Cast.CastApi.getApplicationStatus(mApiClient); mApplicationMetadata = Cast.CastApi.getApplicationMetadata(mApiClient); - sendMessageToClients("update_session", buildSessionMessage(), INVALID_SEQUENCE_NUMBER); + broadcastClientMessage("update_session", buildSessionMessage()); } catch (IllegalStateException e) { Log.e(TAG, "Can't get application status", e); } @@ -754,12 +822,12 @@ public void updateSessionStatus() { public void onVolumeChanged() { updateSessionStatus(); - if (mVolumeRequestSequenceNumbers.isEmpty()) return; + if (mVolumeRequests.isEmpty()) return; - for (int sequenceNumber : mVolumeRequestSequenceNumbers) { - sendMessageToClients("v2_message", null, sequenceNumber); + for (RequestRecord r : mVolumeRequests) { + sendClientMessageTo(r.clientId, "v2_message", null, r.sequenceNumber); } - mVolumeRequestSequenceNumbers.clear(); + mVolumeRequests.clear(); } private String buildSessionMessage() { @@ -799,13 +867,18 @@ private String buildSessionMessage() { } } - private void sendMessageToClients(String type, String message, int sequenceNumber) { - for (String client : mClients) { - mRouteDelegate.onMessage(mMediaRouteId, - buildInternalMessage(type, message, client, sequenceNumber)); + private void broadcastClientMessage(String type, String message) { + for (String clientId : mClients) { + sendClientMessageTo(clientId, type, message, INVALID_SEQUENCE_NUMBER); } } + private void sendClientMessageTo( + String clientId, String type, String message, int sequenceNumber) { + mRouteDelegate.onMessage(mMediaRouteId, + buildInternalMessage(type, message, clientId, sequenceNumber)); + } + private JSONArray getCapabilities(CastDevice device) { JSONArray jsonCapabilities = new JSONArray(); if (device.hasCapability(CastDevice.CAPABILITY_AUDIO_IN)) {