blob: fed517c7009247df0c25b0ae7376c4d6a78f7b8a [file] [log] [blame]
// Copyright 2015 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.cast;
import android.support.v7.media.MediaRouter;
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.ChromeMediaRouter;
import org.chromium.chrome.browser.media.router.ClientRecord;
import org.chromium.chrome.browser.media.router.MediaRoute;
import org.chromium.chrome.browser.media.router.MediaRouteManager;
import org.chromium.chrome.browser.media.router.MediaRouteProvider;
import org.chromium.chrome.browser.media.router.MediaSink;
import org.chromium.chrome.browser.media.router.MediaSource;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* A {@link MediaRouteProvider} implementation for Cast devices and applications.
*/
public class CastMediaRouteProvider extends BaseMediaRouteProvider {
private static final String TAG = "MediaRouter";
private static final String AUTO_JOIN_PRESENTATION_ID = "auto-join";
private static final String PRESENTATION_ID_SESSION_ID_PREFIX = "cast-session_";
private final CastMessageHandler mMessageHandler;
private ClientRecord mLastRemovedRouteRecord;
private final Map<String, ClientRecord> mClientRecords = new HashMap<String, ClientRecord>();
/**
* @return Initialized {@link CastMediaRouteProvider} object.
*/
public static CastMediaRouteProvider create(MediaRouteManager manager) {
return new CastMediaRouteProvider(ChromeMediaRouter.getAndroidMediaRouter(), manager);
}
// Migrated to CafMediaRouteProvider. See https://crbug.com/711860.
@Override
public void onSessionStartFailed() {
super.onSessionStartFailed();
mClientRecords.clear();
}
// Migrated to CafMediaRouteProvider. See https://crbug.com/711860.
@Override
public void onSessionStarted(CastSession session) {
super.onSessionStarted(session);
mMessageHandler.onSessionCreated(mSession);
}
// Migrated to CafMediaRouteProvider. See https://crbug.com/711860.
@Override
public void onSessionEnded() {
if (mSession == null) return;
if (mClientRecords.isEmpty()) {
for (String routeId : mRoutes.keySet()) mManager.onRouteClosed(routeId);
mRoutes.clear();
} else {
mLastRemovedRouteRecord = mClientRecords.values().iterator().next();
for (ClientRecord client : mClientRecords.values()) {
mManager.onRouteClosed(client.routeId);
mRoutes.remove(client.routeId);
}
mClientRecords.clear();
}
mSession = null;
if (mAndroidMediaRouter != null) {
mAndroidMediaRouter.selectRoute(mAndroidMediaRouter.getDefaultRoute());
}
}
public void onMessageSentResult(boolean success, int callbackId) {
mManager.onMessageSentResult(success, callbackId);
}
public void onMessage(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);
}
public CastMessageHandler getMessageHandler() {
return mMessageHandler;
}
// Migrated to CafMediaRouteProvider. See https://crbug.com/711860.
public Set<String> getClients() {
return mClientRecords.keySet();
}
// Migrated to CafMediaRouteProvider. See https://crbug.com/711860.
public Map<String, ClientRecord> getClientRecords() {
return mClientRecords;
}
@Override
protected MediaSource getSourceFromId(String sourceId) {
return CastMediaSource.from(sourceId);
}
@Override
protected ChromeCastSessionManager.CastSessionLaunchRequest createSessionLaunchRequest(
MediaSource source, MediaSink sink, String presentationId, String origin, int tabId,
boolean isIncognito, int nativeRequestId) {
return new CreateRouteRequest(source, sink, presentationId, origin, tabId, isIncognito,
nativeRequestId, this, CreateRouteRequest.RequestedCastSessionType.CAST,
mMessageHandler);
}
// Migrated to CafMediaRouteProvider. See https://crbug.com/711860.
@Override
public void onSessionStarting(
ChromeCastSessionManager.CastSessionLaunchRequest sessionLaunchRequest) {
CreateRouteRequest request = (CreateRouteRequest) sessionLaunchRequest;
MediaSink sink = request.getSink();
MediaSource source = request.getSource();
MediaRoute route =
new MediaRoute(sink.getId(), source.getSourceId(), request.getPresentationId());
addRoute(route, request.getOrigin(), request.getTabId());
mManager.onRouteCreated(route.id, route.sinkId, request.getNativeRequestId(), this, true);
String clientId = ((CastMediaSource) source).getClientId();
if (clientId != null) {
ClientRecord clientRecord = mClientRecords.get(clientId);
if (clientRecord != null) {
sendReceiverAction(clientRecord.routeId, sink, clientId, "cast");
}
}
}
// Migrated to CafMediaRouteProvider. See https://crbug.com/711860.
@Override
public void joinRoute(String sourceId, String presentationId, String origin, int tabId,
int nativeRequestId) {
CastMediaSource source = CastMediaSource.from(sourceId);
if (source == null || source.getClientId() == null) {
mManager.onRouteRequestError("Unsupported presentation URL", nativeRequestId);
return;
}
if (mSession == null) {
mManager.onRouteRequestError("No presentation", nativeRequestId);
return;
}
if (!canJoinExistingSession(presentationId, origin, tabId, source)) {
mManager.onRouteRequestError("No matching route", nativeRequestId);
return;
}
MediaRoute route = new MediaRoute(mSession.getSinkId(), sourceId, presentationId);
addRoute(route, origin, tabId);
mManager.onRouteCreated(route.id, route.sinkId, nativeRequestId, this, false);
}
// Migrated to CafMediaRouteProvider. See https://crbug.com/711860.
@Override
public void closeRoute(String routeId) {
MediaRoute route = mRoutes.get(routeId);
if (route == null) return;
if (mSession == null) {
mRoutes.remove(routeId);
mManager.onRouteClosed(routeId);
return;
}
ClientRecord client = getClientRecordByRouteId(routeId);
if (client != null && mAndroidMediaRouter != null) {
MediaSink sink = MediaSink.fromSinkId(mSession.getSinkId(), mAndroidMediaRouter);
if (sink != null) sendReceiverAction(routeId, sink, client.clientId, "stop");
}
ChromeCastSessionManager.get().stopApplication();
}
// Migrated to CafMediaRouteProvider. See https://crbug.com/711860.
@Override
public void detachRoute(String routeId) {
mRoutes.remove(routeId);
removeClient(getClientRecordByRouteId(routeId));
}
@Override
public void sendStringMessage(String routeId, String message, int nativeCallbackId) {
Log.d(TAG, "Received message from client: %s", message);
if (!mRoutes.containsKey(routeId)) {
mManager.onMessageSentResult(false, nativeCallbackId);
return;
}
boolean success = false;
try {
JSONObject jsonMessage = new JSONObject(message);
String messageType = jsonMessage.getString("type");
// TODO(zqzhang): Move the handling of "client_connect", "client_disconnect" and
// "leave_session" from CastMRP to CastMessageHandler. Also, need to have a
// ClientManager for client managing.
if ("client_connect".equals(messageType)) {
success = handleClientConnectMessage(jsonMessage);
} else if ("client_disconnect".equals(messageType)) {
success = handleClientDisconnectMessage(jsonMessage);
} else if ("leave_session".equals(messageType)) {
success = handleLeaveSessionMessage(jsonMessage);
} else if (mSession != null) {
success = mMessageHandler.handleSessionMessage(jsonMessage);
}
} catch (JSONException e) {
Log.e(TAG, "JSONException while handling internal message: " + e);
success = false;
}
mManager.onMessageSentResult(success, nativeCallbackId);
}
private boolean handleClientConnectMessage(JSONObject jsonMessage) throws JSONException {
String clientId = jsonMessage.getString("clientId");
if (clientId == null) return false;
ClientRecord clientRecord = mClientRecords.get(clientId);
if (clientRecord == null) return false;
clientRecord.isConnected = true;
if (mSession != null) mSession.onClientConnected(clientId);
if (clientRecord.pendingMessages.size() == 0) return true;
for (String message : clientRecord.pendingMessages) {
Log.d(TAG, "Deqeueing message for client %s: %s", clientId, message);
mManager.onMessage(clientRecord.routeId, message);
}
clientRecord.pendingMessages.clear();
return true;
}
private boolean handleClientDisconnectMessage(JSONObject jsonMessage) throws JSONException {
String clientId = jsonMessage.getString("clientId");
if (clientId == null) return false;
ClientRecord client = mClientRecords.get(clientId);
if (client == null) return false;
mRoutes.remove(client.routeId);
removeClient(client);
mManager.onRouteClosed(client.routeId);
return true;
}
private boolean handleLeaveSessionMessage(JSONObject jsonMessage) throws JSONException {
String clientId = jsonMessage.getString("clientId");
if (clientId == null || mSession == null) return false;
String sessionId = jsonMessage.getString("message");
if (!mSession.getSessionId().equals(sessionId)) return false;
ClientRecord leavingClient = mClientRecords.get(clientId);
if (leavingClient == null) return false;
int sequenceNumber = jsonMessage.optInt("sequenceNumber", -1);
onMessage(clientId, buildInternalMessage("leave_session", sequenceNumber, clientId, null));
// Send a "disconnect_session" message to all the clients that match with the leaving
// client's auto join policy.
for (ClientRecord client : mClientRecords.values()) {
if ((CastMediaSource.AUTOJOIN_TAB_AND_ORIGIN_SCOPED.equals(leavingClient.autoJoinPolicy)
&& isSameOrigin(client.origin, leavingClient.origin)
&& client.tabId == leavingClient.tabId)
|| (CastMediaSource.AUTOJOIN_ORIGIN_SCOPED.equals(leavingClient.autoJoinPolicy)
&& isSameOrigin(client.origin, leavingClient.origin))) {
onMessage(client.clientId,
buildInternalMessage("disconnect_session", -1, client.clientId, sessionId));
}
}
return true;
}
private String buildInternalMessage(
String type, int sequenceNumber, String clientId, String message) throws JSONException {
JSONObject jsonMessage = new JSONObject();
jsonMessage.put("type", type);
jsonMessage.put("sequenceNumber", sequenceNumber);
jsonMessage.put("timeoutMillis", 0);
jsonMessage.put("clientId", clientId);
jsonMessage.put("message", message);
return jsonMessage.toString();
}
@VisibleForTesting
CastMediaRouteProvider(MediaRouter androidMediaRouter, MediaRouteManager manager) {
super(androidMediaRouter, manager);
mMessageHandler = new CastMessageHandler(this);
}
// Migrated to CafMediaRouteProvider. See https://crbug.com/711860.
private boolean canAutoJoin(CastMediaSource source, String origin, int tabId) {
if (source.getAutoJoinPolicy().equals(CastMediaSource.AUTOJOIN_PAGE_SCOPED)) return false;
CastMediaSource currentSource = CastMediaSource.from(mSession.getSourceId());
if (!currentSource.getApplicationId().equals(source.getApplicationId())) return false;
ClientRecord client = null;
if (!mClientRecords.isEmpty()) {
client = mClientRecords.values().iterator().next();
} else if (mLastRemovedRouteRecord != null) {
client = mLastRemovedRouteRecord;
return isSameOrigin(origin, client.origin) && tabId == client.tabId;
}
if (client == null) return false;
boolean sameOrigin = isSameOrigin(origin, client.origin);
if (source.getAutoJoinPolicy().equals(CastMediaSource.AUTOJOIN_ORIGIN_SCOPED)) {
return sameOrigin;
} else if (source.getAutoJoinPolicy().equals(
CastMediaSource.AUTOJOIN_TAB_AND_ORIGIN_SCOPED)) {
return sameOrigin && tabId == client.tabId;
}
return false;
}
// Migrated to CafMediaRouteProvider. See https://crbug.com/711860.
private boolean canJoinExistingSession(
String presentationId, String origin, int tabId, CastMediaSource source) {
if (AUTO_JOIN_PRESENTATION_ID.equals(presentationId)) {
return canAutoJoin(source, origin, tabId);
} else if (presentationId.startsWith(PRESENTATION_ID_SESSION_ID_PREFIX)) {
String sessionId = presentationId.substring(PRESENTATION_ID_SESSION_ID_PREFIX.length());
if (mSession.getSessionId().equals(sessionId)) return true;
} else {
for (MediaRoute route : mRoutes.values()) {
if (route.presentationId.equals(presentationId)) return true;
}
}
return false;
}
// Migrated to CafMediaRouteProvider. See https://crbug.com/711860.
@Nullable
private ClientRecord getClientRecordByRouteId(String routeId) {
for (ClientRecord record : mClientRecords.values()) {
if (record.routeId.equals(routeId)) return record;
}
return null;
}
// Migrated to CafMediaRouteProvider. See https://crbug.com/711860.
@VisibleForTesting
void addRoute(MediaRoute route, String origin, int tabId) {
mRoutes.put(route.id, route);
CastMediaSource source = CastMediaSource.from(route.sourceId);
final String clientId = source.getClientId();
if (clientId == null || mClientRecords.get(clientId) != null) return;
mClientRecords.put(clientId,
new ClientRecord(
route.id,
clientId,
source.getApplicationId(),
source.getAutoJoinPolicy(),
origin,
tabId));
}
// Migrated to CastMessageHandler.sendReceiverActionToClient. See https://crbug.com/711860.
private void sendReceiverAction(
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", CastSessionImpl.getCapabilities(sink.getDevice()));
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);
onMessage(clientId, json.toString());
} catch (JSONException e) {
Log.e(TAG, "Failed to send receiver action message", e);
}
}
private void removeClient(@Nullable ClientRecord client) {
if (client == null) return;
mLastRemovedRouteRecord = client;
mClientRecords.remove(client.clientId);
}
/**
* Compares two origins. Empty origin strings correspond to unique origins in
* url::Origin.
*
* @param originA A URL origin.
* @param originB A URL origin.
* @return True if originA and originB represent the same origin, false otherwise.
*/
// Migrated to CafMediaRouteProvider. See https://crbug.com/711860.
private static final boolean isSameOrigin(String originA, String originB) {
if (originA == null || originA.isEmpty() || originB == null || originB.isEmpty())
return false;
return originA.equals(originB);
}
}