Merge fx-team to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Tue, 30 Dec 2014 13:32:22 -0800
changeset 247403 69b64e65fbb203acd9e5fd931e13e0a9150ccb0d
parent 247398 1d9ecea73a1e000f03a344682fb30377f4625616 (current diff)
parent 247402 d075ae34162cacffb737b0130bac1f3765f7636b (diff)
child 247514 88037f94b7d77d58f73b9b58d7c1c6235a966ca9
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone37.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c a=merge
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -3,24 +3,26 @@
  * 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 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;
 import java.util.HashSet;
 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;
 
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.DynamicToolbar.PinReason;
 import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.Tabs.TabEvents;
@@ -646,26 +648,16 @@ public class BrowserApp extends GeckoApp
             public void onEnabledChanged(boolean enabled) {
                 setDynamicToolbarEnabled(enabled);
             }
         });
 
         // 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();
     }
 
     private void setupSystemUITinting() {
         if (!Versions.feature19Plus) {
             return;
         }
 
@@ -1158,26 +1150,16 @@ public class BrowserApp extends GeckoApp
             if (nfc != null) {
                 // null this out even though the docs say it's not needed,
                 // because the source code looks like it will only do this
                 // automatically on API 14+
                 nfc.setNdefPushMessageCallback(null, this);
             }
         }
 
-        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();
     }
 
     @Override
     protected void initializeChrome() {
         super.initializeChrome();
 
         mDoorHangerPopup.setAnchor(mBrowserToolbar.getDoorHangerAnchor());
@@ -1594,28 +1576,52 @@ public class BrowserApp extends GeckoApp
                     @Override
                     public void run() {
                         // Force tabs panel inflation once the initial
                         // pageload is finished.
                         ensureTabsPanelExists();
                     }
                 });
 
+                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.
                     // Avoid any potential startup CPU/thread contention by delaying the pref broadcast.
                     final long oneSecondInMillis = 1000;
                     ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() {
                         @Override
                         public void run() {
                              GeckoPreferences.broadcastStumblerPref(BrowserApp.this);
                         }
                     }, oneSecondInMillis);
                 }
+
                 super.handleMessage(event, message);
             } else if (event.equals("Gecko:Ready")) {
                 // Handle this message in GeckoApp, but also enable the Settings
                 // menuitem, which is specific to BrowserApp.
                 super.handleMessage(event, message);
                 final Menu menu = mMenu;
                 ThreadUtils.postToUiThread(new Runnable() {
                     @Override
--- a/mobile/android/base/ChromeCast.java
+++ b/mobile/android/base/ChromeCast.java
@@ -156,17 +156,17 @@ class ChromeCast implements GeckoMediaPl
 
             sendError(callback, "");
         }
     }
 
     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;
         this.route = route;
         this.canMirror = route.supportsControlCategory(CastMediaControlIntent.categoryForCast(MIRROR_RECEIVER_APP_ID));
     }
 
     // This dumps everything we can find about the device into JSON. This will hopefully make it
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -1909,17 +1909,17 @@ public abstract class GeckoApp
         if (!Versions.feature14Plus) {
             // Update accessibility settings in case it has been changed by the
             // user. On API14+, this is handled in LayerView by registering an
             // accessibility state change listener.
             GeckoAccessibility.updateAccessibilitySettings(this);
         }
 
         if (mAppStateListeners != null) {
-            for (GeckoAppShell.AppStateListener listener: mAppStateListeners) {
+            for (GeckoAppShell.AppStateListener listener : mAppStateListeners) {
                 listener.onResume();
             }
         }
 
         // We use two times: a pseudo-unique wall-clock time to identify the
         // current session across power cycles, and the elapsed realtime to
         // track the duration of the session.
         final long now = System.currentTimeMillis();
@@ -1941,17 +1941,17 @@ public abstract class GeckoApp
                 final HealthRecorder rec = mHealthRecorder;
                 if (rec != null) {
                     rec.setCurrentSession(currentSession);
                     rec.processDelayed();
                 } else {
                     Log.w(LOGTAG, "Can't record session: rec is null.");
                 }
             }
-         });
+        });
     }
 
     @Override
     public void onWindowFocusChanged(boolean hasFocus) {
         super.onWindowFocusChanged(hasFocus);
 
         if (!mInitialized && hasFocus) {
             initialize();
@@ -1991,17 +1991,17 @@ public abstract class GeckoApp
                 // In theory, the first browser session will not run long enough that we need to
                 // prune during it and we'd rather run it when the browser is inactive so we wait
                 // until here to register the prune service.
                 GeckoPreferences.broadcastHealthReportPrune(context);
             }
         });
 
         if (mAppStateListeners != null) {
-            for(GeckoAppShell.AppStateListener listener: mAppStateListeners) {
+            for (GeckoAppShell.AppStateListener listener : mAppStateListeners) {
                 listener.onPause();
             }
         }
 
         super.onPause();
     }
 
     @Override
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -1170,34 +1170,36 @@ public class GeckoAppShell
 
         if (action.equalsIgnoreCase(Intent.ACTION_SEND)) {
             Intent shareIntent = getShareIntent(context, targetURI, mimeType, title);
             return Intent.createChooser(shareIntent,
                                         context.getResources().getString(R.string.share_title)); 
         }
 
         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;
         }
 
         if (!isUriSafeForScheme(uri)) {
             return null;
         }
 
         final String scheme = uri.getScheme();
 
         // Compute our most likely intent, then check to see if there are any
         // 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) &&
             !TextUtils.isEmpty(uri.getSchemeSpecificPart())) {
 
             // Return an intent with a URI that will open the YouTube page in the
             // current Fennec instance.
             final Class<?> c;
--- a/mobile/android/base/MediaPlayerManager.java
+++ b/mobile/android/base/MediaPlayerManager.java
@@ -1,152 +1,104 @@
 /* -*- 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.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.Iterator;
 import java.util.Map;
-import java.util.Iterator;
 
 /* 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) {
         if (SHOW_DEBUG) {
             Log.e(LOGTAG, msg, e);
         }
     }
 
     private static void debug(String msg) {
         if (SHOW_DEBUG) {
             Log.d(LOGTAG, msg);
         }
     }
 
-    private final Context context;
-    private final MediaRouter mediaRouter;
+    private MediaRouter mediaRouter = null;
     private final Map<String, GeckoMediaPlayer> displays = new HashMap<String, GeckoMediaPlayer>();
-    private static MediaPlayerManager instance;
 
-    @JNITarget
-    public static void init(Context context) {
-        if (instance != null) {
-            debug("MediaPlayerManager initialized twice");
-            return;
-        }
-
-        instance = new MediaPlayerManager(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:End",
+                "MediaPlayer:Mirror",
+                "MediaPlayer:Message");
     }
 
-    private MediaPlayerManager(Context context) {
-        this.context = context;
-
-        if (context instanceof GeckoApp) {
-            GeckoApp app = (GeckoApp) context;
-            app.addAppStateListener(this);
-        }
-
-        mediaRouter = MediaRouter.getInstance(context);
-        EventDispatcher.getInstance().registerGeckoThreadListener(this,
-                                                                  "MediaPlayer:Load",
-                                                                  "MediaPlayer:Start",
-                                                                  "MediaPlayer:Stop",
-                                                                  "MediaPlayer:Play",
-                                                                  "MediaPlayer:Pause",
-                                                                  "MediaPlayer:Get",
-                                                                  "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
     @Override
     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<GeckoMediaPlayer> 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);
             if (callback != null) {
                 callback.sendError(null);
             }
             return;
         }
@@ -174,16 +126,18 @@ class MediaPlayerManager implements Nati
     }
 
     private final MediaRouter.Callback callback =
         new MediaRouter.Callback() {
             @Override
             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")
             public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo route) {
             }
 
             // These methods aren't used by the support version Media Router
             @SuppressWarnings("unused")
@@ -196,56 +150,65 @@ class MediaPlayerManager implements Nati
 
             @Override
             public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) {
             }
 
             @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()));
                 }
             }
         };
 
     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);
         }
 
         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() { }
-
 }
--- a/mobile/android/chrome/content/CastingApps.js
+++ b/mobile/android/chrome/content/CastingApps.js
@@ -38,32 +38,63 @@ var matchstickDevice = {
 var mediaPlayerDevice = {
   id: "media:router",
   target: "media:router",
   factory: function(aService) {
     Cu.import("resource://gre/modules/MediaPlayerApp.jsm");
     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 = {
   _castMenuId: -1,
   mirrorStartMenuId: -1,
   mirrorStopMenuId: -1,
 
   init: function ca_init() {
     if (!this.isCastingEnabled()) {
       return;
     }
 
     // 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
     SimpleServiceDiscovery.search(120 * 1000);
 
     this._castMenuId = NativeWindow.contextmenus.add(
       Strings.browser.GetStringFromName("contextmenu.sendToDevice"),
       this.filterCast,
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -334,16 +334,17 @@ var BrowserApp = {
         BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false);
         Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
         Messaging.sendRequest({ type: "Gecko:DelayedStartup" });
 
         // Queue up some other performance-impacting initializations
         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
         Services.tm.mainThread.dispatch(function() {
           // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008.
           SafeBrowsing.init();
         }, Ci.nsIThread.DISPATCH_NORMAL);
 #endif
@@ -435,17 +436,16 @@ var BrowserApp = {
     CharacterEncoding.init();
     ActivityObserver.init();
     // TODO: replace with Android implementation of WebappOSUtils.isLaunchable.
     Cu.import("resource://gre/modules/Webapps.jsm");
     DOMApplicationRegistry.allAppsLaunchable = true;
     RemoteDebugger.init();
     UserAgentOverrides.init();
     DesktopUserAgent.init();
-    CastingApps.init();
     Distribution.init();
     Tabs.init();
 #ifdef ACCESSIBILITY
     AccessFu.attach(window);
 #endif
 #ifdef NIGHTLY_BUILD
     ShumwayUtils.init();
 #endif
--- a/python/mozbuild/mozbuild/backend/templates/android_eclipse/project.properties
+++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse/project.properties
@@ -4,11 +4,11 @@
 #
 # This file must be checked in Version Control Systems.
 #
 # To customize properties used by the Ant build system edit
 # "ant.properties", and override values to adapt the script to your
 # project structure.
 
 # Project target.
-target=android-@ANDROID_TARGET_SDK@
+target=android-L
 @IDE_PROJECT_LIBRARY_SETTING@
 @IDE_PROJECT_LIBRARY_REFERENCES@
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -280,17 +280,18 @@ BrowserTabList.prototype._getBrowsers = 
     // browser.contentWindow as the debuggee global.
     for (let browser of this._getChildren(win)) {
       yield browser;
     }
   }
 };
 
 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) {
   return browser.getAttribute("remote") == "true";
 };
 
 BrowserTabList.prototype.getList = function() {
   let topXULWindow = Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType);
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -38,16 +38,17 @@ EXTRA_JS_MODULES += [
     'Promise.jsm',
     'PromiseUtils.jsm',
     'PropertyListUtils.jsm',
     'RemoteController.jsm',
     'RemoteFinder.jsm',
     'RemoteSecurityUI.jsm',
     'RemoteWebNavigation.jsm',
     'RemoteWebProgress.jsm',
+    'secondscreen/SimpleServiceDiscovery.jsm',
     'SelectContentHelper.jsm',
     'SelectParentHelper.jsm',
     'sessionstore/FormData.jsm',
     'sessionstore/ScrollPosition.jsm',
     'sessionstore/XPathGenerator.jsm',
     'ShortcutUtils.jsm',
     'Sntp.jsm',
     'SpatialNavigation.jsm',
@@ -58,17 +59,16 @@ EXTRA_JS_MODULES += [
     'WebChannel.jsm',
     'ZipUtils.jsm',
 ]
 
 EXTRA_PP_JS_MODULES += [
     'CertUtils.jsm',
     'ResetProfile.jsm',
     'secondscreen/RokuApp.jsm',
-    'secondscreen/SimpleServiceDiscovery.jsm',
     'Services.jsm',
     'Troubleshoot.jsm',
     'UpdateChannel.jsm',
     'WindowDraggingUtils.jsm',
     'WindowsPrefSync.jsm',
 ]
 
 if 'Android' != CONFIG['OS_TARGET']:
--- a/toolkit/modules/secondscreen/SimpleServiceDiscovery.jsm
+++ b/toolkit/modules/secondscreen/SimpleServiceDiscovery.jsm
@@ -7,27 +7,18 @@
 
 this.EXPORTED_SYMBOLS = ["SimpleServiceDiscovery"];
 
 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);
   conv.charset = "utf8";
   return conv;
 });
 
 // Spec information:
@@ -183,46 +174,18 @@ var SimpleServiceDiscovery = {
             socket.send(SSDP_ADDRESS, SSDP_PORT, msgRaw, msgRaw.length);
           } catch (e) {
             log("failed to convert to byte array: " + e);
           }
         }, timeout);
         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 {
       fixedDevices = Services.prefs.getCharPref("browser.casting.fixedDevices");
     } catch (e) {}
 
     if (!fixedDevices) {
       return;
@@ -254,18 +217,17 @@ var SimpleServiceDiscovery = {
   _searchShutdown: function _searchShutdown() {
     if (this._searchSocket) {
       // This will call onStopListening.
       this._searchSocket.close();
 
       // 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);
         }
       }
     }
   },
 
   getSupportedExtensions: function() {
     let extensions = [];
     this.services.forEach(function(service) {
@@ -394,35 +356,58 @@ var SimpleServiceDiscovery = {
         aService.appsURL = xhr.getResponseHeader("Application-URL");
         if (aService.appsURL && !aService.appsURL.endsWith("/"))
           aService.appsURL += "/";
         aService.friendlyName = doc.querySelector("friendlyName").textContent;
         aService.uuid = doc.querySelector("UDN").textContent;
         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;
   }
 }