diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 91e26e2ae0ce..18586cadb583 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -105,8 +105,10 @@ android { } if (!mozconfig.substs.MOZ_NATIVE_DEVICES) { - exclude 'org/mozilla/gecko/ChromeCast.java' + exclude 'org/mozilla/gecko/ChromeCastDisplay.java' + exclude 'org/mozilla/gecko/ChromeCastPlayer.java' exclude 'org/mozilla/gecko/GeckoMediaPlayer.java' + exclude 'org/mozilla/gecko/GeckoPresentationDisplay.java' exclude 'org/mozilla/gecko/MediaPlayerManager.java' } diff --git a/mobile/android/base/java/org/mozilla/gecko/ChromeCastDisplay.java b/mobile/android/base/java/org/mozilla/gecko/ChromeCastDisplay.java new file mode 100644 index 000000000000..4f311dbd1542 --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/ChromeCastDisplay.java @@ -0,0 +1,66 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * vim: ts=4 sw=4 expandtab: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import org.json.JSONObject; +import org.json.JSONException; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.util.EventCallback; + +import com.google.android.gms.cast.CastDevice; +import com.google.android.gms.cast.CastRemoteDisplayLocalService; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GooglePlayServicesUtil; +import com.google.android.gms.common.api.Status; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.support.v7.media.MediaRouter.RouteInfo; +import android.util.Log; + +public class ChromeCastDisplay implements GeckoPresentationDisplay { + + static final String REMOTE_DISPLAY_APP_ID = "4574A331"; + + private static final String LOGTAG = "GeckoChromeCastDisplay"; + private final RouteInfo route; + private CastDevice castDevice; + + public ChromeCastDisplay(Context context, RouteInfo route) { + int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context); + if (status != ConnectionResult.SUCCESS) { + throw new IllegalStateException("Play services are required for Chromecast support (got status code " + status + ")"); + } + + this.route = route; + this.castDevice = CastDevice.getFromBundle(route.getExtras()); + } + + public JSONObject toJSON() { + final JSONObject obj = new JSONObject(); + try { + if (castDevice == null) { + return null; + } + obj.put("uuid", route.getId()); + obj.put("friendlyName", castDevice.getFriendlyName()); + obj.put("type", "chromecast"); + } catch (JSONException ex) { + Log.d(LOGTAG, "Error building route", ex); + } + + return obj; + } + + @Override + public void start(EventCallback callback) { } + + @Override + public void stop(EventCallback callback) { } +} diff --git a/mobile/android/base/java/org/mozilla/gecko/ChromeCast.java b/mobile/android/base/java/org/mozilla/gecko/ChromeCastPlayer.java similarity index 99% rename from mobile/android/base/java/org/mozilla/gecko/ChromeCast.java rename to mobile/android/base/java/org/mozilla/gecko/ChromeCastPlayer.java index 8eadb61b8fcc..c531b8c377b9 100644 --- a/mobile/android/base/java/org/mozilla/gecko/ChromeCast.java +++ b/mobile/android/base/java/org/mozilla/gecko/ChromeCastPlayer.java @@ -34,7 +34,7 @@ import android.support.v7.media.MediaRouter.RouteInfo; import android.util.Log; /* Implementation of GeckoMediaPlayer for talking to ChromeCast devices */ -class ChromeCast implements GeckoMediaPlayer { +class ChromeCastPlayer implements GeckoMediaPlayer { private static final boolean SHOW_DEBUG = false; static final String MIRROR_RECEIVER_APP_ID = "08FF1091"; @@ -168,7 +168,7 @@ class ChromeCast implements GeckoMediaPlayer { } } - public ChromeCast(Context context, RouteInfo route) { + public ChromeCastPlayer(Context context, RouteInfo route) { int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context); if (status != ConnectionResult.SUCCESS) { throw new IllegalStateException("Play services are required for Chromecast support (got status code " + status + ")"); @@ -493,7 +493,7 @@ class ChromeCast implements GeckoMediaPlayer { apiClient.connect(); } - private static final String LOGTAG = "GeckoChromeCast"; + private static final String LOGTAG = "GeckoChromeCastPlayer"; private void debug(String msg, Exception e) { if (SHOW_DEBUG) { Log.e(LOGTAG, msg, e); diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoPresentationDisplay.java b/mobile/android/base/java/org/mozilla/gecko/GeckoPresentationDisplay.java new file mode 100644 index 000000000000..df9844d7b037 --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/GeckoPresentationDisplay.java @@ -0,0 +1,22 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import org.json.JSONObject; +import org.mozilla.gecko.util.EventCallback; + +/** + * Wrapper for MediaRouter types supported by Android to use for + * Presentation API, such as Chromecast, Miracast, etc. + */ +interface GeckoPresentationDisplay { + /** + * Can return null. + */ + JSONObject toJSON(); + void start(EventCallback callback); + void stop(EventCallback callback); +} diff --git a/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java b/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java index 1757650ae9e0..f87c52feec41 100644 --- a/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java +++ b/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java @@ -64,7 +64,8 @@ public class MediaPlayerManager extends Fragment implements NativeEventListener } protected MediaRouter mediaRouter = null; - protected final Map displays = new HashMap(); + protected final Map players = new HashMap(); + protected final Map displays = new HashMap(); // used for Presentation API @Override public void onCreate(Bundle savedInstanceState) { @@ -100,9 +101,9 @@ public class MediaPlayerManager extends Fragment implements NativeEventListener public void handleMessage(String event, final NativeJSObject message, final EventCallback callback) { debug(event); - final GeckoMediaPlayer display = displays.get(message.getString("id")); - if (display == null) { - Log.e(LOGTAG, "Couldn't find a display for this id: " + message.getString("id") + " for message: " + event); + final GeckoMediaPlayer player = players.get(message.getString("id")); + if (player == null) { + Log.e(LOGTAG, "Couldn't find a player for this id: " + message.getString("id") + " for message: " + event); if (callback != null) { callback.sendError(null); } @@ -110,24 +111,24 @@ public class MediaPlayerManager extends Fragment implements NativeEventListener } if ("MediaPlayer:Play".equals(event)) { - display.play(callback); + player.play(callback); } else if ("MediaPlayer:Start".equals(event)) { - display.start(callback); + player.start(callback); } else if ("MediaPlayer:Stop".equals(event)) { - display.stop(callback); + player.stop(callback); } else if ("MediaPlayer:Pause".equals(event)) { - display.pause(callback); + player.pause(callback); } else if ("MediaPlayer:End".equals(event)) { - display.end(callback); + player.end(callback); } else if ("MediaPlayer:Mirror".equals(event)) { - display.mirror(callback); + player.mirror(callback); } else if ("MediaPlayer:Message".equals(event) && message.has("data")) { - display.message(message.getString("data"), callback); + player.message(message.getString("data"), callback); } else if ("MediaPlayer:Load".equals(event)) { final String url = message.optString("source", ""); final String type = message.optString("type", "video/mp4"); final String title = message.optString("title", ""); - display.load(title, url, type, callback); + player.load(title, url, type, callback); } } @@ -136,9 +137,15 @@ public class MediaPlayerManager extends Fragment implements NativeEventListener @Override public void onRouteRemoved(MediaRouter router, RouteInfo route) { debug("onRouteRemoved: route=" + route); - displays.remove(route.getId()); + + // Remove from media player list. + players.remove(route.getId()); GeckoAppShell.notifyObservers("MediaPlayer:Removed", route.getId()); updatePresentation(); + + // Remove from presentation display list. + displays.remove(route.getId()); + GeckoAppShell.notifyObservers("AndroidCastDevice:Removed", route.getId()); } @SuppressWarnings("unused") @@ -164,21 +171,44 @@ public class MediaPlayerManager extends Fragment implements NativeEventListener @Override public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) { debug("onRouteAdded: route=" + route); - final GeckoMediaPlayer display = getMediaPlayerForRoute(route); - saveAndNotifyOfDisplay("MediaPlayer:Added", route, display); + final GeckoMediaPlayer player = getMediaPlayerForRoute(route); + saveAndNotifyOfPlayer("MediaPlayer:Added", route, player); updatePresentation(); + + final GeckoPresentationDisplay display = getPresentationDisplayForRoute(route); + saveAndNotifyOfDisplay("AndroidCastDevice:Added", route, display); } @Override public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) { debug("onRouteChanged: route=" + route); - final GeckoMediaPlayer display = displays.get(route.getId()); - saveAndNotifyOfDisplay("MediaPlayer:Changed", route, display); + final GeckoMediaPlayer player = players.get(route.getId()); + saveAndNotifyOfPlayer("MediaPlayer:Changed", route, player); updatePresentation(); + + final GeckoPresentationDisplay display = displays.get(route.getId()); + saveAndNotifyOfDisplay("AndroidCastDevice:Changed", route, display); + } + + private void saveAndNotifyOfPlayer(final String eventName, + MediaRouter.RouteInfo route, + final GeckoMediaPlayer player) { + if (player == null) { + return; + } + + final JSONObject json = player.toJSON(); + if (json == null) { + return; + } + + players.put(route.getId(), player); + GeckoAppShell.notifyObservers(eventName, json.toString()); } private void saveAndNotifyOfDisplay(final String eventName, - MediaRouter.RouteInfo route, final GeckoMediaPlayer display) { + MediaRouter.RouteInfo route, + final GeckoPresentationDisplay display) { if (display == null) { return; } @@ -196,7 +226,7 @@ public class MediaPlayerManager extends Fragment implements NativeEventListener private GeckoMediaPlayer getMediaPlayerForRoute(MediaRouter.RouteInfo route) { try { if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) { - return new ChromeCast(getActivity(), route); + return new ChromeCastPlayer(getActivity(), route); } } catch (Exception ex) { debug("Error handling presentation", ex); @@ -205,6 +235,17 @@ public class MediaPlayerManager extends Fragment implements NativeEventListener return null; } + private GeckoPresentationDisplay getPresentationDisplayForRoute(MediaRouter.RouteInfo route) { + try { + if (route.supportsControlCategory(CastMediaControlIntent.categoryForCast(ChromeCastDisplay.REMOTE_DISPLAY_APP_ID))) { + return new ChromeCastDisplay(getActivity(), route); + } + } catch (Exception ex) { + debug("Error handling presentation", ex); + } + return null; + } + @Override public void onPause() { super.onPause(); @@ -225,7 +266,8 @@ public class MediaPlayerManager extends Fragment implements NativeEventListener final MediaRouteSelector selectorBuilder = new MediaRouteSelector.Builder() .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO) .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) - .addControlCategory(CastMediaControlIntent.categoryForCast(ChromeCast.MIRROR_RECEIVER_APP_ID)) + .addControlCategory(CastMediaControlIntent.categoryForCast(ChromeCastPlayer.MIRROR_RECEIVER_APP_ID)) + .addControlCategory(CastMediaControlIntent.categoryForCast(ChromeCastDisplay.REMOTE_DISPLAY_APP_ID)) .build(); mediaRouter.addCallback(selectorBuilder, callback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY); } diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index 053a4c760132..2d79f7a7cf11 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -839,8 +839,10 @@ moz_native_devices_jars = [ CONFIG['ANDROID_PLAY_SERVICES_CAST_AAR_LIB'], ] moz_native_devices_sources = ['java/org/mozilla/gecko/' + x for x in [ - 'ChromeCast.java', + 'ChromeCastDisplay.java', + 'ChromeCastPlayer.java', 'GeckoMediaPlayer.java', + 'GeckoPresentationDisplay.java', 'MediaPlayerManager.java', 'PresentationMediaPlayerManager.java', ]]