Commit 28ca7e11 authored by Zhiqiang Zhang's avatar Zhiqiang Zhang Committed by Commit Bot

[CAF MR] Implementing CafMessageHandler

This CL migrates the CastMessageHandler for CAF. There's not
much code change between CastMessageHandler and CafMessageHandler.

Bug: 711860
Change-Id: Id58454c51cd6ada50b507d750a979ffe4e248241
Reviewed-on: https://chromium-review.googlesource.com/1169920
Commit-Queue: Zhiqiang Zhang <zqzhang@chromium.org>
Reviewed-by: default avatarMounir Lamouri <mlamouri@chromium.org>
Reviewed-by: default avatarThomas Guilbert <tguilbert@chromium.org>
Cr-Commit-Position: refs/heads/master@{#583043}
parent 5991ad41
...@@ -13,9 +13,9 @@ import com.google.android.gms.common.api.GoogleApiClient; ...@@ -13,9 +13,9 @@ import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.ResultCallback;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.chrome.browser.media.router.CastSessionUtil;
import org.chromium.chrome.browser.media.router.FlingingController; import org.chromium.chrome.browser.media.router.FlingingController;
import org.chromium.chrome.browser.media.router.MediaController; import org.chromium.chrome.browser.media.router.MediaController;
import org.chromium.chrome.browser.media.router.cast.CastSessionUtil;
import org.chromium.chrome.browser.media.ui.MediaNotificationInfo; import org.chromium.chrome.browser.media.ui.MediaNotificationInfo;
import org.chromium.chrome.browser.media.ui.MediaNotificationManager; import org.chromium.chrome.browser.media.ui.MediaNotificationManager;
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
package org.chromium.chrome.browser.media.router.cast; package org.chromium.chrome.browser.media.router;
/** /**
* Returns a request id in a range that is considered fairly unique. These request ids are used to * Returns a request id in a range that is considered fairly unique. These request ids are used to
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
package org.chromium.chrome.browser.media.router.cast; package org.chromium.chrome.browser.media.router;
import com.google.android.gms.cast.CastDevice; import com.google.android.gms.cast.CastDevice;
import com.google.android.gms.cast.RemoteMediaPlayer; import com.google.android.gms.cast.RemoteMediaPlayer;
...@@ -16,6 +16,9 @@ import org.chromium.content_public.common.MediaMetadata; ...@@ -16,6 +16,9 @@ import org.chromium.content_public.common.MediaMetadata;
public class CastSessionUtil { public class CastSessionUtil {
public static final String MEDIA_NAMESPACE = "urn:x-cast:com.google.cast.media"; public static final String MEDIA_NAMESPACE = "urn:x-cast:com.google.cast.media";
// The value is borrowed from the Android Cast SDK code to match their behavior.
public static final double MIN_VOLUME_LEVEL_DELTA = 1e-7;
/** /**
* Builds a MediaMetadata from the given CastDevice and MediaPlayer, and sets it on the builder * Builds a MediaMetadata from the given CastDevice and MediaPlayer, and sets it on the builder
*/ */
......
...@@ -36,7 +36,7 @@ public abstract class CafBaseMediaRouteProvider ...@@ -36,7 +36,7 @@ public abstract class CafBaseMediaRouteProvider
private static final String TAG = "CafMR"; private static final String TAG = "CafMR";
protected static final List<MediaSink> NO_SINKS = Collections.emptyList(); protected static final List<MediaSink> NO_SINKS = Collections.emptyList();
protected final MediaRouter mAndroidMediaRouter; private final @NonNull MediaRouter mAndroidMediaRouter;
protected final MediaRouteManager mManager; protected final MediaRouteManager mManager;
protected final Map<String, DiscoveryCallback> mDiscoveryCallbacks = protected final Map<String, DiscoveryCallback> mDiscoveryCallbacks =
new HashMap<String, DiscoveryCallback>(); new HashMap<String, DiscoveryCallback>();
...@@ -87,13 +87,6 @@ public abstract class CafBaseMediaRouteProvider ...@@ -87,13 +87,6 @@ public abstract class CafBaseMediaRouteProvider
public final void startObservingMediaSinks(String sourceId) { public final void startObservingMediaSinks(String sourceId) {
Log.d(TAG, "startObservingMediaSinks: " + sourceId); Log.d(TAG, "startObservingMediaSinks: " + sourceId);
if (mAndroidMediaRouter == null) {
// If the MediaRouter API is not available, report no devices so the page doesn't even
// try to cast.
onSinksReceived(sourceId, NO_SINKS);
return;
}
MediaSource source = getSourceFromId(sourceId); MediaSource source = getSourceFromId(sourceId);
if (source == null) { if (source == null) {
// If the source is invalid or not supported by this provider, report no devices // If the source is invalid or not supported by this provider, report no devices
...@@ -134,8 +127,6 @@ public abstract class CafBaseMediaRouteProvider ...@@ -134,8 +127,6 @@ public abstract class CafBaseMediaRouteProvider
public final void stopObservingMediaSinks(String sourceId) { public final void stopObservingMediaSinks(String sourceId) {
Log.d(TAG, "startObservingMediaSinks: " + sourceId); Log.d(TAG, "startObservingMediaSinks: " + sourceId);
if (mAndroidMediaRouter == null) return;
MediaSource source = getSourceFromId(sourceId); MediaSource source = getSourceFromId(sourceId);
if (source == null) return; if (source == null) return;
...@@ -159,10 +150,6 @@ public abstract class CafBaseMediaRouteProvider ...@@ -159,10 +150,6 @@ public abstract class CafBaseMediaRouteProvider
if (mPendingCreateRouteRequestInfo != null) { if (mPendingCreateRouteRequestInfo != null) {
// TODO(zqzhang): do something. // TODO(zqzhang): do something.
} }
if (mAndroidMediaRouter == null) {
mManager.onRouteRequestError("Not supported", nativeRequestId);
return;
}
MediaSink sink = MediaSink.fromSinkId(sinkId, mAndroidMediaRouter); MediaSink sink = MediaSink.fromSinkId(sinkId, mAndroidMediaRouter);
if (sink == null) { if (sink == null) {
...@@ -222,6 +209,10 @@ public abstract class CafBaseMediaRouteProvider ...@@ -222,6 +209,10 @@ public abstract class CafBaseMediaRouteProvider
mPendingCreateRouteRequestInfo = null; mPendingCreateRouteRequestInfo = null;
} }
public @NonNull MediaRouter getAndroidMediaRouter() {
return mAndroidMediaRouter;
}
// TODO(zqzhang): this is a temporary workaround for give CafMRP to manage ClientRecords on // TODO(zqzhang): this is a temporary workaround for give CafMRP to manage ClientRecords on
// session start. This needs to be removed once ClientRecord management gets refactored. // session start. This needs to be removed once ClientRecord management gets refactored.
abstract void onSessionStarted(CreateRouteRequestInfo request); abstract void onSessionStarted(CreateRouteRequestInfo request);
......
...@@ -20,6 +20,7 @@ import org.chromium.chrome.browser.media.router.MediaSink; ...@@ -20,6 +20,7 @@ import org.chromium.chrome.browser.media.router.MediaSink;
import org.chromium.chrome.browser.media.router.MediaSource; import org.chromium.chrome.browser.media.router.MediaSource;
import org.chromium.chrome.browser.media.router.cast.CastMediaSource; import org.chromium.chrome.browser.media.router.cast.CastMediaSource;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
...@@ -37,6 +38,7 @@ public class CafMediaRouteProvider extends CafBaseMediaRouteProvider { ...@@ -37,6 +38,7 @@ public class CafMediaRouteProvider extends CafBaseMediaRouteProvider {
private ClientRecord mLastRemovedRouteRecord; private ClientRecord mLastRemovedRouteRecord;
private final Map<String, ClientRecord> mClientRecords = new HashMap<String, ClientRecord>(); private final Map<String, ClientRecord> mClientRecords = new HashMap<String, ClientRecord>();
private CafMessageHandler mMessageHandler;
public static CafMediaRouteProvider create(MediaRouteManager manager) { public static CafMediaRouteProvider create(MediaRouteManager manager) {
return new CafMediaRouteProvider(ChromeMediaRouter.getAndroidMediaRouter(), manager); return new CafMediaRouteProvider(ChromeMediaRouter.getAndroidMediaRouter(), manager);
...@@ -54,10 +56,10 @@ public class CafMediaRouteProvider extends CafBaseMediaRouteProvider { ...@@ -54,10 +56,10 @@ public class CafMediaRouteProvider extends CafBaseMediaRouteProvider {
public void requestSessionLaunch(CreateRouteRequestInfo request) { public void requestSessionLaunch(CreateRouteRequestInfo request) {
CastUtils.getCastContext().setReceiverApplicationId(request.source.getApplicationId()); CastUtils.getCastContext().setReceiverApplicationId(request.source.getApplicationId());
for (MediaRouter.RouteInfo routeInfo : mAndroidMediaRouter.getRoutes()) { for (MediaRouter.RouteInfo routeInfo : getAndroidMediaRouter().getRoutes()) {
if (routeInfo.getId().equals(request.sink.getId())) { if (routeInfo.getId().equals(request.sink.getId())) {
// Unselect and then select so that CAF will get notified of the selection. // Unselect and then select so that CAF will get notified of the selection.
mAndroidMediaRouter.unselect(0); getAndroidMediaRouter().unselect(0);
routeInfo.select(); routeInfo.select();
break; break;
} }
...@@ -100,11 +102,11 @@ public class CafMediaRouteProvider extends CafBaseMediaRouteProvider { ...@@ -100,11 +102,11 @@ public class CafMediaRouteProvider extends CafBaseMediaRouteProvider {
if (!isRouteInRecord) return; if (!isRouteInRecord) return;
ClientRecord client = getClientRecordByRouteId(routeId); ClientRecord client = getClientRecordByRouteId(routeId);
if (client != null && mAndroidMediaRouter != null) { if (client != null) {
MediaSink sink = MediaSink.fromSinkId( MediaSink sink = MediaSink.fromSinkId(
sessionController().getSink().getId(), mAndroidMediaRouter); sessionController().getSink().getId(), getAndroidMediaRouter());
if (sink != null) { if (sink != null) {
sessionController().notifyReceiverAction(routeId, sink, client.clientId, "stop"); mMessageHandler.sendReceiverActionToClient(routeId, sink, client.clientId, "stop");
} }
} }
} }
...@@ -133,6 +135,20 @@ public class CafMediaRouteProvider extends CafBaseMediaRouteProvider { ...@@ -133,6 +135,20 @@ public class CafMediaRouteProvider extends CafBaseMediaRouteProvider {
return CastMediaSource.from(sourceId); return CastMediaSource.from(sourceId);
} }
public void sendMessageToClient(String clientId, String message) {
ClientRecord clientRecord = mClientRecords.get(clientId);
if (clientRecord == null) return;
if (!clientRecord.isConnected) {
Log.d(TAG, "Queueing message to client %s: %s", clientId, message);
clientRecord.pendingMessages.add(message);
return;
}
Log.d(TAG, "Sending message to client %s: %s", clientId, message);
mManager.onMessage(clientRecord.routeId, message);
}
/////////////////////////////////////////////// ///////////////////////////////////////////////
// SessionManagerListener implementation // SessionManagerListener implementation
/////////////////////////////////////////////// ///////////////////////////////////////////////
...@@ -156,10 +172,13 @@ public class CafMediaRouteProvider extends CafBaseMediaRouteProvider { ...@@ -156,10 +172,13 @@ public class CafMediaRouteProvider extends CafBaseMediaRouteProvider {
if (clientId != null) { if (clientId != null) {
ClientRecord clientRecord = mClientRecords.get(clientId); ClientRecord clientRecord = mClientRecords.get(clientId);
if (clientRecord != null) { if (clientRecord != null) {
sessionController().notifyReceiverAction( mMessageHandler.sendReceiverActionToClient(
clientRecord.routeId, sink, clientId, "cast"); clientRecord.routeId, sink, clientId, "cast");
} }
} }
mMessageHandler.onSessionStarted(sessionController());
sessionController().getSession().getRemoteMediaClient().requestStatus();
} }
@Override @Override
...@@ -191,9 +210,7 @@ public class CafMediaRouteProvider extends CafBaseMediaRouteProvider { ...@@ -191,9 +210,7 @@ public class CafMediaRouteProvider extends CafBaseMediaRouteProvider {
} }
detachFromSession(); detachFromSession();
if (mAndroidMediaRouter != null) { getAndroidMediaRouter().selectRoute(getAndroidMediaRouter().getDefaultRoute());
mAndroidMediaRouter.selectRoute(mAndroidMediaRouter.getDefaultRoute());
}
} }
@Override @Override
...@@ -291,4 +308,12 @@ public class CafMediaRouteProvider extends CafBaseMediaRouteProvider { ...@@ -291,4 +308,12 @@ public class CafMediaRouteProvider extends CafBaseMediaRouteProvider {
return false; return false;
return originA.equals(originB); return originA.equals(originB);
} }
Collection<String> getClients() {
return mClientRecords.keySet();
}
Map<String, ClientRecord> getClientRecordss() {
return mClientRecords;
}
} }
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.media.router.caf;
import android.os.Handler;
import android.support.v4.util.ArrayMap;
import android.text.TextUtils;
import android.util.SparseArray;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.Status;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.media.router.CastRequestIdGenerator;
import org.chromium.chrome.browser.media.router.CastSessionUtil;
import org.chromium.chrome.browser.media.router.ClientRecord;
import org.chromium.chrome.browser.media.router.MediaSink;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
/**
* The handler for cast messages. It receives events between the Cast SDK and the page, process and
* dispatch the messages accordingly. The handler talks to the Cast SDK via CastSession, and
* talks to the pages via the media router.
*/
public class CafMessageHandler {
private static final String TAG = "CafMR";
// Sequence number used when no sequence number is required or was initially passed.
static final int INVALID_SEQUENCE_NUMBER = -1;
private static final String MEDIA_MESSAGE_TYPES[] = {
"PLAY", "LOAD", "PAUSE", "SEEK", "STOP_MEDIA", "MEDIA_SET_VOLUME", "MEDIA_GET_STATUS",
"EDIT_TRACKS_INFO", "QUEUE_LOAD", "QUEUE_INSERT", "QUEUE_UPDATE", "QUEUE_REMOVE",
"QUEUE_REORDER",
};
private static final String MEDIA_SUPPORTED_COMMANDS[] = {
"pause", "seek", "stream_volume", "stream_mute",
};
// Lock used to lazy initialize sMediaOverloadedMessageTypes.
private static final Object INIT_LOCK = new Object();
// Map associating types that have a different names outside of the media namespace and inside.
// In other words, some types are sent as MEDIA_FOO or FOO_MEDIA by the client by the Cast
// expect them to be named FOO. The reason being that FOO might exist in multiple namespaces
// but the client isn't aware of namespacing.
private static Map<String, String> sMediaOverloadedMessageTypes;
private SparseArray<RequestRecord> mRequests;
private ArrayMap<String, Queue<Integer>> mStopRequests;
private Queue<RequestRecord> mVolumeRequests;
// The reference to CastSession, only valid after calling {@link onSessionCreated}, and will be
// reset to null when calling {@link onApplicationStopped}.
private CastSessionController mSessionController;
private final CafMediaRouteProvider mRouteProvider;
private Handler mHandler;
/**
* The record for client requests. {@link CafMessageHandler} uses this class to manage the
* client requests and match responses to the requests.
*/
static class RequestRecord {
public final String clientId;
public final int sequenceNumber;
public RequestRecord(String clientId, int sequenceNumber) {
this.clientId = clientId;
this.sequenceNumber = sequenceNumber;
}
}
/**
* Initializes a new {@link CafMessageHandler} instance.
* @param session The {@link CastSession} for communicating with the Cast SDK.
* @param provider The {@link CafMediaRouteProvider} for communicating with the page.
*/
public CafMessageHandler(CafMediaRouteProvider provider) {
mRouteProvider = provider;
mRequests = new SparseArray<RequestRecord>();
mStopRequests = new ArrayMap<String, Queue<Integer>>();
mVolumeRequests = new ArrayDeque<RequestRecord>();
mHandler = new Handler();
synchronized (INIT_LOCK) {
if (sMediaOverloadedMessageTypes == null) {
sMediaOverloadedMessageTypes = new HashMap<String, String>();
sMediaOverloadedMessageTypes.put("STOP_MEDIA", "STOP");
sMediaOverloadedMessageTypes.put("MEDIA_SET_VOLUME", "SET_VOLUME");
sMediaOverloadedMessageTypes.put("MEDIA_GET_STATUS", "GET_STATUS");
}
}
}
@VisibleForTesting
static String[] getMediaMessageTypesForTest() {
return MEDIA_MESSAGE_TYPES;
}
@VisibleForTesting
static Map<String, String> getMediaOverloadedMessageTypesForTest() {
return sMediaOverloadedMessageTypes;
}
@VisibleForTesting
SparseArray<RequestRecord> getRequestsForTest() {
return mRequests;
}
@VisibleForTesting
Queue<RequestRecord> getVolumeRequestsForTest() {
return mVolumeRequests;
}
@VisibleForTesting
Map<String, Queue<Integer>> getStopRequestsForTest() {
return mStopRequests;
}
/**
* Set the session when a session is started, and notify all clients that are not connected.
* @param session The newly created session.
*/
public void onSessionStarted(CastSessionController sessionController) {
mSessionController = sessionController;
for (ClientRecord client : mRouteProvider.getClientRecords().values()) {
if (!client.isConnected) continue;
sendEnclosedMessageToClient(
client.clientId, "new_session", buildSessionMessage(), INVALID_SEQUENCE_NUMBER);
}
// Register namespace.
}
/////////////////////////////////////////////////////////////////////////////////////////////
// Functions for handling messages from the page to the Cast device.
/**
* Handles messages related to the cast session, i.e. messages happening on a established
* connection. All these messages are sent from the page to the Cast SDK.
* @param message The JSONObject message to be handled.
*/
public boolean handleSessionMessageFromClient(JSONObject message) throws JSONException {
String messageType = message.getString("type");
if ("v2_message".equals(messageType)) {
return handleCastV2MessageFromClient(message);
} else if ("app_message".equals(messageType)) {
return handleAppMessageFromClient(message);
} else {
Log.e(TAG, "Unsupported message: %s", message);
return false;
}
}
// An example of the Cast V2 message:
// {
// "type": "v2_message",
// "message": {
// "type": "...",
// ...
// },
// "sequenceNumber": 0,
// "timeoutMillis": 0,
// "clientId": "144042901280235697"
// }
@VisibleForTesting
boolean handleCastV2MessageFromClient(JSONObject jsonMessage) throws JSONException {
assert "v2_message".equals(jsonMessage.getString("type"));
final String clientId = jsonMessage.getString("clientId");
if (clientId == null || !mRouteProvider.getClients().contains(clientId)) return false;
JSONObject jsonCastMessage = jsonMessage.getJSONObject("message");
String messageType = jsonCastMessage.getString("type");
final int sequenceNumber = jsonMessage.optInt("sequenceNumber", INVALID_SEQUENCE_NUMBER);
if ("STOP".equals(messageType)) {
handleStopMessage(clientId, sequenceNumber);
return true;
}
if ("SET_VOLUME".equals(messageType)) {
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 sendJsonCastMessage(
jsonCastMessage, CastSessionUtil.MEDIA_NAMESPACE, clientId, sequenceNumber);
}
return true;
}
boolean handleVolumeMessage(JSONObject volumeMessage, final String clientId,
final int sequenceNumber) throws JSONException {
if (volumeMessage == null) return false;
if (!mSessionController.isConnected()) return false;
boolean shouldWaitForVolumeChange = false;
try {
if (!volumeMessage.isNull("muted")) {
boolean newMuted = volumeMessage.getBoolean("muted");
if (mSessionController.getSession().isMute() != newMuted) {
mSessionController.getSession().setMute(newMuted);
shouldWaitForVolumeChange = true;
}
}
if (!volumeMessage.isNull("level")) {
double newLevel = volumeMessage.getDouble("level");
double currentLevel = mSessionController.getSession().getVolume();
if (!Double.isNaN(currentLevel)
&& Math.abs(currentLevel - newLevel)
> CastSessionUtil.MIN_VOLUME_LEVEL_DELTA) {
mSessionController.getSession().setVolume(newLevel);
shouldWaitForVolumeChange = true;
}
}
} catch (IOException | IllegalStateException e) {
Log.e(TAG, "Failed to send volume command: " + e);
return false;
}
// For each successful volume message we need to respond with an empty "v2_message" so the
// Cast Web SDK can call the success callback of the page. If we expect the volume to change
// as the result of the command, we're relying on {@link Cast.CastListener#onVolumeChanged}
// to get called by the Android Cast SDK when the receiver status is updated. We keep the
// sequence number until then. If the volume doesn't change as the result of the command, we
// won't get notified by the Android SDK
if (shouldWaitForVolumeChange) {
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() {
onVolumeChanged(clientId, sequenceNumber);
}
});
}
return true;
}
@VisibleForTesting
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);
mSessionController.endSession();
}
// An example of the Cast application message:
// {
// "type":"app_message",
// "message": {
// "sessionId":"...",
// "namespaceName":"...",
// "message": ...
// },
// "sequenceNumber":0,
// "timeoutMillis":3000,
// "clientId":"14417311915272175"
// }
@VisibleForTesting
boolean handleAppMessageFromClient(JSONObject jsonMessage) throws JSONException {
assert "app_message".equals(jsonMessage.getString("type"));
String clientId = jsonMessage.getString("clientId");
if (clientId == null || !mRouteProvider.getClients().contains(clientId)) return false;
JSONObject jsonAppMessageWrapper = jsonMessage.getJSONObject("message");
if (!mSessionController.getSession().getSessionId().equals(
jsonAppMessageWrapper.getString("sessionId"))) {
return false;
}
String namespaceName = jsonAppMessageWrapper.getString("namespaceName");
if (namespaceName == null || namespaceName.isEmpty()) return false;
if (!mSessionController.getNamespaces().contains(namespaceName)) return false;
int sequenceNumber = jsonMessage.optInt("sequenceNumber", INVALID_SEQUENCE_NUMBER);
Object actualMessageObject = jsonAppMessageWrapper.get("message");
if (actualMessageObject == null) return false;
if (actualMessageObject instanceof String) {
String actualMessage = jsonAppMessageWrapper.getString("message");
return sendStringCastMessage(actualMessage, namespaceName, clientId, sequenceNumber);
}
JSONObject actualMessage = jsonAppMessageWrapper.getJSONObject("message");
return sendJsonCastMessage(actualMessage, namespaceName, clientId, sequenceNumber);
}
@VisibleForTesting
boolean sendJsonCastMessage(JSONObject message, final String namespace, final String clientId,
final int sequenceNumber) throws JSONException {
if (mSessionController == null || !mSessionController.isConnected()) return false;
removeNullFields(message);
// Map the request id to a valid sequence number only.
if (sequenceNumber != INVALID_SEQUENCE_NUMBER) {
// If for some reason, there is already a requestId other than 0, it
// is kept. Otherwise, one is generated. In all cases it's associated with the
// sequenceNumber passed by the client.
int requestId = message.optInt("requestId", 0);
if (requestId == 0) {
requestId = CastRequestIdGenerator.getNextRequestId();
message.put("requestId", requestId);
}
mRequests.append(requestId, new RequestRecord(clientId, sequenceNumber));
}
return sendStringCastMessage(message.toString(), namespace, clientId, sequenceNumber);
}
/////////////////////////////////////////////////////////////////////////////////////////////
// Functions for handling messages from the Cast device to the pages.
/**
* Forwards the messages from the Cast device to the clients, and perform proper actions if it
* is media message.
* @param namespace The application specific namespace this message belongs to.
* @param message The message within the namespace that's being sent by the receiver
*/
public void onMessageReceived(String namespace, String message) {
RequestRecord request = null;
try {
JSONObject jsonMessage = new JSONObject(message);
int requestId = jsonMessage.getInt("requestId");
if (mRequests.indexOfKey(requestId) >= 0) {
request = mRequests.get(requestId);
mRequests.delete(requestId);
}
} catch (JSONException e) {
}
if (CastSessionUtil.MEDIA_NAMESPACE.equals(namespace)) {
onMediaMessage(message, request);
return;
}
onAppMessage(message, namespace, request);
}
/**
* 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 request The information about the client and the sequence number to respond with.
*/
@VisibleForTesting
void onMediaMessage(String message, RequestRecord request) {
mSessionController.updateRemoteMediaClient(message);
if (isMediaStatusMessage(message)) {
// MEDIA_STATUS needs to be sent to all the clients.
for (String clientId : mRouteProvider.getClients()) {
if (request != null && clientId.equals(request.clientId)) continue;
sendEnclosedMessageToClient(
clientId, "v2_message", message, INVALID_SEQUENCE_NUMBER);
}
}
if (request != null) {
sendEnclosedMessageToClient(
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 sent by the receiver.
* @param namespace The application specific namespace this message belongs to.
* @param request The information about the client and the sequence number to respond with.
*/
@VisibleForTesting
void onAppMessage(String message, String namespace, RequestRecord request) {
try {
JSONObject jsonMessage = new JSONObject();
jsonMessage.put("sessionId", mSessionController.getSession().getSessionId());
jsonMessage.put("namespaceName", namespace);
jsonMessage.put("message", message);
if (request != null) {
sendEnclosedMessageToClient(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);
}
}
/**
* Notifies the application has stopped to all requesting clients.
*/
public void onApplicationStopped() {
for (String clientId : mRouteProvider.getClients()) {
Queue<Integer> sequenceNumbersForClient = mStopRequests.get(clientId);
if (sequenceNumbersForClient == null) {
sendEnclosedMessageToClient(clientId, "remove_session",
mSessionController.getSession().getSessionId(), INVALID_SEQUENCE_NUMBER);
continue;
}
for (int sequenceNumber : sequenceNumbersForClient) {
sendEnclosedMessageToClient(clientId, "remove_session",
mSessionController.getSession().getSessionId(), sequenceNumber);
}
mStopRequests.remove(clientId);
}
mSessionController = null;
}
/**
* When the Cast device volume really changed, updates the session status and notify all
* requesting clients.
*/
public void onVolumeChanged() {
if (mVolumeRequests.isEmpty()) return;
for (RequestRecord r : mVolumeRequests) onVolumeChanged(r.clientId, r.sequenceNumber);
mVolumeRequests.clear();
}
@VisibleForTesting
void onVolumeChanged(String clientId, int sequenceNumber) {
sendEnclosedMessageToClient(clientId, "v2_message", null, sequenceNumber);
}
/**
* Broadcasts the message to all clients.
* @param type The type of the message.
* @param message The message to broadcast.
*/
public void broadcastClientMessage(String type, String message) {
for (String clientId : mRouteProvider.getClients()) {
sendEnclosedMessageToClient(clientId, type, message, INVALID_SEQUENCE_NUMBER);
}
}
public void sendReceiverActionToClient(
String routeId, MediaSink sink, String clientId, String action) {
try {
JSONObject jsonReceiver = new JSONObject();
jsonReceiver.put("label", sink.getId());
jsonReceiver.put("friendlyName", sink.getName());
jsonReceiver.put("capabilities", toJSONArray(mSessionController.getCapabilities()));
jsonReceiver.put("volume", null);
jsonReceiver.put("isActiveInput", null);
jsonReceiver.put("displayStatus", null);
jsonReceiver.put("receiverType", "cast");
JSONObject jsonReceiverAction = new JSONObject();
jsonReceiverAction.put("receiver", jsonReceiver);
jsonReceiverAction.put("action", action);
JSONObject json = new JSONObject();
json.put("type", "receiver_action");
json.put("sequenceNumber", -1);
json.put("timeoutMillis", 0);
json.put("clientId", clientId);
json.put("message", jsonReceiverAction);
mRouteProvider.sendMessageToClient(clientId, json.toString());
} catch (JSONException e) {
Log.e(TAG, "Failed to send receiver action message", e);
}
}
/**
* Sends a message to a specific client.
* @param clientId The id of the receiving client.
* @param type The type of the message.
* @param message The message to be sent.
* @param sequenceNumber The sequence number for matching requesting and responding messages.
*/
public void sendEnclosedMessageToClient(
String clientId, String type, String message, int sequenceNumber) {
mRouteProvider.sendMessageToClient(
clientId, buildEnclosedClientMessage(type, message, clientId, sequenceNumber));
}
@VisibleForTesting
String buildEnclosedClientMessage(
String type, String message, String clientId, int sequenceNumber) {
JSONObject json = new JSONObject();
try {
json.put("type", type);
json.put("sequenceNumber", sequenceNumber);
json.put("timeoutMillis", 0);
json.put("clientId", clientId);
// TODO(mlamouri): we should have a more reliable way to handle string, null and Object
// messages.
if (message == null || "remove_session".equals(type)
|| "disconnect_session".equals(type)) {
json.put("message", message);
} else {
JSONObject jsonMessage = new JSONObject(message);
if ("v2_message".equals(type)
&& "MEDIA_STATUS".equals(jsonMessage.getString("type"))) {
sanitizeMediaStatusMessage(jsonMessage);
}
json.put("message", jsonMessage);
}
} catch (JSONException e) {
Log.e(TAG, "Failed to build the reply: " + e);
}
return json.toString();
}
/**
* @return A message containing the information of the {@link CastSession}.
*/
public String buildSessionMessage() {
if (mSessionController == null || !mSessionController.isConnected()) return "{}";
try {
// "volume" is a part of "receiver" initialized below.
JSONObject jsonVolume = new JSONObject();
jsonVolume.put("level", mSessionController.getSession().getVolume());
jsonVolume.put("muted", mSessionController.getSession().isMute());
// "receiver" is a part of "message" initialized below.
JSONObject jsonReceiver = new JSONObject();
jsonReceiver.put(
"label", mSessionController.getSession().getCastDevice().getDeviceId());
jsonReceiver.put("friendlyName",
mSessionController.getSession().getCastDevice().getFriendlyName());
jsonReceiver.put("capabilities", toJSONArray(mSessionController.getCapabilities()));
jsonReceiver.put("volume", jsonVolume);
jsonReceiver.put(
"isActiveInput", mSessionController.getSession().getActiveInputState());
jsonReceiver.put("displayStatus", null);
jsonReceiver.put("receiverType", "cast");
JSONArray jsonNamespaces = new JSONArray();
for (String namespace : mSessionController.getNamespaces()) {
JSONObject jsonNamespace = new JSONObject();
jsonNamespace.put("name", namespace);
jsonNamespaces.put(jsonNamespace);
}
JSONObject jsonMessage = new JSONObject();
jsonMessage.put("sessionId", mSessionController.getSession().getSessionId());
jsonMessage.put("statusText", mSessionController.getSession().getApplicationStatus());
jsonMessage.put("receiver", jsonReceiver);
jsonMessage.put("namespaces", jsonNamespaces);
jsonMessage.put("media", toJSONArray(new ArrayList<>()));
jsonMessage.put("status", "connected");
jsonMessage.put("transportId", "web-4");
jsonMessage.put("appId",
mSessionController.getSession().getApplicationMetadata().getApplicationId());
jsonMessage.put("displayName",
mSessionController.getSession().getCastDevice().getFriendlyName());
return jsonMessage.toString();
} catch (JSONException e) {
Log.w(TAG, "Building session message failed", e);
return "{}";
}
}
/////////////////////////////////////////////////////////////////////////////////////////////
// Utility functions
/**
* Modifies the received MediaStatus message to match the format expected by the client.
*/
private void sanitizeMediaStatusMessage(JSONObject object) throws JSONException {
object.put("sessionId", mSessionController.getSession().getSessionId());
JSONArray mediaStatus = object.getJSONArray("status");
for (int i = 0; i < mediaStatus.length(); ++i) {
JSONObject status = mediaStatus.getJSONObject(i);
status.put("sessionId", mSessionController.getSession().getSessionId());
if (!status.has("supportedMediaCommands")) continue;
JSONArray commands = new JSONArray();
int bitfieldCommands = status.getInt("supportedMediaCommands");
for (int j = 0; j < 4; ++j) {
if ((bitfieldCommands & (1 << j)) != 0) {
commands.put(MEDIA_SUPPORTED_COMMANDS[j]);
}
}
status.put("supportedMediaCommands", commands); // Removes current entry.
}
}
/**
* Remove 'null' fields from a JSONObject. This method calls itself recursively until all the
* fields have been looked at.
* TODO(mlamouri): move to some util class?
*/
private static void removeNullFields(Object object) throws JSONException {
if (object instanceof JSONArray) {
JSONArray array = (JSONArray) object;
for (int i = 0; i < array.length(); ++i) removeNullFields(array.get(i));
} else if (object instanceof JSONObject) {
JSONObject json = (JSONObject) object;
JSONArray names = json.names();
if (names == null) return;
for (int i = 0; i < names.length(); ++i) {
String key = names.getString(i);
if (json.isNull(key)) {
json.remove(key);
} else {
removeNullFields(json.get(key));
}
}
}
}
@VisibleForTesting
boolean isMediaStatusMessage(String message) {
try {
JSONObject jsonMessage = new JSONObject(message);
return "MEDIA_STATUS".equals(jsonMessage.getString("type"));
} catch (JSONException e) {
return false;
}
}
private JSONArray toJSONArray(List<String> from) throws JSONException {
JSONArray result = new JSONArray();
for (String entry : from) {
result.put(entry);
}
return result;
}
private boolean sendStringCastMessage(
String message, String namespace, String clientId, int sequenceNumber) {
if (mSessionController == null || !mSessionController.isConnected()) return false;
PendingResult<Status> pendingResult =
mSessionController.getSession().sendMessage(namespace, message);
if (!TextUtils.equals(namespace, CastSessionUtil.MEDIA_NAMESPACE)) {
// Media commands wait for the media status update as a result.
pendingResult.setResultCallback(
(Status result) -> onSendAppMessageResult(result, clientId, sequenceNumber));
}
return true;
}
/**
* Notifies a client that an app message has been sent.
* @param clientId The client id the message is sent from.
* @param sequenceNumber The sequence number of the message.
*/
private void onSendAppMessageResult(Status result, String clientId, int sequenceNumber) {
if (!result.isSuccess()) {
// TODO(avayvod): should actually report back to the page.
// See https://crbug.com/550445.
Log.e(TAG, "Failed to send the message: " + result);
return;
}
// App messages wait for the empty message with the sequence
// number.
sendEnclosedMessageToClient(clientId, "app_message", null, sequenceNumber);
}
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.media.router.caf;
/**
* The handler for cast messages. It receives events between the Cast SDK and the page, process and
* dispatch the messages accordingly. The handler talks to the Cast SDK via CastSession, and
* talks to the pages via the media router.
*/
public class CastMessageHandler {
// Sequence number used when no sequence number is required or was initially passed.
static final int INVALID_SEQUENCE_NUMBER = -1;
}
...@@ -4,11 +4,18 @@ ...@@ -4,11 +4,18 @@
package org.chromium.chrome.browser.media.router.caf; package org.chromium.chrome.browser.media.router.caf;
import android.support.v7.media.MediaRouter;
import com.google.android.gms.cast.CastDevice;
import com.google.android.gms.cast.framework.CastSession; import com.google.android.gms.cast.framework.CastSession;
import org.chromium.chrome.browser.media.router.CastSessionUtil;
import org.chromium.chrome.browser.media.router.MediaSink; import org.chromium.chrome.browser.media.router.MediaSink;
import org.chromium.chrome.browser.media.router.MediaSource; import org.chromium.chrome.browser.media.router.MediaSource;
import java.util.ArrayList;
import java.util.List;
/** /**
* A wrapper for {@link CastSession}, extending its functionality for Chrome MediaRouter. * A wrapper for {@link CastSession}, extending its functionality for Chrome MediaRouter.
* *
...@@ -43,19 +50,46 @@ public class CastSessionController { ...@@ -43,19 +50,46 @@ public class CastSessionController {
} }
public void endSession() { public void endSession() {
CastSession currentCastSession = MediaRouter mediaRouter = mProvider.getAndroidMediaRouter();
CastUtils.getCastContext().getSessionManager().getCurrentCastSession(); mediaRouter.selectRoute(mediaRouter.getDefaultRoute());
if (currentCastSession == mCastSession) { }
CastUtils.getCastContext().getSessionManager().endCurrentSession(true);
public List<String> getNamespaces() {
// Not implemented.
return new ArrayList<>();
}
public List<String> getCapabilities() {
List<String> capabilities = new ArrayList<>();
if (mCastSession == null || !mCastSession.isConnected()) return capabilities;
CastDevice device = mCastSession.getCastDevice();
if (device.hasCapability(CastDevice.CAPABILITY_AUDIO_IN)) {
capabilities.add("audio_in");
} }
if (device.hasCapability(CastDevice.CAPABILITY_AUDIO_OUT)) {
capabilities.add("audio_out");
}
if (device.hasCapability(CastDevice.CAPABILITY_VIDEO_IN)) {
capabilities.add("video_in");
}
if (device.hasCapability(CastDevice.CAPABILITY_VIDEO_OUT)) {
capabilities.add("video_out");
}
return capabilities;
} }
public void onSessionStarted() { public void onSessionStarted() {
// Not implemented. // Not implemented.
} }
public void notifyReceiverAction( public boolean isConnected() {
String routeId, MediaSink sink, String clientId, String action) { return mCastSession != null && mCastSession.isConnected();
// Not implemented. }
public void updateRemoteMediaClient(String message) {
if (!isConnected()) return;
mCastSession.getRemoteMediaClient().onMessageReceived(
mCastSession.getCastDevice(), CastSessionUtil.MEDIA_NAMESPACE, message);
} }
} }
...@@ -394,7 +394,7 @@ public class CastMediaRouteProvider extends BaseMediaRouteProvider { ...@@ -394,7 +394,7 @@ public class CastMediaRouteProvider extends BaseMediaRouteProvider {
tabId)); tabId));
} }
// TODO(zqzhang): Move this method to CastMessageHandler. // Migrated to CastMessageHandler.sendReceiverActionToClient. See https://crbug.com/711860.
private void sendReceiverAction( private void sendReceiverAction(
String routeId, MediaSink sink, String clientId, String action) { String routeId, MediaSink sink, String clientId, String action) {
try { try {
......
...@@ -14,6 +14,8 @@ import org.json.JSONObject; ...@@ -14,6 +14,8 @@ import org.json.JSONObject;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting; import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.media.router.CastRequestIdGenerator;
import org.chromium.chrome.browser.media.router.CastSessionUtil;
import org.chromium.chrome.browser.media.router.ClientRecord; import org.chromium.chrome.browser.media.router.ClientRecord;
import java.util.ArrayDeque; import java.util.ArrayDeque;
...@@ -28,6 +30,7 @@ import java.util.Queue; ...@@ -28,6 +30,7 @@ import java.util.Queue;
* dispatch the messages accordingly. The handler talks to the Cast SDK via CastSession, and * dispatch the messages accordingly. The handler talks to the Cast SDK via CastSession, and
* talks to the pages via the media router. * talks to the pages via the media router.
*/ */
// Migrated to CafMessageHandler. See https://crbug.com/711860.
public class CastMessageHandler { public class CastMessageHandler {
private static final String TAG = "MediaRouter"; private static final String TAG = "MediaRouter";
...@@ -199,8 +202,7 @@ public class CastMessageHandler { ...@@ -199,8 +202,7 @@ public class CastMessageHandler {
} }
if ("SET_VOLUME".equals(messageType)) { if ("SET_VOLUME".equals(messageType)) {
CastSession.HandleVolumeMessageResult result = CastSession.HandleVolumeMessageResult result = mSession.handleVolumeMessage(
mSession.handleVolumeMessage(
jsonCastMessage.getJSONObject("volume"), clientId, sequenceNumber); jsonCastMessage.getJSONObject("volume"), clientId, sequenceNumber);
if (!result.mSucceeded) return false; if (!result.mSucceeded) return false;
......
...@@ -20,6 +20,7 @@ import org.json.JSONObject; ...@@ -20,6 +20,7 @@ import org.json.JSONObject;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.chrome.R; import org.chromium.chrome.R;
import org.chromium.chrome.browser.media.router.CastSessionUtil;
import org.chromium.chrome.browser.media.router.FlingingController; import org.chromium.chrome.browser.media.router.FlingingController;
import org.chromium.chrome.browser.media.router.MediaSource; import org.chromium.chrome.browser.media.router.MediaSource;
import org.chromium.chrome.browser.media.ui.MediaNotificationInfo; import org.chromium.chrome.browser.media.ui.MediaNotificationInfo;
...@@ -42,9 +43,6 @@ import javax.annotation.Nullable; ...@@ -42,9 +43,6 @@ import javax.annotation.Nullable;
public class CastSessionImpl implements MediaNotificationListener, CastSession { public class CastSessionImpl implements MediaNotificationListener, CastSession {
private static final String TAG = "MediaRouter"; private static final String TAG = "MediaRouter";
// 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 CastMessagingChannel implements Cast.MessageReceivedCallback { private static class CastMessagingChannel implements Cast.MessageReceivedCallback {
private final CastSession mSession; private final CastSession mSession;
...@@ -405,7 +403,8 @@ public class CastSessionImpl implements MediaNotificationListener, CastSession { ...@@ -405,7 +403,8 @@ public class CastSessionImpl implements MediaNotificationListener, CastSession {
double newLevel = volume.getDouble("level"); double newLevel = volume.getDouble("level");
double currentLevel = Cast.CastApi.getVolume(mApiClient); double currentLevel = Cast.CastApi.getVolume(mApiClient);
if (!Double.isNaN(currentLevel) if (!Double.isNaN(currentLevel)
&& Math.abs(currentLevel - newLevel) > MIN_VOLUME_LEVEL_DELTA) { && Math.abs(currentLevel - newLevel)
> CastSessionUtil.MIN_VOLUME_LEVEL_DELTA) {
Cast.CastApi.setVolume(mApiClient, newLevel); Cast.CastApi.setVolume(mApiClient, newLevel);
waitForVolumeChange = true; waitForVolumeChange = true;
} }
......
...@@ -17,12 +17,12 @@ import org.json.JSONObject; ...@@ -17,12 +17,12 @@ import org.json.JSONObject;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.chrome.R; import org.chromium.chrome.R;
import org.chromium.chrome.browser.media.remote.RemoteMediaPlayerWrapper; import org.chromium.chrome.browser.media.remote.RemoteMediaPlayerWrapper;
import org.chromium.chrome.browser.media.router.CastSessionUtil;
import org.chromium.chrome.browser.media.router.FlingingController; import org.chromium.chrome.browser.media.router.FlingingController;
import org.chromium.chrome.browser.media.router.MediaSource; import org.chromium.chrome.browser.media.router.MediaSource;
import org.chromium.chrome.browser.media.router.cast.CastMessageHandler; import org.chromium.chrome.browser.media.router.cast.CastMessageHandler;
import org.chromium.chrome.browser.media.router.cast.CastSession; import org.chromium.chrome.browser.media.router.cast.CastSession;
import org.chromium.chrome.browser.media.router.cast.CastSessionInfo; import org.chromium.chrome.browser.media.router.cast.CastSessionInfo;
import org.chromium.chrome.browser.media.router.cast.CastSessionUtil;
import org.chromium.chrome.browser.media.router.cast.ChromeCastSessionManager; import org.chromium.chrome.browser.media.router.cast.ChromeCastSessionManager;
import org.chromium.chrome.browser.media.ui.MediaNotificationInfo; import org.chromium.chrome.browser.media.ui.MediaNotificationInfo;
import org.chromium.chrome.browser.media.ui.MediaNotificationListener; import org.chromium.chrome.browser.media.ui.MediaNotificationListener;
......
...@@ -742,6 +742,8 @@ chrome_java_sources = [ ...@@ -742,6 +742,8 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/media/remote/RemoteVideoInfo.java", "java/src/org/chromium/chrome/browser/media/remote/RemoteVideoInfo.java",
"java/src/org/chromium/chrome/browser/media/remote/PositionExtrapolator.java", "java/src/org/chromium/chrome/browser/media/remote/PositionExtrapolator.java",
"java/src/org/chromium/chrome/browser/media/router/BaseMediaRouteDialogManager.java", "java/src/org/chromium/chrome/browser/media/router/BaseMediaRouteDialogManager.java",
"java/src/org/chromium/chrome/browser/media/router/CastRequestIdGenerator.java",
"java/src/org/chromium/chrome/browser/media/router/CastSessionUtil.java",
"java/src/org/chromium/chrome/browser/media/router/ClientRecord.java", "java/src/org/chromium/chrome/browser/media/router/ClientRecord.java",
"java/src/org/chromium/chrome/browser/media/router/ChromeMediaRouter.java", "java/src/org/chromium/chrome/browser/media/router/ChromeMediaRouter.java",
"java/src/org/chromium/chrome/browser/media/router/ChromeMediaRouterDialogController.java", "java/src/org/chromium/chrome/browser/media/router/ChromeMediaRouterDialogController.java",
...@@ -766,16 +768,14 @@ chrome_java_sources = [ ...@@ -766,16 +768,14 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/media/router/caf/CastUtils.java", "java/src/org/chromium/chrome/browser/media/router/caf/CastUtils.java",
"java/src/org/chromium/chrome/browser/media/router/caf/CafBaseMediaRouteProvider.java", "java/src/org/chromium/chrome/browser/media/router/caf/CafBaseMediaRouteProvider.java",
"java/src/org/chromium/chrome/browser/media/router/caf/CafMediaRouteProvider.java", "java/src/org/chromium/chrome/browser/media/router/caf/CafMediaRouteProvider.java",
"java/src/org/chromium/chrome/browser/media/router/caf/CastMessageHandler.java", "java/src/org/chromium/chrome/browser/media/router/caf/CafMessageHandler.java",
"java/src/org/chromium/chrome/browser/media/router/cast/BaseMediaRouteProvider.java", "java/src/org/chromium/chrome/browser/media/router/cast/BaseMediaRouteProvider.java",
"java/src/org/chromium/chrome/browser/media/router/cast/CastMediaRouteProvider.java", "java/src/org/chromium/chrome/browser/media/router/cast/CastMediaRouteProvider.java",
"java/src/org/chromium/chrome/browser/media/router/cast/CastMediaSource.java", "java/src/org/chromium/chrome/browser/media/router/cast/CastMediaSource.java",
"java/src/org/chromium/chrome/browser/media/router/cast/CastMessageHandler.java", "java/src/org/chromium/chrome/browser/media/router/cast/CastMessageHandler.java",
"java/src/org/chromium/chrome/browser/media/router/cast/CastRequestIdGenerator.java",
"java/src/org/chromium/chrome/browser/media/router/cast/CastSession.java", "java/src/org/chromium/chrome/browser/media/router/cast/CastSession.java",
"java/src/org/chromium/chrome/browser/media/router/cast/CastSessionImpl.java", "java/src/org/chromium/chrome/browser/media/router/cast/CastSessionImpl.java",
"java/src/org/chromium/chrome/browser/media/router/cast/CastSessionInfo.java", "java/src/org/chromium/chrome/browser/media/router/cast/CastSessionInfo.java",
"java/src/org/chromium/chrome/browser/media/router/cast/CastSessionUtil.java",
"java/src/org/chromium/chrome/browser/media/router/cast/ChromeCastSessionManager.java", "java/src/org/chromium/chrome/browser/media/router/cast/ChromeCastSessionManager.java",
"java/src/org/chromium/chrome/browser/media/router/cast/CreateRouteRequest.java", "java/src/org/chromium/chrome/browser/media/router/cast/CreateRouteRequest.java",
"java/src/org/chromium/chrome/browser/media/router/cast/remoting/RemotingCastSession.java", "java/src/org/chromium/chrome/browser/media/router/cast/remoting/RemotingCastSession.java",
......
...@@ -35,6 +35,7 @@ import org.robolectric.shadows.ShadowLog; ...@@ -35,6 +35,7 @@ import org.robolectric.shadows.ShadowLog;
import org.chromium.base.test.BaseRobolectricTestRunner; import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature; import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.media.router.CastSessionUtil;
import org.chromium.chrome.browser.media.router.ClientRecord; import org.chromium.chrome.browser.media.router.ClientRecord;
import org.chromium.chrome.browser.media.router.cast.CastMessageHandler.RequestRecord; import org.chromium.chrome.browser.media.router.cast.CastMessageHandler.RequestRecord;
import org.chromium.chrome.browser.media.router.cast.JSONTestUtils.JSONObjectLike; import org.chromium.chrome.browser.media.router.cast.JSONTestUtils.JSONObjectLike;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment