Skip to content
This repository has been archived by the owner on Apr 3, 2020. It is now read-only.

Commit

Permalink
[Cast, Presentation API, Android] Reply to the STOP command if the ap…
Browse files Browse the repository at this point in the history
…plication 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}
  • Loading branch information
avayvod authored and Commit bot committed Nov 30, 2015
1 parent 8dfd114 commit dbd6927
Showing 1 changed file with 109 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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);
}
}

Expand Down Expand Up @@ -161,8 +172,9 @@ private static void removeNullFields(Object object) throws JSONException {
private MediaNotificationInfo.Builder mNotificationBuilder;
private RemoteMediaPlayer mMediaPlayer;

private SparseIntArray mRequests;
private Queue<Integer> mVolumeRequestSequenceNumbers = new ArrayDeque<Integer>();
private SparseArray<RequestRecord> mRequests;
private Queue<RequestRecord> mVolumeRequests;
private ArrayMap<String, Queue<Integer>> mStopRequests;

private Handler mHandler;

Expand Down Expand Up @@ -197,7 +209,9 @@ public CastRouteController(
mApplicationMetadata = metadata;
mApplicationStatus = applicationStatus;
mCastDevice = castDevice;
mRequests = new SparseIntArray();
mRequests = new SparseArray<RequestRecord>();
mVolumeRequests = new ArrayDeque<RequestRecord>();
mStopRequests = new ArrayMap<String, Queue<Integer>>();
mHandler = new Handler();

mMessageChannel = new CastMessagingChannel(this);
Expand Down Expand Up @@ -266,7 +280,7 @@ public String getSessionId() {

@Override
public void close() {
stopApplication(INVALID_SEQUENCE_NUMBER);
stopApplication();
}

@Override
Expand Down Expand Up @@ -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;
Expand All @@ -371,7 +411,20 @@ private void stopApplication(final int sequenceNumber) {
.setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status status) {
sendMessageToClients("remove_session", mSessionId, sequenceNumber);
for (String clientId : mClients) {
Queue<Integer> 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
Expand Down Expand Up @@ -517,25 +570,37 @@ 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)) {
if (sMediaOverloadedMessageTypes.containsKey(messageType)) {
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<Integer> sequenceNumbersForClient = mStopRequests.get(clientId);
if (sequenceNumbersForClient == null) {
sequenceNumbersForClient = new ArrayDeque<Integer>();
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.
Expand All @@ -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;

Expand Down Expand Up @@ -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);
}
});
}
Expand Down Expand Up @@ -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;

Expand All @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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() {
Expand Down Expand Up @@ -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)) {
Expand Down

0 comments on commit dbd6927

Please sign in to comment.