// 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; import android.content.Context; import android.support.v7.media.MediaRouter; import org.chromium.base.SysUtils; import org.chromium.base.VisibleForTesting; import org.chromium.base.annotations.CalledByNative; import org.chromium.base.annotations.JNINamespace; import org.chromium.chrome.browser.media.router.cast.CastMediaRouteProvider; import org.chromium.chrome.browser.media.router.cast.MediaSink; import org.chromium.chrome.browser.media.router.cast.MediaSource; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.Nullable; /** * Implements the JNI interface called from the C++ Media Router implementation on Android. * Owns a list of {@link MediaRouteProvider} implementations and dispatches native calls to them. */ @JNINamespace("media_router") public class ChromeMediaRouter implements MediaRouteManager { private static final String TAG = "MediaRouter"; private static MediaRouteProvider.Builder sRouteProviderBuilder = new CastMediaRouteProvider.Builder(); // The pointer to the native object. Can be null only during tests. private final long mNativeMediaRouterAndroid; private final List<MediaRouteProvider> mRouteProviders = new ArrayList<MediaRouteProvider>(); private final Map<String, MediaRouteProvider> mRouteIdsToProviders = new HashMap<String, MediaRouteProvider>(); private final Map<String, Map<MediaRouteProvider, List<MediaSink>>> mSinksPerSourcePerProvider = new HashMap<String, Map<MediaRouteProvider, List<MediaSink>>>(); private final Map<String, List<MediaSink>> mSinksPerSource = new HashMap<String, List<MediaSink>>(); @VisibleForTesting public static void setRouteProviderBuilderForTest(MediaRouteProvider.Builder builder) { sRouteProviderBuilder = builder; } @VisibleForTesting protected List<MediaRouteProvider> getRouteProvidersForTest() { return mRouteProviders; } @VisibleForTesting protected Map<String, MediaRouteProvider> getRouteIdsToProvidersForTest() { return mRouteIdsToProviders; } @VisibleForTesting protected Map<String, Map<MediaRouteProvider, List<MediaSink>>> getSinksPerSourcePerProviderForTest() { return mSinksPerSourcePerProvider; } @VisibleForTesting protected Map<String, List<MediaSink>> getSinksPerSourceForTest() { return mSinksPerSource; } /** * Obtains the {@link MediaRouter} instance given the application context. * @param applicationContext The context to get the Android media router service for. * @return Null if the media router API is not supported, the service instance otherwise. */ @Nullable public static MediaRouter getAndroidMediaRouter(Context applicationContext) { try { // Pre-MR1 versions of JB do not have the complete MediaRouter APIs, // so getting the MediaRouter instance will throw an exception. return MediaRouter.getInstance(applicationContext); } catch (NoSuchMethodError e) { return null; } catch (NoClassDefFoundError e) { // TODO(mlamouri): happens with Robolectric. return null; } } @Override public void onSinksReceived( String sourceId, MediaRouteProvider provider, List<MediaSink> sinks) { if (!mSinksPerSourcePerProvider.containsKey(sourceId)) { mSinksPerSourcePerProvider.put( sourceId, new HashMap<MediaRouteProvider, List<MediaSink>>()); } // Replace the sinks found by this provider with the new list. Map<MediaRouteProvider, List<MediaSink>> sinksPerProvider = mSinksPerSourcePerProvider.get(sourceId); sinksPerProvider.put(provider, sinks); List<MediaSink> allSinksPerSource = new ArrayList<MediaSink>(); for (List<MediaSink> s : sinksPerProvider.values()) allSinksPerSource.addAll(s); mSinksPerSource.put(sourceId, allSinksPerSource); if (mNativeMediaRouterAndroid != 0) { nativeOnSinksReceived(mNativeMediaRouterAndroid, sourceId, allSinksPerSource.size()); } } @Override public void onRouteCreated( String mediaRouteId, String mediaSinkId, int requestId, MediaRouteProvider provider, boolean wasLaunched) { mRouteIdsToProviders.put(mediaRouteId, provider); if (mNativeMediaRouterAndroid != 0) { nativeOnRouteCreated(mNativeMediaRouterAndroid, mediaRouteId, mediaSinkId, requestId, wasLaunched); } } @Override public void onRouteRequestError(String errorText, int requestId) { if (mNativeMediaRouterAndroid != 0) { nativeOnRouteRequestError(mNativeMediaRouterAndroid, errorText, requestId); } } @Override public void onRouteClosed(String mediaRouteId) { if (mNativeMediaRouterAndroid != 0) { nativeOnRouteClosed(mNativeMediaRouterAndroid, mediaRouteId); } mRouteIdsToProviders.remove(mediaRouteId); } @Override public void onRouteClosedWithError(String mediaRouteId, String message) { if (mNativeMediaRouterAndroid != 0) { nativeOnRouteClosedWithError(mNativeMediaRouterAndroid, mediaRouteId, message); } mRouteIdsToProviders.remove(mediaRouteId); } @Override public void onMessageSentResult(boolean success, int callbackId) { nativeOnMessageSentResult(mNativeMediaRouterAndroid, success, callbackId); } @Override public void onMessage(String mediaRouteId, String message) { nativeOnMessage(mNativeMediaRouterAndroid, mediaRouteId, message); } /** * Initializes the media router and its providers. * @param nativeMediaRouterAndroid the handler for the native counterpart of this instance * @param applicationContext the application context to use to obtain system APIs * @return an initialized {@link ChromeMediaRouter} instance */ @CalledByNative public static ChromeMediaRouter create(long nativeMediaRouterAndroid, Context applicationContext) { ChromeMediaRouter router = new ChromeMediaRouter(nativeMediaRouterAndroid); MediaRouteProvider provider = sRouteProviderBuilder.create(applicationContext, router); if (provider != null) router.addMediaRouteProvider(provider); return router; } /** * Starts background monitoring for available media sinks compatible with the given * |sourceUrn| if the device is in a state that allows it. * @param sourceId a URL to use for filtering of the available media sinks * @return whether the monitoring started (ie. was allowed). */ @CalledByNative public boolean startObservingMediaSinks(String sourceId) { if (SysUtils.isLowEndDevice()) return false; for (MediaRouteProvider provider : mRouteProviders) { provider.startObservingMediaSinks(sourceId); } return true; } /** * Stops background monitoring for available media sinks compatible with the given * |sourceUrn| * @param sourceId a URL passed to {@link #startObservingMediaSinks(String)} before. */ @CalledByNative public void stopObservingMediaSinks(String sourceId) { for (MediaRouteProvider provider : mRouteProviders) { provider.stopObservingMediaSinks(sourceId); } mSinksPerSource.remove(sourceId); mSinksPerSourcePerProvider.remove(sourceId); } /** * Returns the URN of the media sink corresponding to the given source URN * and an index. Essentially a way to access the corresponding {@link MediaSink}'s * list via JNI. * @param sourceUrn The URN to get the sink for. * @param index The index of the sink in the current sink array. * @return the corresponding sink URN if found or null. */ @CalledByNative public String getSinkUrn(String sourceUrn, int index) { return getSink(sourceUrn, index).getUrn(); } /** * Returns the name of the media sink corresponding to the given source URN * and an index. Essentially a way to access the corresponding {@link MediaSink}'s * list via JNI. * @param sourceUrn The URN to get the sink for. * @param index The index of the sink in the current sink array. * @return the corresponding sink name if found or null. */ @CalledByNative public String getSinkName(String sourceUrn, int index) { return getSink(sourceUrn, index).getName(); } /** * Initiates route creation with the given parameters. Notifies the native client of success * and failure. * @param sourceId the id of the {@link MediaSource} to route to the sink. * @param sinkId the id of the {@link MediaSink} to route the source to. * @param presentationId the id of the presentation to be used by the page. * @param origin the origin of the frame requesting a new route. * @param tabId the id of the tab the requesting frame belongs to. * @param isIncognito whether the route is being requested from an Incognito profile. * @param requestId the id of the route creation request tracked by the native side. */ @CalledByNative public void createRoute( String sourceId, String sinkId, String presentationId, String origin, int tabId, boolean isIncognito, int requestId) { MediaRouteProvider provider = getProviderForSource(sourceId); if (provider == null) { onRouteRequestError("No provider supports createRoute with source: " + sourceId + " and sink: " + sinkId, requestId); return; } provider.createRoute( sourceId, sinkId, presentationId, origin, tabId, isIncognito, requestId); } /** * Initiates route joining with the given parameters. Notifies the native client of success * or failure. * @param sourceId the id of the {@link MediaSource} to route to the sink. * @param sinkId the id of the {@link MediaSink} to route the source to. * @param presentationId the id of the presentation to be used by the page. * @param origin the origin of the frame requesting a new route. * @param tabId the id of the tab the requesting frame belongs to. * @param requestId the id of the route creation request tracked by the native side. */ @CalledByNative public void joinRoute( String sourceId, String presentationId, String origin, int tabId, int requestId) { MediaRouteProvider provider = getProviderForSource(sourceId); if (provider == null) { onRouteRequestError("Route not found.", requestId); return; } provider.joinRoute(sourceId, presentationId, origin, tabId, requestId); } /** * Closes the route specified by the id. * @param routeId the id of the route to close. */ @CalledByNative public void closeRoute(String routeId) { MediaRouteProvider provider = mRouteIdsToProviders.get(routeId); if (provider == null) return; provider.closeRoute(routeId); } /** * Notifies the specified route that it's not attached to the web page anymore. * @param routeId the id of the route that was detached. */ @CalledByNative public void detachRoute(String routeId) { MediaRouteProvider provider = mRouteIdsToProviders.get(routeId); if (provider == null) return; provider.detachRoute(routeId); mRouteIdsToProviders.remove(routeId); } /** * Sends a string message to the specified route. * @param routeId The id of the route to send the message to. * @param message The message to send. * @param callbackId The id of the result callback tracked by the native side. */ @CalledByNative public void sendStringMessage(String routeId, String message, int callbackId) { MediaRouteProvider provider = mRouteIdsToProviders.get(routeId); if (provider == null) { nativeOnMessageSentResult(mNativeMediaRouterAndroid, false, callbackId); return; } provider.sendStringMessage(routeId, message, callbackId); } /** * Sends a binary message to the specified route. * @param routeId The id of the route to send the message to. * @param data The binary message to send. * @param callbackId The id of the result callback tracked by the native side. */ @CalledByNative public void sendBinaryMessage(String routeId, byte[] data, int callbackId) { MediaRouteProvider provider = mRouteIdsToProviders.get(routeId); if (provider == null) { nativeOnMessageSentResult(mNativeMediaRouterAndroid, false, callbackId); return; } provider.sendBinaryMessage(routeId, data, callbackId); } @VisibleForTesting protected ChromeMediaRouter(long nativeMediaRouter) { mNativeMediaRouterAndroid = nativeMediaRouter; } @VisibleForTesting protected void addMediaRouteProvider(MediaRouteProvider provider) { mRouteProviders.add(provider); } private MediaSink getSink(String sourceId, int index) { assert mSinksPerSource.containsKey(sourceId); return mSinksPerSource.get(sourceId).get(index); } private MediaRouteProvider getProviderForSource(String sourceId) { for (MediaRouteProvider provider : mRouteProviders) { if (provider.supportsSource(sourceId)) return provider; } return null; } native void nativeOnSinksReceived( long nativeMediaRouterAndroid, String sourceUrn, int count); native void nativeOnRouteCreated( long nativeMediaRouterAndroid, String mediaRouteId, String mediaSinkId, int createRouteRequestId, boolean wasLaunched); native void nativeOnRouteRequestError( long nativeMediaRouterAndroid, String errorText, int createRouteRequestId); native void nativeOnRouteClosed(long nativeMediaRouterAndroid, String mediaRouteId); native void nativeOnRouteClosedWithError( long nativeMediaRouterAndroid, String mediaRouteId, String message); native void nativeOnMessageSentResult( long nativeMediaRouterAndroid, boolean success, int callbackId); native void nativeOnMessage(long nativeMediaRouterAndroid, String mediaRouteId, String message); }