| // 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)); |
| } |
| |
| // Migrated to CafMessageHandler. See https://crbug.com/711860. |
| @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); |
| } |
| |
| // Migrated to CafMessageHandler. See https://crbug.com/711860. |
| 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; |
| } |
| |
| // Migrated to CafMessageHandler. See https://crbug.com/711860. |
| 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; |
| } |
| |
| // Migrated to CafMessageHandler. See https://crbug.com/711860. |
| 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; |
| } |
| |
| // Migrated to CafMessageHandler. See https://crbug.com/711860. |
| 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); |
| } |
| } |