diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java index 1521171255a8..f8d172d31c24 100644 --- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -8,6 +8,7 @@ package org.mozilla.gecko; import java.io.File; import java.io.FileNotFoundException; import java.lang.Override; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.URLEncoder; import java.util.EnumSet; @@ -16,6 +17,7 @@ import java.util.List; import java.util.Locale; import java.util.Vector; +import android.support.v4.app.Fragment; import org.json.JSONException; import org.json.JSONObject; @@ -651,16 +653,6 @@ public class BrowserApp extends GeckoApp // Set the maximum bits-per-pixel the favicon system cares about. IconDirectoryEntry.setMaxBPP(GeckoAppShell.getScreenDepth()); - Class mediaManagerClass = getMediaPlayerManager(); - if (mediaManagerClass != null) { - try { - Method init = mediaManagerClass.getMethod("init", Context.class); - init.invoke(null, this); - } catch(Exception ex) { - Log.e(LOGTAG, "Error initializing media manager", ex); - } - } - mTilesRecorder = new TilesRecorder(); } @@ -1163,16 +1155,6 @@ public class BrowserApp extends GeckoApp } } - Class mediaManagerClass = getMediaPlayerManager(); - if (mediaManagerClass != null) { - try { - Method destroy = mediaManagerClass.getMethod("onDestroy", (Class[]) null); - destroy.invoke(null); - } catch(Exception ex) { - Log.e(LOGTAG, "Error destroying media manager", ex); - } - } - super.onDestroy(); } @@ -1599,6 +1581,29 @@ public class BrowserApp extends GeckoApp } }); + if (AppConstants.MOZ_MEDIA_PLAYER) { + // Check if the fragment is already added. This should never be true here, but this is + // a nice safety check. + // If casting is disabled, these classes aren't built. We use reflection to initialize them. + final Class mediaManagerClass = getMediaPlayerManager(); + + if (mediaManagerClass != null) { + try { + final String tag = ""; + mediaManagerClass.getDeclaredField("MEDIA_PLAYER_TAG").get(tag); + Log.i(LOGTAG, "Found tag " + tag); + final Fragment frag = getSupportFragmentManager().findFragmentByTag(tag); + if (frag == null) { + final Method getInstance = mediaManagerClass.getMethod("newInstance", (Class[]) null); + final Fragment mpm = (Fragment) getInstance.invoke(null); + getSupportFragmentManager().beginTransaction().disallowAddToBackStack().add(mpm, tag).commit(); + } + } catch (Exception ex) { + Log.e(LOGTAG, "Error initializing media manager", ex); + } + } + } + if (AppConstants.MOZ_STUMBLER_BUILD_TIME_ENABLED) { // Start (this acts as ping if started already) the stumbler lib; if the stumbler has queued data it will upload it. // Stumbler operates on its own thread, and startup impact is further minimized by delaying work (such as upload) a few seconds. @@ -1611,6 +1616,7 @@ public class BrowserApp extends GeckoApp } }, oneSecondInMillis); } + super.handleMessage(event, message); } else if (event.equals("Gecko:Ready")) { // Handle this message in GeckoApp, but also enable the Settings diff --git a/mobile/android/base/ChromeCast.java b/mobile/android/base/ChromeCast.java index feb73f2f8d81..5c1370ade71d 100644 --- a/mobile/android/base/ChromeCast.java +++ b/mobile/android/base/ChromeCast.java @@ -161,7 +161,7 @@ class ChromeCast implements GeckoMediaPlayer { public ChromeCast(Context context, RouteInfo route) { int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context); if (status != ConnectionResult.SUCCESS) { - throw new IllegalStateException("Play services are required for Chromecast support (go status code " + status + ")"); + throw new IllegalStateException("Play services are required for Chromecast support (got status code " + status + ")"); } this.context = context; diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java index 02a2cbe3ea55..339d90871c63 100644 --- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -1914,7 +1914,7 @@ public abstract class GeckoApp } if (mAppStateListeners != null) { - for (GeckoAppShell.AppStateListener listener: mAppStateListeners) { + for (GeckoAppShell.AppStateListener listener : mAppStateListeners) { listener.onResume(); } } @@ -1946,7 +1946,7 @@ public abstract class GeckoApp Log.w(LOGTAG, "Can't record session: rec is null."); } } - }); + }); } @Override @@ -1996,7 +1996,7 @@ public abstract class GeckoApp }); if (mAppStateListeners != null) { - for(GeckoAppShell.AppStateListener listener: mAppStateListeners) { + for (GeckoAppShell.AppStateListener listener : mAppStateListeners) { listener.onPause(); } } diff --git a/mobile/android/base/GeckoAppShell.java b/mobile/android/base/GeckoAppShell.java index 3e0206dafb6d..449bdaa2b038 100644 --- a/mobile/android/base/GeckoAppShell.java +++ b/mobile/android/base/GeckoAppShell.java @@ -1175,7 +1175,7 @@ public class GeckoAppShell } final Uri uri = normalizeUriScheme(targetURI.indexOf(':') >= 0 ? Uri.parse(targetURI) : new Uri.Builder().scheme(targetURI).build()); - if (mimeType.length() > 0) { + if (!TextUtils.isEmpty(mimeType)) { Intent intent = getIntentForActionString(action); intent.setDataAndType(uri, mimeType); return intent; @@ -1191,8 +1191,10 @@ public class GeckoAppShell // custom handlers that would apply. // Start with the original URI. If we end up modifying it, we'll // overwrite it. + final String extension = MimeTypeMap.getFileExtensionFromUrl(targetURI); + final String mimeType2 = getMimeTypeFromExtension(extension); final Intent intent = getIntentForActionString(action); - intent.setData(uri); + intent.setDataAndType(uri, mimeType2); if ("vnd.youtube".equals(scheme) && !hasHandlersForIntent(intent) && diff --git a/mobile/android/base/MediaPlayerManager.java b/mobile/android/base/MediaPlayerManager.java index 223f81833184..5ebf2ac91a50 100644 --- a/mobile/android/base/MediaPlayerManager.java +++ b/mobile/android/base/MediaPlayerManager.java @@ -5,35 +5,44 @@ package org.mozilla.gecko; -import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.mozglue.JNITarget; -import org.mozilla.gecko.util.NativeEventListener; -import org.mozilla.gecko.util.NativeJSObject; - -import org.json.JSONArray; -import org.json.JSONObject; -import org.json.JSONException; - -import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; import android.support.v7.media.MediaControlIntent; import android.support.v7.media.MediaRouteSelector; import android.support.v7.media.MediaRouter; import android.support.v7.media.MediaRouter.RouteInfo; import android.util.Log; - import com.google.android.gms.cast.CastMediaControlIntent; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.mozglue.JNITarget; +import org.mozilla.gecko.util.EventCallback; +import org.mozilla.gecko.util.NativeEventListener; +import org.mozilla.gecko.util.NativeJSObject; import java.util.HashMap; -import java.util.Map; import java.util.Iterator; +import java.util.Map; /* Manages a list of GeckoMediaPlayers methods (i.e. Chromecast/Miracast). Routes messages * from Gecko to the correct caster based on the id of the display */ -class MediaPlayerManager implements NativeEventListener, - GeckoAppShell.AppStateListener { +public class MediaPlayerManager extends Fragment implements NativeEventListener { + /** + * Create a new instance of DetailsFragment, initialized to + * show the text at 'index'. + */ + @JNITarget + public static MediaPlayerManager newInstance() { + return new MediaPlayerManager(); + } + private static final String LOGTAG = "GeckoMediaPlayerManager"; + @JNITarget + public static final String MEDIA_PLAYER_TAG = "MPManagerFragment"; + private static final boolean SHOW_DEBUG = false; // Simplified debugging interfaces private static void debug(String msg, Exception e) { @@ -48,62 +57,36 @@ class MediaPlayerManager implements NativeEventListener, } } - private final Context context; - private final MediaRouter mediaRouter; + private MediaRouter mediaRouter = null; private final Map displays = new HashMap(); - private static MediaPlayerManager instance; - @JNITarget - public static void init(Context context) { - if (instance != null) { - debug("MediaPlayerManager initialized twice"); - return; - } - - instance = new MediaPlayerManager(context); - } - - private MediaPlayerManager(Context context) { - this.context = context; - - if (context instanceof GeckoApp) { - GeckoApp app = (GeckoApp) context; - app.addAppStateListener(this); - } - - mediaRouter = MediaRouter.getInstance(context); + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); EventDispatcher.getInstance().registerGeckoThreadListener(this, - "MediaPlayer:Load", - "MediaPlayer:Start", - "MediaPlayer:Stop", - "MediaPlayer:Play", - "MediaPlayer:Pause", - "MediaPlayer:Get", - "MediaPlayer:End", - "MediaPlayer:Mirror", - "MediaPlayer:Message"); + "MediaPlayer:Load", + "MediaPlayer:Start", + "MediaPlayer:Stop", + "MediaPlayer:Play", + "MediaPlayer:Pause", + "MediaPlayer:End", + "MediaPlayer:Mirror", + "MediaPlayer:Message"); } + @Override @JNITarget - public static void onDestroy() { - if (instance == null) { - return; - } - - EventDispatcher.getInstance().unregisterGeckoThreadListener(instance, + public void onDestroy() { + super.onDestroy(); + EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "MediaPlayer:Load", "MediaPlayer:Start", "MediaPlayer:Stop", "MediaPlayer:Play", "MediaPlayer:Pause", - "MediaPlayer:Get", "MediaPlayer:End", "MediaPlayer:Mirror", "MediaPlayer:Message"); - if (instance.context instanceof GeckoApp) { - GeckoApp app = (GeckoApp) instance.context; - app.removeAppStateListener(instance); - } } // GeckoEventListener implementation @@ -111,37 +94,6 @@ class MediaPlayerManager implements NativeEventListener, public void handleMessage(String event, final NativeJSObject message, final EventCallback callback) { debug(event); - if ("MediaPlayer:Get".equals(event)) { - final JSONObject result = new JSONObject(); - final JSONArray disps = new JSONArray(); - - final Iterator items = displays.values().iterator(); - while (items.hasNext()) { - GeckoMediaPlayer disp = items.next(); - try { - JSONObject json = disp.toJSON(); - if (json == null) { - items.remove(); - } else { - disps.put(json); - } - } catch(Exception ex) { - // This may happen if the device isn't a real Chromecast, - // for example Matchstick casting devices. - Log.e(LOGTAG, "Couldn't create JSON for display", ex); - } - } - - try { - result.put("displays", disps); - } catch(JSONException ex) { - Log.i(LOGTAG, "Error sending displays", ex); - } - - callback.sendSuccess(result); - return; - } - 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); @@ -179,6 +131,8 @@ class MediaPlayerManager implements NativeEventListener, public void onRouteRemoved(MediaRouter router, RouteInfo route) { debug("onRouteRemoved: route=" + route); displays.remove(route.getId()); + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent( + "MediaPlayer:Removed", route.getId())); } @SuppressWarnings("unused") @@ -201,18 +155,22 @@ class MediaPlayerManager implements NativeEventListener, @Override public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) { debug("onRouteAdded: route=" + route); - GeckoMediaPlayer display = getMediaPlayerForRoute(route); + final GeckoMediaPlayer display = getMediaPlayerForRoute(route); if (display != null) { displays.put(route.getId(), display); + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent( + "MediaPlayer:Added", display.toJSON().toString())); } } @Override public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) { debug("onRouteChanged: route=" + route); - GeckoMediaPlayer display = displays.get(route.getId()); + final GeckoMediaPlayer display = displays.get(route.getId()); if (display != null) { displays.put(route.getId(), display); + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent( + "MediaPlayer:Changed", display.toJSON().toString())); } } }; @@ -220,7 +178,7 @@ class MediaPlayerManager implements NativeEventListener, private GeckoMediaPlayer getMediaPlayerForRoute(MediaRouter.RouteInfo route) { try { if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) { - return new ChromeCast(context, route); + return new ChromeCast(getActivity(), route); } } catch(Exception ex) { debug("Error handling presentation", ex); @@ -229,23 +187,28 @@ class MediaPlayerManager implements NativeEventListener, return null; } - /* Implementing GeckoAppShell.AppStateListener */ @Override public void onPause() { + super.onPause(); mediaRouter.removeCallback(callback); + mediaRouter = null; } @Override public void onResume() { - MediaRouteSelector selectorBuilder = new MediaRouteSelector.Builder() + super.onResume(); + + // The mediaRouter shouldn't exist here, but this is a nice safety check. + if (mediaRouter != null) { + return; + } + + mediaRouter = MediaRouter.getInstance(getActivity()); + final MediaRouteSelector selectorBuilder = new MediaRouteSelector.Builder() .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO) .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) .addControlCategory(CastMediaControlIntent.categoryForCast(ChromeCast.MIRROR_RECEIVER_APP_ID)) .build(); mediaRouter.addCallback(selectorBuilder, callback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY); } - - @Override - public void onOrientationChanged() { } - } diff --git a/mobile/android/chrome/content/CastingApps.js b/mobile/android/chrome/content/CastingApps.js index 8f0336e6ff80..2f649ec92567 100644 --- a/mobile/android/chrome/content/CastingApps.js +++ b/mobile/android/chrome/content/CastingApps.js @@ -43,7 +43,35 @@ var mediaPlayerDevice = { return new MediaPlayerApp(aService); }, types: ["video/mp4", "video/webm", "application/x-mpegurl"], - extensions: ["mp4", "webm", "m3u", "m3u8"] + extensions: ["mp4", "webm", "m3u", "m3u8"], + init: function() { + Services.obs.addObserver(this, "MediaPlayer:Added", false); + Services.obs.addObserver(this, "MediaPlayer:Changed", false); + Services.obs.addObserver(this, "MediaPlayer:Removed", false); + }, + observe: function(subject, topic, data) { + if (topic === "MediaPlayer:Added") { + let service = this.toService(JSON.parse(data)); + SimpleServiceDiscovery.addService(service); + } else if (topic === "MediaPlayer:Changed") { + let service = this.toService(JSON.parse(data)); + SimpleServiceDiscovery.updateService(service); + } else if (topic === "MediaPlayer:Removed") { + SimpleServiceDiscovery.removeService(data); + } + }, + toService: function(display) { + // Convert the native data into something matching what is created in _processService() + return { + location: display.location, + target: "media:router", + friendlyName: display.friendlyName, + uuid: display.uuid, + manufacturer: display.manufacturer, + modelName: display.modelName, + mirror: display.mirror + }; + } }; var CastingApps = { @@ -59,6 +87,9 @@ var CastingApps = { // Register targets SimpleServiceDiscovery.registerDevice(rokuDevice); SimpleServiceDiscovery.registerDevice(matchstickDevice); + + // MediaPlayerDevice will notify us any time the native device list changes. + mediaPlayerDevice.init(); SimpleServiceDiscovery.registerDevice(mediaPlayerDevice); // Search for devices continuously every 120 seconds diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 31f255c2d73e..2a417d4aacab 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -339,6 +339,7 @@ var BrowserApp = { Services.tm.mainThread.dispatch(function() { // Init LoginManager Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); + CastingApps.init(); }, Ci.nsIThread.DISPATCH_NORMAL); #ifdef MOZ_SAFE_BROWSING @@ -440,7 +441,6 @@ var BrowserApp = { RemoteDebugger.init(); UserAgentOverrides.init(); DesktopUserAgent.init(); - CastingApps.init(); Distribution.init(); Tabs.init(); #ifdef ACCESSIBILITY diff --git a/python/mozbuild/mozbuild/backend/templates/android_eclipse/project.properties b/python/mozbuild/mozbuild/backend/templates/android_eclipse/project.properties index f34d1d0180bc..2106d9646aac 100644 --- a/python/mozbuild/mozbuild/backend/templates/android_eclipse/project.properties +++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse/project.properties @@ -9,6 +9,6 @@ # project structure. # Project target. -target=android-@ANDROID_TARGET_SDK@ +target=android-L @IDE_PROJECT_LIBRARY_SETTING@ @IDE_PROJECT_LIBRARY_REFERENCES@ diff --git a/toolkit/devtools/server/actors/webbrowser.js b/toolkit/devtools/server/actors/webbrowser.js index 70924e4c82d6..c35073744a5e 100644 --- a/toolkit/devtools/server/actors/webbrowser.js +++ b/toolkit/devtools/server/actors/webbrowser.js @@ -285,7 +285,8 @@ BrowserTabList.prototype._getBrowsers = function*() { }; BrowserTabList.prototype._getChildren = function(aWindow) { - return aWindow.gBrowser ? aWindow.gBrowser.browsers : []; + let children = aWindow.gBrowser ? aWindow.gBrowser.browsers : []; + return children ? children : []; }; BrowserTabList.prototype._isRemoteBrowser = function(browser) { diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build index bd5cb1636f48..39126fec36bd 100644 --- a/toolkit/modules/moz.build +++ b/toolkit/modules/moz.build @@ -43,6 +43,7 @@ EXTRA_JS_MODULES += [ 'RemoteSecurityUI.jsm', 'RemoteWebNavigation.jsm', 'RemoteWebProgress.jsm', + 'secondscreen/SimpleServiceDiscovery.jsm', 'SelectContentHelper.jsm', 'SelectParentHelper.jsm', 'sessionstore/FormData.jsm', @@ -63,7 +64,6 @@ EXTRA_PP_JS_MODULES += [ 'CertUtils.jsm', 'ResetProfile.jsm', 'secondscreen/RokuApp.jsm', - 'secondscreen/SimpleServiceDiscovery.jsm', 'Services.jsm', 'Troubleshoot.jsm', 'UpdateChannel.jsm', diff --git a/toolkit/modules/secondscreen/SimpleServiceDiscovery.jsm b/toolkit/modules/secondscreen/SimpleServiceDiscovery.jsm index f1a600774684..7b1f3e54d4b0 100644 --- a/toolkit/modules/secondscreen/SimpleServiceDiscovery.jsm +++ b/toolkit/modules/secondscreen/SimpleServiceDiscovery.jsm @@ -12,17 +12,8 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Timer.jsm"); -#ifdef ANDROID -Cu.import("resource://gre/modules/Messaging.jsm"); -#endif -// Define the "log" function as a binding of the Log.d function so it specifies -// the "debug" priority and a log tag. -#ifdef ANDROID -let log = Cu.import("resource://gre/modules/AndroidLog.jsm",{}).AndroidLog.d.bind(null, "SSDP"); -#else let log = Cu.reportError; -#endif XPCOMUtils.defineLazyGetter(this, "converter", function () { let conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter); @@ -188,36 +179,8 @@ var SimpleServiceDiscovery = { timeout += SSDP_TRANSMISSION_INTERVAL; } } - -#ifdef ANDROID - // We also query Java directly here for any devices that Android might support natively (i.e. Chromecast or Miracast) - this.getAndroidDevices(); -#endif }, -#ifdef ANDROID - getAndroidDevices: function() { - Messaging.sendRequestForResult({ type: "MediaPlayer:Get" }).then((result) => { - for (let id in result.displays) { - let display = result.displays[id]; - - // Convert the native data into something matching what is created in _processService() - let service = { - location: display.location, - target: "media:router", - friendlyName: display.friendlyName, - uuid: display.uuid, - manufacturer: display.manufacturer, - modelName: display.modelName, - mirror: display.mirror - }; - - this._addService(service); - } - }); - }, -#endif - _searchFixedDevices: function _searchFixedDevices() { let fixedDevices = null; try { @@ -259,8 +222,7 @@ var SimpleServiceDiscovery = { // Clean out any stale services for (let [key, service] of this._services) { if (service.lastPing != this._searchTimestamp) { - Services.obs.notifyObservers(null, EVENT_SERVICE_LOST, service.uuid); - this._services.delete(service.uuid); + this.removeService(service.uuid); } } } @@ -399,29 +361,52 @@ var SimpleServiceDiscovery = { aService.manufacturer = doc.querySelector("manufacturer").textContent; aService.modelName = doc.querySelector("modelName").textContent; - this._addService(aService); + this.addService(aService); } }).bind(this), false); xhr.send(null); }, + // Add a service to the WeakMap, even if one already exists with this id. + // Returns true if this succeeded or false if it failed _addService: function(service) { // Filter out services that do not match the device filter if (!this._filterService(service)) { - return; + return false; } + let device = this._devices.get(service.target); + if (device && device.mirror) { + service.mirror = true; + } + this._services.set(service.uuid, service); + return true; + }, + + addService: function(service) { // Only add and notify if we don't already know about this service if (!this._services.has(service.uuid)) { - let device = this._devices.get(service.target); - if (device && device.mirror) { - service.mirror = true; + if (!this._addService(service)) { + return; } - this._services.set(service.uuid, service); Services.obs.notifyObservers(null, EVENT_SERVICE_FOUND, service.uuid); } + // Make sure we remember this service is not stale + this._services.get(service.uuid).lastPing = this._searchTimestamp; + }, + + removeService: function(uuid) { + Services.obs.notifyObservers(null, EVENT_SERVICE_LOST, uuid); + this._services.delete(uuid); + }, + + updateService: function(service) { + if (!this._addService(service)) { + return; + } + // Make sure we remember this service is not stale this._services.get(service.uuid).lastPing = this._searchTimestamp; }