merge fx-team to mozilla-central
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Thu, 07 Nov 2013 14:24:24 +0100
changeset 168495 f73dd492c34c
parent 168478 21b77163bf9f (current diff)
parent 168494 9f1168eae1b7 (diff)
child 168529 7433abfef863
push id3224
push userlsblakk@mozilla.com
push dateTue, 04 Feb 2014 01:06:49 +0000
treeherdermozilla-beta@60c04d0987f1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone28.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 mozilla-central
Makefile.in
--- a/Makefile.in
+++ b/Makefile.in
@@ -67,17 +67,17 @@ config.status: $(topsrcdir)/configure
 # this main make file because having it in rules.mk and applied to partial tree
 # builds resulted in a world of hurt. Gory details are in bug 877308.
 #
 # The mach build driver will ensure the backend is up to date for partial tree
 # builds. This cleanly avoids most of the pain.
 
 backend.RecursiveMakeBackend:
 	@echo "Build configuration changed. Regenerating backend."
-	./config.status
+	$(PYTHON) config.status
 
 Makefile: backend.RecursiveMakeBackend
 	@$(TOUCH) $@
 
 include backend.RecursiveMakeBackend.pp
 
 default:: backend.RecursiveMakeBackend
 
--- a/dom/webidl/Document.webidl
+++ b/dom/webidl/Document.webidl
@@ -146,17 +146,17 @@ partial interface Document {
                 attribute EventHandler oncut;
                 attribute EventHandler onpaste;
                 attribute EventHandler onbeforescriptexecute;
                 attribute EventHandler onafterscriptexecute;
   /**
    * True if this document is synthetic : stand alone image, video, audio file,
    * etc.
    */
-  [ChromeOnly] readonly attribute boolean mozSyntheticDocument;
+  [Func="IsChromeOrXBL"] readonly attribute boolean mozSyntheticDocument;
   /**
    * Returns the script element whose script is currently being processed.
    *
    * @see <https://developer.mozilla.org/en/DOM/document.currentScript>
    */
   readonly attribute Element? currentScript;
   /**
    * Release the current mouse capture if it is on an element within this
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -707,28 +707,43 @@ abstract public class BrowserApp extends
 
         if (itemId == R.id.share) {
             shareCurrentUrl();
             return true;
         }
 
         if (itemId == R.id.subscribe) {
             Tab tab = Tabs.getInstance().getSelectedTab();
-            if (tab != null && tab.getFeedsEnabled()) {
+            if (tab != null && tab.hasFeeds()) {
                 JSONObject args = new JSONObject();
                 try {
                     args.put("tabId", tab.getId());
                 } catch (JSONException e) {
                     Log.e(LOGTAG, "error building json arguments");
                 }
                 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Feeds:Subscribe", args.toString()));
             }
             return true;
         }
 
+        if (itemId == R.id.add_search_engine) {
+            Tab tab = Tabs.getInstance().getSelectedTab();
+            if (tab != null && tab.hasOpenSearch()) {
+                JSONObject args = new JSONObject();
+                try {
+                    args.put("tabId", tab.getId());
+                } catch (JSONException e) {
+                    Log.e(LOGTAG, "error building json arguments");
+                    return true;
+                }
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Add", args.toString()));
+            }
+            return true;
+        }
+
         if (itemId == R.id.copyurl) {
             Tab tab = Tabs.getInstance().getSelectedTab();
             if (tab != null) {
                 String url = tab.getURL();
                 if (url != null) {
                     Clipboard.setText(url);
                 }
             }
--- a/mobile/android/base/BrowserToolbar.java
+++ b/mobile/android/base/BrowserToolbar.java
@@ -372,25 +372,29 @@ public class BrowserToolbar extends Geck
                 Tab tab = Tabs.getInstance().getSelectedTab();
                 if (tab != null) {
                     String url = tab.getURL();
                     if (url == null) {
                         menu.findItem(R.id.copyurl).setVisible(false);
                         menu.findItem(R.id.share).setVisible(false);
                         menu.findItem(R.id.add_to_launcher).setVisible(false);
                     }
-                    if (!tab.getFeedsEnabled()) {
+
+                    if (!tab.hasFeeds()) {
                         menu.findItem(R.id.subscribe).setVisible(false);
                     }
+
+                    menu.findItem(R.id.add_search_engine).setVisible(tab.hasOpenSearch());
                 } else {
                     // if there is no tab, remove anything tab dependent
                     menu.findItem(R.id.copyurl).setVisible(false);
                     menu.findItem(R.id.share).setVisible(false);
                     menu.findItem(R.id.add_to_launcher).setVisible(false);
                     menu.findItem(R.id.subscribe).setVisible(false);
+                    menu.findItem(R.id.add_search_engine).setVisible(false);
                 }
 
                 menu.findItem(R.id.share).setVisible(!GeckoProfile.get(getContext()).inGuestMode());
             }
         });
 
         mUrlEditText.addTextChangedListener(this);
 
--- a/mobile/android/base/GeckoView.java
+++ b/mobile/android/base/GeckoView.java
@@ -8,37 +8,45 @@ package org.mozilla.gecko;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.mozglue.GeckoLoader;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.ThreadUtils;
 
+import org.json.JSONException;
 import org.json.JSONObject;
 
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
 public class GeckoView extends LayerView
     implements GeckoEventListener, ContextGetter {
 
     private static final String LOGTAG = "GeckoView";
 
+    private ChromeDelegate mChromeDelegate;
+    private ContentDelegate mContentDelegate;
+
     public GeckoView(Context context, AttributeSet attrs) {
         super(context, attrs);
         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GeckoView);
         String url = a.getString(R.styleable.GeckoView_url);
         boolean doInit = a.getBoolean(R.styleable.GeckoView_doinit, true);
         a.recycle();
 
         if (!doInit)
@@ -71,52 +79,476 @@ public class GeckoView extends LayerView
             GeckoThread.setAction(Intent.ACTION_VIEW);
             GeckoAppShell.sendEventToGecko(GeckoEvent.createURILoadEvent(url));
         }
         GeckoAppShell.setContextGetter(this);
         if (context instanceof Activity) {
             Tabs tabs = Tabs.getInstance();
             tabs.attachToContext(context);
         }
+
         GeckoAppShell.registerEventListener("Gecko:Ready", this);
+        GeckoAppShell.registerEventListener("Content:StateChange", this);
+        GeckoAppShell.registerEventListener("Content:LoadError", this);
+        GeckoAppShell.registerEventListener("Content:PageShow", this);
+        GeckoAppShell.registerEventListener("DOMTitleChanged", this);
+        GeckoAppShell.registerEventListener("Link:Favicon", this);
+        GeckoAppShell.registerEventListener("Prompt:Show", this);
+        GeckoAppShell.registerEventListener("Prompt:ShowTop", this);
 
         ThreadUtils.setUiThread(Thread.currentThread(), new Handler());
         initializeView(GeckoAppShell.getEventDispatcher());
 
         GeckoProfile profile = GeckoProfile.get(context).forceCreate();
         BrowserDB.initialize(profile.getName());
 
         if (GeckoThread.checkAndSetLaunchState(GeckoThread.LaunchState.Launching, GeckoThread.LaunchState.Launched)) {
             GeckoAppShell.setLayerView(this);
             GeckoThread.createAndStart();
         }
     }
 
-    public void loadUrl(String uri) {
-        Tabs.getInstance().loadUrl(uri);
+    /**
+    * Add a Browser to the GeckoView container.
+    * @param url The URL resource to load into the new Browser.
+    */
+    public Browser addBrowser(String url) {
+        Tab tab = Tabs.getInstance().loadUrl(url, Tabs.LOADURL_NEW_TAB);
+        if (tab != null) {
+            return new Browser(tab.getId());
+        }
+        return null;
+    }
+
+    /**
+    * Remove a Browser from the GeckoView container.
+    * @param browser The Browser to remove.
+    */
+    public void removeBrowser(Browser browser) {
+        Tab tab = Tabs.getInstance().getTab(browser.getId());
+        if (tab != null) {
+            Tabs.getInstance().closeTab(tab);
+        }
+    }
+
+    /**
+    * Set the active/visible Browser.
+    * @param browser The Browser to make selected.
+    */
+    public void setCurrentBrowser(Browser browser) {
+        Tab tab = Tabs.getInstance().getTab(browser.getId());
+        if (tab != null) {
+            Tabs.getInstance().selectTab(tab.getId());
+        }
+    }
+
+    /**
+    * Get the active/visible Browser.
+    * @return The current selected Browser.
+    */
+    public Browser getCurrentBrowser() {
+        Tab tab = Tabs.getInstance().getSelectedTab();
+        if (tab != null) {
+            return new Browser(tab.getId());
+        }
+        return null;
+    }
+
+    /**
+    * Get the list of current Browsers in the GeckoView container.
+    * @return An unmodifiable List of Browser objects.
+    */
+    public List<Browser> getBrowsers() {
+        ArrayList<Browser> browsers = new ArrayList<Browser>();
+        Iterable<Tab> tabs = Tabs.getInstance().getTabsInOrder();
+        for (Tab tab : tabs) {
+            browsers.add(new Browser(tab.getId()));
+        }
+        return Collections.unmodifiableList(browsers);
+    }
+
+    /**
+    * Not part of the public API. Ignore.
+    */
+    public void handleMessage(final String event, final JSONObject message) {
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    if (event.equals("Gecko:Ready")) {
+                        GeckoView.this.handleReady(message);
+                    } else if (event.equals("Content:StateChange")) {
+                        GeckoView.this.handleStateChange(message);
+                    } else if (event.equals("Content:LoadError")) {
+                        GeckoView.this.handleLoadError(message);
+                    } else if (event.equals("Content:PageShow")) {
+                        GeckoView.this.handlePageShow(message);
+                    } else if (event.equals("DOMTitleChanged")) {
+                        GeckoView.this.handleTitleChanged(message);
+                    } else if (event.equals("Link:Favicon")) {
+                        GeckoView.this.handleLinkFavicon(message);
+                    } else if (event.equals("Prompt:Show") || event.equals("Prompt:ShowTop")) {
+                        GeckoView.this.handlePrompt(message);
+                    }
+                } catch (Exception e) {
+                    Log.w(LOGTAG, "handleMessage threw for " + event, e);
+                }
+            }
+        });
     }
 
-    public void loadUrlInNewTab(String uri) {
-        Tabs.getInstance().loadUrl(uri, Tabs.LOADURL_NEW_TAB);
-     }
+    private void handleReady(final JSONObject message) {
+        GeckoThread.setLaunchState(GeckoThread.LaunchState.GeckoRunning);
+        Tab selectedTab = Tabs.getInstance().getSelectedTab();
+        if (selectedTab != null)
+            Tabs.getInstance().notifyListeners(selectedTab, Tabs.TabEvents.SELECTED);
+        geckoConnected();
+        GeckoAppShell.setLayerClient(getLayerClient());
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Viewport:Flush", null));
+        show();
+        requestRender();
+
+        if (mChromeDelegate != null) {
+            mChromeDelegate.onReady(this);
+        }
+    }
+
+    private void handleStateChange(final JSONObject message) throws JSONException {
+        int state = message.getInt("state");
+        if ((state & GeckoAppShell.WPL_STATE_IS_NETWORK) != 0) {
+            if ((state & GeckoAppShell.WPL_STATE_START) != 0) {
+                if (mContentDelegate != null) {
+                    int id = message.getInt("tabID");
+                    mContentDelegate.onPageStart(this, new Browser(id), message.getString("uri"));
+                }
+            } else if ((state & GeckoAppShell.WPL_STATE_STOP) != 0) {
+                if (mContentDelegate != null) {
+                    int id = message.getInt("tabID");
+                    mContentDelegate.onPageStop(this, new Browser(id), message.getBoolean("success"));
+                }
+            }
+        }
+    }
+
+    private void handleLoadError(final JSONObject message) throws JSONException {
+        if (mContentDelegate != null) {
+            int id = message.getInt("tabID");
+            mContentDelegate.onPageStop(GeckoView.this, new Browser(id), false);
+        }
+    }
+
+    private void handlePageShow(final JSONObject message) throws JSONException {
+        if (mContentDelegate != null) {
+            int id = message.getInt("tabID");
+            mContentDelegate.onPageShow(GeckoView.this, new Browser(id));
+        }
+    }
 
-    public void handleMessage(String event, JSONObject message) {
-        if (event.equals("Gecko:Ready")) {
-            GeckoThread.setLaunchState(GeckoThread.LaunchState.GeckoRunning);
-            Tab selectedTab = Tabs.getInstance().getSelectedTab();
-            if (selectedTab != null)
-                Tabs.getInstance().notifyListeners(selectedTab, Tabs.TabEvents.SELECTED);
-            geckoConnected();
-            GeckoAppShell.setLayerClient(getLayerClient());
-            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Viewport:Flush", null));
-            show();
-            requestRender();
+    private void handleTitleChanged(final JSONObject message) throws JSONException {
+        if (mContentDelegate != null) {
+            int id = message.getInt("tabID");
+            mContentDelegate.onReceivedTitle(GeckoView.this, new Browser(id), message.getString("title"));
+        }
+    }
+
+    private void handleLinkFavicon(final JSONObject message) throws JSONException {
+        if (mContentDelegate != null) {
+            int id = message.getInt("tabID");
+            mContentDelegate.onReceivedFavicon(GeckoView.this, new Browser(id), message.getString("href"), message.getInt("size"));
         }
     }
 
+    private void handlePrompt(final JSONObject message) throws JSONException {
+        if (mChromeDelegate != null) {
+            String hint = message.optString("hint");
+            if ("alert".equals(hint)) {
+                String text = message.optString("text");
+                mChromeDelegate.onAlert(GeckoView.this, null, text, new PromptResult(message.optString("guid")));
+            } else if ("confirm".equals(hint)) {
+                String text = message.optString("text");
+                mChromeDelegate.onConfirm(GeckoView.this, null, text, new PromptResult(message.optString("guid")));
+            } else if ("prompt".equals(hint)) {
+                String text = message.optString("text");
+                String defaultValue = message.optString("textbox0");
+                mChromeDelegate.onPrompt(GeckoView.this, null, text, defaultValue, new PromptResult(message.optString("guid")));
+            } else if ("remotedebug".equals(hint)) {
+                mChromeDelegate.onDebugRequest(GeckoView.this, new PromptResult(message.optString("guid")));
+            }
+        }
+    }
+
+    /**
+    * Set the chrome callback handler.
+    * This will replace the current handler.
+    * @param chrome An implementation of GeckoViewChrome.
+    */
+    public void setChromeDelegate(ChromeDelegate chrome) {
+        mChromeDelegate = chrome;
+    }
+
+    /**
+    * Set the content callback handler.
+    * This will replace the current handler.
+    * @param content An implementation of ContentDelegate.
+    */
+    public void setContentDelegate(ContentDelegate content) {
+        mContentDelegate = content;
+    }
+
     public static void setGeckoInterface(final BaseGeckoInterface geckoInterface) {
         GeckoAppShell.setGeckoInterface(geckoInterface);
     }
 
     public static GeckoAppShell.GeckoInterface getGeckoInterface() {
         return GeckoAppShell.getGeckoInterface();
     }
+
+    /**
+    * Wrapper for a browser in the GeckoView container. Associated with a browser
+    * element in the Gecko system.
+    */
+    public class Browser {
+        private final int mId;
+        private Browser(int Id) {
+            mId = Id;
+        }
+
+        /**
+        * Get the ID of the Browser. This is the same ID used by Gecko for it's underlying
+        * browser element.
+        * @return The integer ID of the Browser.
+        */
+        private int getId() {
+            return mId;
+        }
+
+        /**
+        * Load a URL resource into the Browser.
+        * @param url The URL string.
+        */
+        public void loadUrl(String url) {
+            JSONObject args = new JSONObject();
+            try {
+                args.put("url", url);
+                args.put("parentId", -1);
+                args.put("newTab", false);
+                args.put("tabID", mId);
+            } catch (Exception e) {
+                Log.w(LOGTAG, "Error building JSON arguments for loadUrl.", e);
+            }
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Load", args.toString()));
+        }
+
+        /**
+        * Reload the current URL resource into the Browser. The URL is force loaded from the
+        * network and is not pulled from cache.
+        */
+        public void reload() {
+            Tab tab = Tabs.getInstance().getTab(mId);
+            if (tab != null) {
+                tab.doReload();
+            }
+        }
+
+        /**
+        * Stop the current loading operation.
+        */
+        public void stop() {
+            Tab tab = Tabs.getInstance().getTab(mId);
+            if (tab != null) {
+                tab.doStop();
+            }
+        }
+
+        /**
+        * Check to see if the Browser has session history and can go back to a
+        * previous page.
+        * @return A boolean flag indicating if previous session exists.
+        * This method will likely be removed and replaced by a callback in GeckoViewContent
+        */
+        public boolean canGoBack() {
+            Tab tab = Tabs.getInstance().getTab(mId);
+            if (tab != null) {
+                return tab.canDoBack();
+            }
+            return false;
+        }
+
+        /**
+        * Move backward in the session history, if that's possible.
+        */
+        public void goBack() {
+            Tab tab = Tabs.getInstance().getTab(mId);
+            if (tab != null) {
+                tab.doBack();
+            }
+        }
+
+        /**
+        * Check to see if the Browser has session history and can go forward to a
+        * new page.
+        * @return A boolean flag indicating if forward session exists.
+        * This method will likely be removed and replaced by a callback in GeckoViewContent
+        */
+        public boolean canGoForward() {
+            Tab tab = Tabs.getInstance().getTab(mId);
+            if (tab != null) {
+                return tab.canDoForward();
+            }
+            return false;
+        }
+
+        /**
+        * Move forward in the session history, if that's possible.
+        */
+        public void goForward() {
+            Tab tab = Tabs.getInstance().getTab(mId);
+            if (tab != null) {
+                tab.doForward();
+            }
+        }
+    }
+
+    /* Provides a means for the client to indicate whether a JavaScript
+     * dialog request should proceed. An instance of this class is passed to
+     * various GeckoViewChrome callback actions.
+     */
+    public class PromptResult {
+        private final int RESULT_OK = 0;
+        private final int RESULT_CANCEL = 1;
+
+        private final String mGUID;
+
+        public PromptResult(String guid) {
+            mGUID = guid;
+        }
+
+        private JSONObject makeResult(int resultCode) {
+            JSONObject result = new JSONObject();
+            try {
+                result.put("guid", mGUID);
+                result.put("button", resultCode);
+            } catch(JSONException ex) { }
+            return result;
+        }
+
+        /**
+        * Handle a confirmation response from the user.
+        */
+        public void confirm() {
+            JSONObject result = makeResult(RESULT_OK);
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Prompt:Reply", result.toString()));
+        }
+
+        /**
+        * Handle a confirmation response from the user.
+        * @param value String value to return to the browser context.
+        */
+        public void confirmWithValue(String value) {
+            JSONObject result = makeResult(RESULT_OK);
+            try {
+                result.put("textbox0", value);
+            } catch(JSONException ex) { }
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Prompt:Reply", result.toString()));
+        }
+
+        /**
+        * Handle a cancellation response from the user.
+        */
+        public void cancel() {
+            JSONObject result = makeResult(RESULT_CANCEL);
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Prompt:Reply", result.toString()));
+        }
+    }
+
+    public interface ChromeDelegate {
+        /**
+        * Tell the host application that Gecko is ready to handle requests.
+        * @param view The GeckoView that initiated the callback.
+        */
+        public void onReady(GeckoView view);
+
+        /**
+        * Tell the host application to display an alert dialog.
+        * @param view The GeckoView that initiated the callback.
+        * @param browser The Browser that is loading the content.
+        * @param message The string to display in the dialog.
+        * @param result A PromptResult used to send back the result without blocking.
+        * Defaults to cancel requests.
+        */
+        public void onAlert(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result);
+    
+        /**
+        * Tell the host application to display a confirmation dialog.
+        * @param view The GeckoView that initiated the callback.
+        * @param browser The Browser that is loading the content.
+        * @param message The string to display in the dialog.
+        * @param result A PromptResult used to send back the result without blocking.
+        * Defaults to cancel requests.
+        */
+        public void onConfirm(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result);
+    
+        /**
+        * Tell the host application to display an input prompt dialog.
+        * @param view The GeckoView that initiated the callback.
+        * @param browser The Browser that is loading the content.
+        * @param message The string to display in the dialog.
+        * @param defaultValue The string to use as default input.
+        * @param result A PromptResult used to send back the result without blocking.
+        * Defaults to cancel requests.
+        */
+        public void onPrompt(GeckoView view, GeckoView.Browser browser, String message, String defaultValue, GeckoView.PromptResult result);
+    
+        /**
+        * Tell the host application to display a remote debugging request dialog.
+        * @param view The GeckoView that initiated the callback.
+        * @param result A PromptResult used to send back the result without blocking.
+        * Defaults to cancel requests.
+        */
+        public void onDebugRequest(GeckoView view, GeckoView.PromptResult result);
+    }
+
+    public interface ContentDelegate {
+        /**
+        * A Browser has started loading content from the network.
+        * @param view The GeckoView that initiated the callback.
+        * @param browser The Browser that is loading the content.
+        * @param url The resource being loaded.
+        */
+        public void onPageStart(GeckoView view, GeckoView.Browser browser, String url);
+    
+        /**
+        * A Browser has finished loading content from the network.
+        * @param view The GeckoView that initiated the callback.
+        * @param browser The Browser that was loading the content.
+        * @param success Whether the page loaded successfully or an error occured.
+        */
+        public void onPageStop(GeckoView view, GeckoView.Browser browser, boolean success);
+    
+        /**
+        * A Browser is displaying content. This page could have been loaded via
+        * network or from the session history.
+        * @param view The GeckoView that initiated the callback.
+        * @param browser The Browser that is showing the content.
+        */
+        public void onPageShow(GeckoView view, GeckoView.Browser browser);
+    
+        /**
+        * A page title was discovered in the content or updated after the content
+        * loaded.
+        * @param view The GeckoView that initiated the callback.
+        * @param browser The Browser that is showing the content.
+        * @param title The title sent from the content.
+        */
+        public void onReceivedTitle(GeckoView view, GeckoView.Browser browser, String title);
+    
+        /**
+        * A link element was discovered in the content or updated after the content
+        * loaded that specifies a favicon.
+        * @param view The GeckoView that initiated the callback.
+        * @param browser The Browser that is showing the content.
+        * @param url The href of the link element specifying the favicon.
+        * @param size The maximum size specified for the favicon, or -1 for any size.
+        */
+        public void onReceivedFavicon(GeckoView view, GeckoView.Browser browser, String url, int size);
+    }
+
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/GeckoViewChrome.java
@@ -0,0 +1,61 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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;
+
+public class GeckoViewChrome implements GeckoView.ChromeDelegate {
+    /**
+    * Tell the host application that Gecko is ready to handle requests.
+    * @param view The GeckoView that initiated the callback.
+    */
+    public void onReady(GeckoView view) {}
+
+    /**
+    * Tell the host application to display an alert dialog.
+    * @param view The GeckoView that initiated the callback.
+    * @param browser The Browser that is loading the content.
+    * @param message The string to display in the dialog.
+    * @param result A PromptResult used to send back the result without blocking.
+    * Defaults to cancel requests.
+    */
+    public void onAlert(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result) {
+        result.cancel();
+    }
+
+    /**
+    * Tell the host application to display a confirmation dialog.
+    * @param view The GeckoView that initiated the callback.
+    * @param browser The Browser that is loading the content.
+    * @param message The string to display in the dialog.
+    * @param result A PromptResult used to send back the result without blocking.
+    * Defaults to cancel requests.
+    */
+    public void onConfirm(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result) {
+        result.cancel();
+    }
+
+    /**
+    * Tell the host application to display an input prompt dialog.
+    * @param view The GeckoView that initiated the callback.
+    * @param browser The Browser that is loading the content.
+    * @param message The string to display in the dialog.
+    * @param defaultValue The string to use as default input.
+    * @param result A PromptResult used to send back the result without blocking.
+    * Defaults to cancel requests.
+    */
+    public void onPrompt(GeckoView view, GeckoView.Browser browser, String message, String defaultValue, GeckoView.PromptResult result) {
+        result.cancel();
+    }
+
+    /**
+    * Tell the host application to display a remote debugging request dialog.
+    * @param view The GeckoView that initiated the callback.
+    * @param result A PromptResult used to send back the result without blocking.
+    * Defaults to cancel requests.
+    */
+    public void onDebugRequest(GeckoView view, GeckoView.PromptResult result) {
+        result.cancel();
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/GeckoViewContent.java
@@ -0,0 +1,51 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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;
+
+public class GeckoViewContent implements GeckoView.ContentDelegate {
+    /**
+    * A Browser has started loading content from the network.
+    * @param view The GeckoView that initiated the callback.
+    * @param browser The Browser that is loading the content.
+    * @param url The resource being loaded.
+    */
+    public void onPageStart(GeckoView view, GeckoView.Browser browser, String url) {}
+
+    /**
+    * A Browser has finished loading content from the network.
+    * @param view The GeckoView that initiated the callback.
+    * @param browser The Browser that was loading the content.
+    * @param success Whether the page loaded successfully or an error occured.
+    */
+    public void onPageStop(GeckoView view, GeckoView.Browser browser, boolean success) {}
+
+    /**
+    * A Browser is displaying content. This page could have been loaded via
+    * network or from the session history.
+    * @param view The GeckoView that initiated the callback.
+    * @param browser The Browser that is showing the content.
+    */
+    public void onPageShow(GeckoView view, GeckoView.Browser browser) {}
+
+    /**
+    * A page title was discovered in the content or updated after the content
+    * loaded.
+    * @param view The GeckoView that initiated the callback.
+    * @param browser The Browser that is showing the content.
+    * @param title The title sent from the content.
+    */
+    public void onReceivedTitle(GeckoView view, GeckoView.Browser browser, String title) {}
+
+    /**
+    * A link element was discovered in the content or updated after the content
+    * loaded that specifies a favicon.
+    * @param view The GeckoView that initiated the callback.
+    * @param browser The Browser that is showing the content.
+    * @param url The href of the link element specifying the favicon.
+    * @param size The maximum size specified for the favicon, or -1 for any size.
+    */
+    public void onReceivedFavicon(GeckoView view, GeckoView.Browser browser, String url, int size) {}
+}
--- a/mobile/android/base/Tab.java
+++ b/mobile/android/base/Tab.java
@@ -38,17 +38,18 @@ public class Tab {
     private long mLastUsed;
     private String mUrl;
     private String mBaseDomain;
     private String mUserSearch;
     private String mTitle;
     private Bitmap mFavicon;
     private String mFaviconUrl;
     private int mFaviconSize;
-    private boolean mFeedsEnabled;
+    private boolean mHasFeeds;
+    private boolean mHasOpenSearch;
     private JSONObject mIdentityData;
     private boolean mReaderEnabled;
     private BitmapDrawable mThumbnail;
     private int mHistoryIndex;
     private int mHistorySize;
     private int mParentId;
     private HomePager.Page mAboutHomePage;
     private boolean mExternal;
@@ -93,17 +94,18 @@ public class Tab {
         mUserSearch = "";
         mExternal = external;
         mParentId = parentId;
         mAboutHomePage = HomePager.Page.TOP_SITES;
         mTitle = title == null ? "" : title;
         mFavicon = null;
         mFaviconUrl = null;
         mFaviconSize = 0;
-        mFeedsEnabled = false;
+        mHasFeeds = false;
+        mHasOpenSearch = false;
         mIdentityData = null;
         mReaderEnabled = false;
         mEnteringReaderMode = false;
         mThumbnail = null;
         mHistoryIndex = -1;
         mHistorySize = 0;
         mBookmark = false;
         mReadingListItem = false;
@@ -231,18 +233,22 @@ public class Tab {
             }
         });
     }
 
     public synchronized String getFaviconURL() {
         return mFaviconUrl;
     }
 
-    public boolean getFeedsEnabled() {
-        return mFeedsEnabled;
+    public boolean hasFeeds() {
+        return mHasFeeds;
+    }
+
+    public boolean hasOpenSearch() {
+        return mHasOpenSearch;
     }
 
     public String getSecurityMode() {
         try {
             return mIdentityData.getString("mode");
         } catch (Exception e) {
             // If mIdentityData is null, or we get a JSONException
             return SiteIdentityPopup.UNKNOWN;
@@ -388,18 +394,22 @@ public class Tab {
         if (mEnteringReaderMode)
             return;
 
         mFavicon = null;
         mFaviconUrl = null;
         mFaviconSize = 0;
     }
 
-    public void setFeedsEnabled(boolean feedsEnabled) {
-        mFeedsEnabled = feedsEnabled;
+    public void setHasFeeds(boolean hasFeeds) {
+        mHasFeeds = hasFeeds;
+    }
+
+    public void setHasOpenSearch(boolean hasOpenSearch) {
+        mHasOpenSearch = hasOpenSearch;
     }
 
     public void updateIdentityData(JSONObject identityData) {
         mIdentityData = identityData;
     }
 
     public void setReaderEnabled(boolean readerEnabled) {
         mReaderEnabled = readerEnabled;
@@ -617,17 +627,17 @@ public class Tab {
             // We can get a location change event for the same document with an anchor tag
             // Notify listeners so that buttons like back or forward will update themselves
             Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.LOCATION_CHANGE, uri);
             return;
         }
 
         setContentType(message.getString("contentType"));
         clearFavicon();
-        setFeedsEnabled(false);
+        setHasFeeds(false);
         updateTitle(null);
         updateIdentityData(null);
         setReaderEnabled(false);
         setZoomConstraints(new ZoomConstraints(true));
         setHasTouchListeners(false);
         setBackgroundColor(DEFAULT_BACKGROUND_COLOR);
         setErrorType(ErrorType.NONE);
 
--- a/mobile/android/base/Tabs.java
+++ b/mobile/android/base/Tabs.java
@@ -97,16 +97,17 @@ public class Tabs implements GeckoEventL
         registerEventListener("Content:ReaderEnabled");
         registerEventListener("Content:StateChange");
         registerEventListener("Content:LoadError");
         registerEventListener("Content:PageShow");
         registerEventListener("DOMContentLoaded");
         registerEventListener("DOMTitleChanged");
         registerEventListener("Link:Favicon");
         registerEventListener("Link:Feed");
+        registerEventListener("Link:OpenSearch");
         registerEventListener("DesktopMode:Changed");
         registerEventListener("Tab:ViewportMetadata");
     }
 
     public synchronized void attachToContext(Context context) {
         final Context appContext = context.getApplicationContext();
         if (mAppContext == appContext) {
             return;
@@ -462,18 +463,21 @@ public class Tabs implements GeckoEventL
                 tab.setErrorType(message.optString("errorType"));
                 notifyListeners(tab, Tabs.TabEvents.LOADED);
             } else if (event.equals("DOMTitleChanged")) {
                 tab.updateTitle(message.getString("title"));
             } else if (event.equals("Link:Favicon")) {
                 tab.updateFaviconURL(message.getString("href"), message.getInt("size"));
                 notifyListeners(tab, TabEvents.LINK_FAVICON);
             } else if (event.equals("Link:Feed")) {
-                tab.setFeedsEnabled(true);
+                tab.setHasFeeds(true);
                 notifyListeners(tab, TabEvents.LINK_FEED);
+            } else if (event.equals("Link:OpenSearch")) {
+                boolean visible = message.getBoolean("visible");
+                tab.setHasOpenSearch(visible);
             } else if (event.equals("DesktopMode:Changed")) {
                 tab.setDesktopMode(message.getBoolean("desktopMode"));
                 notifyListeners(tab, TabEvents.DESKTOP_MODE_CHANGE);
             } else if (event.equals("Tab:ViewportMetadata")) {
                 tab.setZoomConstraints(new ZoomConstraints(message));
                 tab.setIsRTL(message.getBoolean("isRTL"));
                 notifyListeners(tab, TabEvents.VIEWPORT_CHANGE);
             }
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -223,16 +223,17 @@ size. -->
 <!ENTITY contextmenu_paste "Paste">
 <!ENTITY contextmenu_copyurl "Copy Address">
 <!ENTITY contextmenu_edit_bookmark "Edit">
 <!ENTITY contextmenu_subscribe "Subscribe to Page">
 <!ENTITY contextmenu_site_settings "Edit Site Settings">
 <!ENTITY contextmenu_top_sites_edit "Edit">
 <!ENTITY contextmenu_top_sites_pin "Pin Site">
 <!ENTITY contextmenu_top_sites_unpin "Unpin Site">
+<!ENTITY contextmenu_add_search_engine "Add a Search Engine">
 
 <!ENTITY pref_titlebar_mode "Title bar">
 <!ENTITY pref_titlebar_mode_title "Show page title">
 <!ENTITY pref_titlebar_mode_url "Show page address">
 
 <!ENTITY history_removed "Page removed">
 
 <!ENTITY bookmark_edit_title "Edit Bookmark">
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -143,16 +143,18 @@ gbjar.sources += [
     'GeckoPreferences.java',
     'GeckoPreferenceFragment.java',
     'GeckoProfile.java',
     'GeckoSmsManager.java',
     'GeckoThread.java',
     'GeckoJavaSampler.java',
     'GlobalHistory.java',
     'GeckoView.java',
+    'GeckoViewChrome.java',
+    'GeckoViewContent.java',
     'health/BrowserHealthRecorder.java',
     'health/BrowserHealthReporter.java',
     'InputMethods.java',
     'JavaAddonManager.java',
     'LightweightTheme.java',
     'LightweightThemeDrawable.java',
     'LinkPreference.java',
     'MemoryMonitor.java',
--- a/mobile/android/base/resources/menu/titlebar_contextmenu.xml
+++ b/mobile/android/base/resources/menu/titlebar_contextmenu.xml
@@ -12,16 +12,20 @@
           android:title="@string/contextmenu_paste"/>
 
     <item android:id="@+id/share"
           android:title="@string/contextmenu_share"/>
 
     <item android:id="@+id/subscribe"
           android:title="@string/contextmenu_subscribe"/>
 
+    <item android:id="@+id/add_search_engine"
+          android:title="@string/contextmenu_add_search_engine"
+          android:visible="false"/>
+
     <item android:id="@+id/copyurl"
           android:title="@string/contextmenu_copyurl"/>
 
     <item android:id="@+id/site_settings"
           android:title="@string/contextmenu_site_settings" />
 
     <item android:id="@+id/add_to_launcher"
           android:title="@string/contextmenu_add_to_launcher"/>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -223,16 +223,17 @@
   <string name="contextmenu_paste">&contextmenu_paste;</string>
   <string name="contextmenu_copyurl">&contextmenu_copyurl;</string>
   <string name="contextmenu_edit_bookmark">&contextmenu_edit_bookmark;</string>
   <string name="contextmenu_subscribe">&contextmenu_subscribe;</string>
   <string name="contextmenu_site_settings">&contextmenu_site_settings;</string>
   <string name="contextmenu_top_sites_edit">&contextmenu_top_sites_edit;</string>
   <string name="contextmenu_top_sites_pin">&contextmenu_top_sites_pin;</string>
   <string name="contextmenu_top_sites_unpin">&contextmenu_top_sites_unpin;</string>
+  <string name="contextmenu_add_search_engine">&contextmenu_add_search_engine;</string>
 
   <string name="pref_titlebar_mode">&pref_titlebar_mode;</string>
   <string name="pref_titlebar_mode_title">&pref_titlebar_mode_title;</string>
   <string name="pref_titlebar_mode_url">&pref_titlebar_mode_url;</string>
 
   <string name="history_removed">&history_removed;</string>
 
   <string name="bookmark_edit_title">&bookmark_edit_title;</string>
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -1392,20 +1392,27 @@ var BrowserApp = {
             params.postData = submission.postData;
           }
         }
 
         // Don't show progress throbber for about:home or about:reader
         if (!shouldShowProgress(url))
           params.showProgress = false;
 
-        if (data.newTab)
+        if (data.newTab) {
           this.addTab(url, params);
-        else
+        } else {
+          if (data.tabId) {
+            // Use a specific browser instead of the selected browser, if it exists
+            let specificBrowser = this.getTabForId(data.tabId).browser;
+            if (specificBrowser)
+              browser = specificBrowser;
+          }
           this.loadURI(url, browser, params);
+        }
         break;
       }
 
       case "Tab:Selected":
         this._handleTabSelected(this.getTabForId(parseInt(aData)));
         break;
 
       case "Tab:Closed":
@@ -3482,16 +3489,70 @@ Tab.prototype = {
             this.browser.feeds.push({ href: target.href, title: target.title, type: type });
 
             let json = {
               type: "Link:Feed",
               tabID: this.id
             };
             sendMessageToJava(json);
           } catch (e) {}
+        } else if (list.indexOf("[search]" != -1)) {
+          let type = target.type && target.type.toLowerCase();
+
+          // Replace all starting or trailing spaces or spaces before "*;" globally w/ "".
+          type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
+
+          // Check that type matches opensearch.
+          let isOpenSearch = (type == "application/opensearchdescription+xml");
+          if (isOpenSearch && target.title && /^(?:https?|ftp):/i.test(target.href)) {
+            let visibleEngines = Services.search.getVisibleEngines();
+            // NOTE: Engines are currently identified by name, but this can be changed
+            // when Engines are identified by URL (see bug 335102).
+            if (visibleEngines.some(function(e) {
+              return e.name == target.title;
+            })) {
+              // This engine is already present, do nothing.
+              return;
+            }
+
+            if (this.browser.engines) {
+              // This engine has already been handled, do nothing.
+              if (this.browser.engines.some(function(e) {
+                return e.url == target.href;
+              })) {
+                  return;
+              }
+            } else {
+              this.browser.engines = [];
+            }
+
+            // Get favicon.
+            let iconURL = target.ownerDocument.documentURIObject.prePath + "/favicon.ico";
+
+            let newEngine = {
+              title: target.title,
+              url: target.href,
+              iconURL: iconURL
+            };
+
+            this.browser.engines.push(newEngine);
+
+            // Don't send a message to display engines if we've already handled an engine.
+            if (this.browser.engines.length > 1)
+              return;
+
+            // Broadcast message that this tab contains search engines that should be visible.
+            let newEngineMessage = {
+              type: "Link:OpenSearch",
+              tabID: this.id,
+              visible: true
+            };
+
+            sendMessageToJava(newEngineMessage);
+          }
         }
         break;
       }
 
       case "DOMTitleChanged": {
         if (!aEvent.isTrusted)
           return;
 
@@ -3636,16 +3697,32 @@ Tab.prototype = {
     // Filter optimization: Only really send NETWORK state changes to Java listener
     if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
       if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && aWebProgress.isLoadingDocument) {
         // We may receive a document stop event while a document is still loading
         // (such as when doing URI fixup). Don't notify Java UI in these cases.
         return;
       }
 
+      // Clear page-specific opensearch engines and feeds for a new request.
+      if (aStateFlags & Ci.nsIWebProgressListener.STATE_START && aRequest && aWebProgress.isTopLevel) {
+          this.browser.engines = null;
+
+          // Send message to clear search engine option in context menu.
+          let newEngineMessage = {
+            type: "Link:OpenSearch",
+            tabID: this.id,
+            visible: false
+          };
+
+          sendMessageToJava(newEngineMessage);
+
+          this.browser.feeds = null;
+      }
+
       // Check to see if we restoring the content from a previous presentation (session)
       // since there should be no real network activity
       let restoring = aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING;
       let showProgress = restoring ? false : this.showProgress;
 
       // true if the page loaded successfully (i.e., no 404s or other errors)
       let success = false; 
       let uri = "";
@@ -6551,30 +6628,32 @@ OverscrollController.prototype = {
 };
 
 var SearchEngines = {
   _contextMenuId: null,
   PREF_SUGGEST_ENABLED: "browser.search.suggest.enabled",
   PREF_SUGGEST_PROMPTED: "browser.search.suggest.prompted",
 
   init: function init() {
+    Services.obs.addObserver(this, "SearchEngines:Add", false);
     Services.obs.addObserver(this, "SearchEngines:Get", false);
     Services.obs.addObserver(this, "SearchEngines:GetVisible", false);
     Services.obs.addObserver(this, "SearchEngines:SetDefault", false);
     Services.obs.addObserver(this, "SearchEngines:Remove", false);
     let contextName = Strings.browser.GetStringFromName("contextmenu.addSearchEngine");
     let filter = {
       matches: function (aElement) {
         return (aElement.form && NativeWindow.contextmenus.textContext.matches(aElement));
       }
     };
     this._contextMenuId = NativeWindow.contextmenus.add(contextName, filter, this.addEngine);
   },
 
   uninit: function uninit() {
+    Services.obs.removeObserver(this, "SearchEngines:Add");
     Services.obs.removeObserver(this, "SearchEngines:Get");
     Services.obs.removeObserver(this, "SearchEngines:GetVisible");
     Services.obs.removeObserver(this, "SearchEngines:SetDefault");
     Services.obs.removeObserver(this, "SearchEngines:Remove");
     if (this._contextMenuId != null)
       NativeWindow.contextmenus.remove(this._contextMenuId);
   },
 
@@ -6646,16 +6725,19 @@ var SearchEngines = {
   _extractEngineFromJSON: function _extractEngineFromJSON(aData) {
     let data = JSON.parse(aData);
     return Services.search.getEngineByName(data.engine);
   },
 
   observe: function observe(aSubject, aTopic, aData) {
     let engine;
     switch(aTopic) {
+      case "SearchEngines:Add":
+        this.displaySearchEnginesList(aData);
+        break;
       case "SearchEngines:GetVisible":
         Services.search.init(this._handleSearchEnginesGetVisible.bind(this));
         break;
       case "SearchEngines:Get":
         // Return a list of all engines, including "Hidden" ones.
         Services.search.init(this._handleSearchEnginesGetAll.bind(this));
         break;
       case "SearchEngines:SetDefault":
@@ -6672,16 +6754,60 @@ var SearchEngines = {
         Services.search.removeEngine(engine);
         break;
       default:
         dump("Unexpected message type observed: " + aTopic);
         break;
     }
   },
 
+  // Display context menu listing names of the search engines available to be added.
+  displaySearchEnginesList: function displaySearchEnginesList(aData) {
+    let data = JSON.parse(aData);
+    let tab = BrowserApp.getTabForId(data.tabId);
+
+    if (!tab)
+      return;
+
+    let browser = tab.browser;
+    let engines = browser.engines;
+
+    let p = new Prompt({
+      window: browser.contentWindow
+    }).setSingleChoiceItems(engines.map(function(e) {
+      return { label: e.title };
+    })).show((function(data) {
+      if (data.button == -1)
+        return;
+
+      this.addOpenSearchEngine(engines[data.button]);
+      engines.splice(data.button, 1);
+
+      if (engines.length < 1) {
+        // Broadcast message that there are no more add-able search engines.
+        let newEngineMessage = {
+          type: "Link:OpenSearch",
+          tabID: tab.id,
+          visible: false
+        };
+
+        sendMessageToJava(newEngineMessage);
+      }
+    }).bind(this));
+  },
+
+  addOpenSearchEngine: function addOpenSearchEngine(engine) {
+    Services.search.addEngine(engine.url, Ci.nsISearchEngine.DATA_XML, engine.iconURL, false, {
+      onSuccess: function() {
+        // Display a toast confirming addition of new search engine.
+        NativeWindow.toast.show(Strings.browser.formatStringFromName("alertSearchEngineAddedToast", [engine.title], 1), "long");
+      }
+    });
+  },
+
   addEngine: function addEngine(aElement) {
     let form = aElement.form;
     let charset = aElement.ownerDocument.characterSet;
     let docURI = Services.io.newURI(aElement.ownerDocument.URL, charset, null);
     let formURL = Services.io.newURI(form.getAttribute("action"), charset, docURI).spec;
     let method = form.method.toUpperCase();
     let formData = [];
 
@@ -7113,16 +7239,17 @@ var RemoteDebugger = {
     let msg = Strings.browser.GetStringFromName("remoteIncomingPromptMessage");
     let disable = Strings.browser.GetStringFromName("remoteIncomingPromptDisable");
     let cancel = Strings.browser.GetStringFromName("remoteIncomingPromptCancel");
     let agree = Strings.browser.GetStringFromName("remoteIncomingPromptAccept");
 
     // Make prompt. Note: button order is in reverse.
     let prompt = new Prompt({
       window: null,
+      hint: "remotedebug",
       title: title,
       message: msg,
       buttons: [ agree, cancel, disable ],
       priority: 1
     });
 
     // The debugger server expects a synchronous response, so spin on result since Prompt is async.
     let result = null;
--- a/mobile/android/components/PromptService.js
+++ b/mobile/android/components/PromptService.js
@@ -213,28 +213,30 @@ InternalPrompt.prototype = {
     else
       return this.nsIAuthPrompt_promptPassword.apply(this, arguments);
   },
 
   /* ----------  nsIPrompt  ---------- */
 
   alert: function alert(aTitle, aText) {
     let p = this._getPrompt(aTitle, aText, [ PromptUtils.getLocaleString("OK") ]);
+    p.setHint("alert");
     this.showPrompt(p);
   },
 
   alertCheck: function alertCheck(aTitle, aText, aCheckMsg, aCheckState) {
     let p = this._getPrompt(aTitle, aText, [ PromptUtils.getLocaleString("OK") ], aCheckMsg, aCheckState);
     let data = this.showPrompt(p);
     if (aCheckState && data.button > -1)
       aCheckState.value = data.checkbox0 == "true";
   },
 
   confirm: function confirm(aTitle, aText) {
     let p = this._getPrompt(aTitle, aText);
+    p.setHint("confirm");
     let data = this.showPrompt(p);
     return (data.button == 0);
   },
 
   confirmCheck: function confirmCheck(aTitle, aText, aCheckMsg, aCheckState) {
     let p = this._getPrompt(aTitle, aText, null, aCheckMsg, aCheckState);
     let data = this.showPrompt(p);
     let ok = data.button == 0;
@@ -286,16 +288,17 @@ InternalPrompt.prototype = {
     let data = this.showPrompt(p);
     if (aCheckState && data.button > -1)
       aCheckState.value = data.checkbox0 == "true";
     return data.button;
   },
 
   nsIPrompt_prompt: function nsIPrompt_prompt(aTitle, aText, aValue, aCheckMsg, aCheckState) {
     let p = this._getPrompt(aTitle, aText, null, aCheckMsg, aCheckState);
+    p.setHint("prompt");
     p.addTextbox({
       value: aValue.value,
       autofocus: true
     });
     let data = this.showPrompt(p);
 
     let ok = data.button == 0;
     if (aCheckState && data.button > -1)
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -17,16 +17,21 @@ alertDownloadsSize=Download too big
 alertDownloadsNoSpace=Not enough storage space
 alertDownloadsToast=Download started…
 alertDownloadsPause=Pause
 alertDownloadsResume=Resume
 alertDownloadsCancel=Cancel
 
 alertFullScreenToast=Press BACK to leave full-screen mode
 
+# LOCALIZATION NOTE (alertSearchEngineAddedToast)
+# %S will be replaced by the name of the search engine (exposed by the current page)
+# that has been added; for example, 'Google'.
+alertSearchEngineAddedToast='%S' has been added as a search engine
+
 downloadCancelPromptTitle=Cancel Download
 downloadCancelPromptMessage=Do you want to cancel this download?
 
 # LOCALIZATION NOTE (addonError-1, addonError-2, addonError-3, addonError-4):
 # #1 is the add-on name, #2 is the add-on host, #3 is the application name
 addonError-1=The add-on could not be downloaded because of a connection failure on #2.
 addonError-2=The add-on from #2 could not be installed because it does not match the add-on #3 expected.
 addonError-3=The add-on downloaded from #2 could not be installed because it appears to be corrupt.
--- a/mobile/android/modules/Prompt.jsm
+++ b/mobile/android/modules/Prompt.jsm
@@ -27,22 +27,33 @@ function Prompt(aOptions) {
     this.msg.title = aOptions.title;
 
   if ("message" in aOptions && aOptions.message != null)
     this.msg.text = aOptions.message;
 
   if ("buttons" in aOptions && aOptions.buttons != null)
     this.msg.buttons = aOptions.buttons;
 
+  if ("hint" in aOptions && aOptions.hint != null)
+    this.msg.hint = aOptions.hint;
+
   let idService = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); 
   this.guid = idService.generateUUID().toString();
   this.msg.guid = this.guid;
 }
 
 Prompt.prototype = {
+  setHint: function(aHint) {
+    if (!aHint)
+      delete this.msg.hint;
+    else
+      this.msg.hint = aHint;
+    return this;
+  },
+
   addButton: function(aOptions) {
     if (!this.msg.buttons)
       this.msg.buttons = [];
     this.msg.buttons.push(aOptions.label);
     return this;
   },
 
   _addInput: function(aOptions) {
--- a/netwerk/base/public/nsIBrowserSearchService.idl
+++ b/netwerk/base/public/nsIBrowserSearchService.idl
@@ -17,17 +17,17 @@ interface nsISearchSubmission : nsISuppo
   readonly attribute nsIInputStream postData;
 
   /**
    * The URI to submit a search to.
    */
   readonly attribute nsIURI uri;
 };
 
-[scriptable, uuid(ccf6aa20-10a9-4a0c-a81d-31b10ea846de)]
+[scriptable, uuid(7914c4b8-f05b-40c9-a982-38a058cd1769)]
 interface nsISearchEngine : nsISupports
 {
   /**
    * Gets a nsISearchSubmission object that contains information about what to
    * send to the search engine, including the URI and postData, if applicable.
    *
    * @param  data
    *         Data to add to the submission object.
@@ -79,16 +79,35 @@ interface nsISearchEngine : nsISupports
    * given responseType, false otherwise.
    *
    * @param responseType
    *        The MIME type to check for
    */
   boolean supportsResponseType(in AString responseType);
 
   /**
+   * Returns a string with the URL to an engine's icon matching both width and
+   * height. Returns null if icon with specified dimensions is not found.
+   *
+   * @param width
+   *        Width of the requested icon.
+   * @param height
+   *        Height of the requested icon.
+   */
+  AString getIconURLBySize(in long width, in long height);
+
+  /**
+   * Gets an array of all available icons. Each entry is an object with
+   * width, height and url properties. width and height are numeric and
+   * represent the icon's dimensions. url is a string with the URL for
+   * the icon.
+   */
+  jsval getIcons();
+
+  /**
    * Supported search engine types.
    */
   const unsigned long TYPE_MOZSEARCH     = 1;
   const unsigned long TYPE_SHERLOCK      = 2;
   const unsigned long TYPE_OPENSEARCH    = 3;
 
   /**
    * Supported search engine data types.
--- a/toolkit/components/search/nsSearchService.js
+++ b/toolkit/components/search/nsSearchService.js
@@ -1551,47 +1551,90 @@ Engine.prototype = {
 
     // Notify the callback if needed
     if (aEngine._installCallback) {
       aEngine._installCallback();
     }
   },
 
   /**
-   * Sets the .iconURI property of the engine.
+   * Creates a key by serializing an object that contains the icon's width
+   * and height.
+   *
+   * @param aWidth
+   *        Width of the icon.
+   * @param aHeight
+   *        Height of the icon.
+   * @returns key string
+   */
+  _getIconKey: function SRCH_ENG_getIconKey(aWidth, aHeight) {
+    let keyObj = {
+     width: aWidth,
+     height: aHeight
+    };
+
+    return JSON.stringify(keyObj);
+  },
+
+  /**
+   * Add an icon to the icon map used by getIconURIBySize() and getIcons().
+   *
+   * @param aWidth
+   *        Width of the icon.
+   * @param aHeight
+   *        Height of the icon.
+   * @param aURISpec
+   *        String with the icon's URI.
+   */
+  _addIconToMap: function SRCH_ENG_addIconToMap(aWidth, aHeight, aURISpec) {
+    // Use an object instead of a Map() because it needs to be serializable.
+    this._iconMapObj = this._iconMapObj || {};
+    let key = this._getIconKey(aWidth, aHeight);
+    this._iconMapObj[key] = aURISpec;
+  },
+
+  /**
+   * Sets the .iconURI property of the engine. If both aWidth and aHeight are
+   * provided an entry will be added to _iconMapObj that will enable accessing
+   * icon's data through getIcons() and getIconURIBySize() APIs.
    *
    *  @param aIconURL
    *         A URI string pointing to the engine's icon. Must have a http[s],
    *         ftp, or data scheme. Icons with HTTP[S] or FTP schemes will be
    *         downloaded and converted to data URIs for storage in the engine
    *         XML files, if the engine is not readonly.
    *  @param aIsPreferred
    *         Whether or not this icon is to be preferred. Preferred icons can
    *         override non-preferred icons.
+   *  @param aWidth (optional)
+   *         Width of the icon.
+   *  @param aHeight (optional)
+   *         Height of the icon.
    */
-  _setIcon: function SRCH_ENG_setIcon(aIconURL, aIsPreferred) {
-    // If we already have a preferred icon, and this isn't a preferred icon,
-    // just ignore it.
-    if (this._hasPreferredIcon && !aIsPreferred)
-      return;
-
+  _setIcon: function SRCH_ENG_setIcon(aIconURL, aIsPreferred, aWidth, aHeight) {
     var uri = makeURI(aIconURL);
 
     // Ignore bad URIs
     if (!uri)
       return;
 
     LOG("_setIcon: Setting icon url \"" + limitURILength(uri.spec) + "\" for engine \""
         + this.name + "\".");
     // Only accept remote icons from http[s] or ftp
     switch (uri.scheme) {
       case "data":
-        this._iconURI = uri;
-        notifyAction(this, SEARCH_ENGINE_CHANGED);
-        this._hasPreferredIcon = aIsPreferred;
+        if (!this._hasPreferredIcon || aIsPreferred) {
+          this._iconURI = uri;
+          notifyAction(this, SEARCH_ENGINE_CHANGED);
+          this._hasPreferredIcon = aIsPreferred;
+        }
+
+        if (aWidth && aHeight) {
+          this._addIconToMap(aWidth, aHeight, aIconURL)
+        }
         break;
       case "http":
       case "https":
       case "ftp":
         // No use downloading the icon if the engine file is read-only
         if (!this._readOnly ||
             getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true)) {
           LOG("_setIcon: Downloading icon: \"" + uri.spec +
@@ -1605,17 +1648,22 @@ Engine.prototype = {
               return;
 
             if (!aByteArray || aByteArray.length > MAX_ICON_SIZE) {
               LOG("iconLoadCallback: load failed, or the icon was too large!");
               return;
             }
 
             var str = btoa(String.fromCharCode.apply(null, aByteArray));
-            aEngine._iconURI = makeURI(ICON_DATAURL_PREFIX + str);
+            let dataURL = ICON_DATAURL_PREFIX + str;
+            aEngine._iconURI = makeURI(dataURL);
+
+            if (aWidth && aHeight) {
+              aEngine._addIconToMap(aWidth, aHeight, dataURL)
+            }
 
             // The engine might not have a file yet, if it's being downloaded,
             // because the request for the engine file itself (_onLoad) may not
             // yet be complete. In that case, this change will be written to
             // file when _onLoad is called. For readonly engines, we'll store
             // the changes in the cache once notified below.
             if (aEngine._file && !aEngine._readOnly)
               aEngine._serializeToFile();
@@ -1799,24 +1847,27 @@ Engine.prototype = {
   },
 
   /**
    * Get the icon from an OpenSearch Image element.
    * @see http://opensearch.a9.com/spec/1.1/description/#image
    */
   _parseImage: function SRCH_ENG_parseImage(aElement) {
     LOG("_parseImage: Image textContent: \"" + limitURILength(aElement.textContent) + "\"");
-    if (aElement.getAttribute("width")  == "16" &&
-        aElement.getAttribute("height") == "16") {
-      this._setIcon(aElement.textContent, true);
+
+    let width = parseInt(aElement.getAttribute("width"), 10);
+    let height = parseInt(aElement.getAttribute("height"), 10);
+    let isPrefered = width == 16 && height == 16;
+
+    if (isNaN(width) || isNaN(height) || width <= 0 || height <=0) {
+      LOG("OpenSearch image element must have positive width and height.");
+      return;
     }
-    else {
-      LOG("OpenSearch image must have explicit width=16 height=16: " +
-          aElement.textContent);
-    }
+
+    this._setIcon(aElement.textContent, isPrefered, width, height);
   },
 
   _parseAsMozSearch: function SRCH_ENG_parseAsMoz() {
     //forward to the OpenSearch parser
     this._parseAsOpenSearch();
   },
 
   /**
@@ -2204,16 +2255,17 @@ Engine.prototype = {
     this._updateInterval = aJson._updateInterval || null;
     this._updateURL = aJson._updateURL || null;
     this._iconUpdateURL = aJson._iconUpdateURL || null;
     if (aJson._readOnly == undefined)
       this._readOnly = true;
     else
       this._readOnly = false;
     this._iconURI = makeURI(aJson._iconURL);
+    this._iconMapObj = aJson._iconMapObj;
     for (let i = 0; i < aJson._urls.length; ++i) {
       let url = aJson._urls[i];
       let engineURL = new EngineURL(url.type || URLTYPE_SEARCH_HTML,
                                     url.method || "GET", url.template);
       engineURL._initWithJSON(url, this);
       this._urls.push(engineURL);
     }
   },
@@ -2228,17 +2280,18 @@ Engine.prototype = {
   _serializeToJSON: function SRCH_ENG__serializeToJSON(aFilter) {
     var json = {
       _id: this._id,
       _name: this._name,
       _hidden: this.hidden,
       description: this.description,
       __searchForm: this.__searchForm,
       _iconURL: this._iconURL,
-      _urls: [url._serializeToJSON() for each(url in this._urls)] 
+      _iconMapObj: this._iconMapObj,
+      _urls: [url._serializeToJSON() for each(url in this._urls)]
     };
 
     if (this._file instanceof Ci.nsILocalFile)
       json.filePath = this._file.persistentDescriptor;
     if (this._uri)
       json._url = this._uri.spec;
     if (this._installLocation != SEARCH_APP_DIR || !aFilter)
       json._installLocation = this._installLocation;
@@ -2674,18 +2727,61 @@ Engine.prototype = {
     if (aIID.equals(Ci.nsISearchEngine) ||
         aIID.equals(Ci.nsISupports))
       return this;
     throw Cr.NS_ERROR_NO_INTERFACE;
   },
 
   get wrappedJSObject() {
     return this;
+  },
+
+  /**
+   * Returns a string with the URL to an engine's icon matching both width and
+   * height. Returns null if icon with specified dimensions is not found.
+   *
+   * @param width
+   *        Width of the requested icon.
+   * @param height
+   *        Height of the requested icon.
+   */
+  getIconURLBySize: function SRCH_ENG_getIconURLBySize(aWidth, aHeight) {
+    if (!this._iconMapObj)
+      return null;
+
+    let key = this._getIconKey(aWidth, aHeight);
+    if (key in this._iconMapObj) {
+      return this._iconMapObj[key];
+    }
+    return null;
+  },
+
+  /**
+   * Gets an array of all available icons. Each entry is an object with
+   * width, height and url properties. width and height are numeric and
+   * represent the icon's dimensions. url is a string with the URL for
+   * the icon.
+   */
+  getIcons: function SRCH_ENG_getIcons() {
+    let result = [];
+
+    if (!this._iconMapObj)
+      return result;
+
+    for (let key of Object.keys(this._iconMapObj)) {
+      let iconSize = JSON.parse(key);
+      result.push({
+        width: iconSize.width,
+        height: iconSize.height,
+        url: this._iconMapObj[key]
+      });
+    }
+
+    return result;
   }
-
 };
 
 // nsISearchSubmission
 function Submission(aURI, aPostData = null) {
   this._uri = aURI;
   this._postData = aPostData;
 }
 Submission.prototype = {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engineImages.xml
@@ -0,0 +1,22 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+    <ShortName>IconsTest</ShortName>
+    <Description>IconsTest. Search by Test.</Description>
+    <InputEncoding>UTF-8</InputEncoding>
+    <Image width="16" height="16">data:image/x-icon;base64,ico16</Image>
+    <Image width="32" height="32">data:image/x-icon;base64,ico32</Image>
+    <Image width="74" height="74">data:image/png;base64,ico74</Image>
+    <Url type="application/x-suggestions+json" template="http://api.bing.com/osjson.aspx">
+        <Param name="query" value="{searchTerms}"/>
+        <Param name="form" value="MOZW"/>
+    </Url>
+    <Url type="text/html" method="GET" template="http://www.bing.com/search">
+        <Param name="q" value="{searchTerms}"/>
+        <MozParam name="pc" condition="pref" pref="ms-pc"/>
+        <Param name="form" value="MOZW"/>
+    </Url>
+    <SearchForm>http://www.bing.com/search</SearchForm>
+</SearchPlugin>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_multipleIcons.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests getIcons() and getIconURLBySize() on engine with multiple icons.
+ */
+
+"use strict";
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://testing-common/httpd.js");
+
+  function test_multiIcon() {
+  let engine = Services.search.getEngineByName("IconsTest");
+  do_check_neq(engine, null);
+
+  do_print("Running tests on IconsTest engine");
+
+  do_print("The default should be the 16x16 icon");
+  do_check_true(engine.iconURI.spec.contains("ico16"));
+
+  do_check_true(engine.getIconURLBySize(32,32).contains("ico32"));
+  do_check_true(engine.getIconURLBySize(74,74).contains("ico74"));
+
+  do_print("Invalid dimensions should return null.");
+  do_check_null(engine.getIconURLBySize(50,50));
+
+  let allIcons = engine.getIcons();
+
+  do_print("Check that allIcons contains expected icon sizes");
+  do_check_eq(allIcons.length, 3);
+  let expectedWidths = [16, 32, 74];
+  do_check_true(allIcons.every((item) => {
+    let width = item.width;
+    do_check_neq(expectedWidths.indexOf(width), -1);
+    do_check_eq(width, item.height);
+
+    let icon = item.url.split(",").pop();
+    do_check_eq(icon, "ico" + width);
+
+    return true;
+  }));
+
+  do_test_finished();
+}
+
+function run_test() {
+  removeMetadata();
+  updateAppInfo();
+
+  let httpServer = new HttpServer();
+  httpServer.start(4444);
+  httpServer.registerDirectory("/", do_get_cwd());
+
+  do_register_cleanup(function cleanup() {
+    httpServer.stop(function() {});
+  });
+
+  do_test_pending();
+
+  let observer = function(aSubject, aTopic, aData) {
+    if (aData == "engine-loaded") {
+      test_multiIcon();
+    }
+  };
+
+  Services.obs.addObserver(observer, "browser-search-engine-modified", false);
+
+  do_print("Adding engine with multiple images");
+  Services.search.addEngine("http://localhost:4444/data/engineImages.xml",
+    Ci.nsISearchEngine.DATA_XML, null, false);
+
+  do_timeout(12000, function() {
+    do_throw("Timeout");
+  });
+}
--- a/toolkit/components/search/tests/xpcshell/xpcshell.ini
+++ b/toolkit/components/search/tests/xpcshell/xpcshell.ini
@@ -2,16 +2,17 @@
 head = head_search.js
 tail =
 firefox-appdir = browser
 support-files =
   data/chrome.manifest
   data/engine.src
   data/engine.xml
   data/engine2.xml
+  data/engineImages.xml
   data/ico-size-16x16-png.ico
   data/search-metadata.json
   data/search.json
   data/search.sqlite
   data/searchTest.jar
 
 [test_nocache.js]
 [test_645970.js]
@@ -22,12 +23,13 @@ support-files =
 [test_nodb.js]
 [test_nodb_pluschanges.js]
 [test_save_sorted_engines.js]
 [test_purpose.js]
 [test_defaultEngine.js]
 [test_prefSync.js]
 [test_notifications.js]
 [test_addEngine_callback.js]
+[test_multipleIcons.js]
 [test_async.js]
 [test_sync.js]
 [test_sync_fallback.js]
 [test_sync_delay_fallback.js]
--- a/toolkit/content/widgets/videocontrols.xml
+++ b/toolkit/content/widgets/videocontrols.xml
@@ -333,17 +333,25 @@
                                "playing", "waiting", "canplay", "canplaythrough",
                                "seeking", "seeked", "emptied", "loadedmetadata",
                                "error", "suspend", "stalled",
                                "mozinterruptbegin", "mozinterruptend" ],
 
                 firstFrameShown : false,
                 timeUpdateCount : 0,
                 maxCurrentTimeSeen : 0,
-                isAudioOnly : false,
+                _isAudioOnly : false,
+                get isAudioOnly() { return this._isAudioOnly; },
+                set isAudioOnly(val) {
+                    this._isAudioOnly = val;
+                    if (!this.isTopLevelSyntheticDocument)
+                        return;
+                    this.video.style.height = this._controlBarHeight + "px";
+                    this.video.style.width = "66%";
+                },
                 suppressError : false,
 
                 setupStatusFader : function(immediate) {
                     // Since the play button will be showing, we don't want to
                     // show the throbber behind it. The throbber here will
                     // only show if needed after the play button has been pressed.
                     if (!this.clickToPlay.hidden) {
                         this.startFadeOut(this.statusOverlay, true);
@@ -837,17 +845,20 @@
                     if (index >= 0) {
                         endTime = Math.round(buffered.end(index) * 1000);
                     }
                     this.bufferBar.max = duration;
                     this.bufferBar.value = endTime;
                 },
 
                 onVolumeMouseInOut : function (event) {
-                    if (this.isVideoWithoutAudioTrack()) {
+                    let doc = this.video.ownerDocument;
+                    let win = doc.defaultView;
+                    if (this.isVideoWithoutAudioTrack() ||
+                        (this.isAudioOnly && this.isTopLevelSyntheticDocument)) {
                         return;
                     }
                     // Ignore events caused by transitions between mute button and volumeStack,
                     // or between nodes inside these two elements.
                     if (this.isEventWithin(event, this.muteButton, this.volumeStack))
                         return;
                     var isMouseOver = (event.type == "mouseover");
                     this.startFade(this.volumeStack, isMouseOver);
@@ -1330,44 +1341,52 @@
                     return isDescendant(event.target) && isDescendant(event.relatedTarget);
                 },
 
                 log : function (msg) {
                     if (this.debug)
                         dump("videoctl: " + msg + "\n");
                 },
 
+                get isTopLevelSyntheticDocument() {
+                  let doc = this.video.ownerDocument;
+                  let win = doc.defaultView;
+                  return doc.mozSyntheticDocument && win === win.top;
+                },
+
                 _playButtonWidth : 0,
                 _durationLabelWidth : 0,
                 _muteButtonWidth : 0,
                 _fullscreenButtonWidth : 0,
                 _controlBarHeight : 0,
                 _overlayPlayButtonHeight : 64,
                 _overlayPlayButtonWidth : 64,
                 adjustControlSize : function adjustControlSize() {
-                    if (this.isAudioOnly)
+                    let doc = this.video.ownerDocument;
+                    let isAudioOnly = this.isAudioOnly;
+                    if (isAudioOnly && !this.isTopLevelSyntheticDocument)
                         return;
 
-                    let videoHeight = this.video.clientHeight;
-                    let videoWidth = this.video.clientWidth;
-
-                    if (this._overlayPlayButtonHeight > videoHeight || this._overlayPlayButtonWidth > videoWidth)
-                        this.clickToPlay.hidden = true;
-
                     // The scrubber has |flex=1|, therefore |minScrubberWidth|
                     // was generated by empirical testing.
                     let minScrubberWidth = 25;
                     let minWidthAllControls = this._playButtonWidth +
                                               minScrubberWidth +
                                               this._durationLabelWidth +
                                               this._muteButtonWidth +
                                               this._fullscreenButtonWidth;
                     let minHeightForControlBar = this._controlBarHeight;
                     let minWidthOnlyPlayPause = this._playButtonWidth + this._muteButtonWidth;
 
+                    let videoHeight = isAudioOnly ? minHeightForControlBar : this.video.clientHeight;
+                    let videoWidth = isAudioOnly ? minWidthAllControls : this.video.clientWidth;
+
+                    if (this._overlayPlayButtonHeight > videoHeight || this._overlayPlayButtonWidth > videoWidth)
+                        this.clickToPlay.hidden = true;
+
                     let size = "normal";
                     if (videoHeight < minHeightForControlBar)
                         size = "hidden";
                     else if (videoWidth < minWidthOnlyPlayPause)
                         size = "hidden";
                     else if (videoWidth < minWidthAllControls)
                         size = "small";
                     this.controlBar.setAttribute("size", size);
--- a/toolkit/devtools/output-parser.js
+++ b/toolkit/devtools/output-parser.js
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
 const {colorUtils} = require("devtools/css-color");
 const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
 
+const MAX_ITERATIONS = 100;
 const REGEX_QUOTES = /^".*?"|^".*/;
 const REGEX_URL = /^url\(["']?(.+?)(?::(\d+))?["']?\)/;
 const REGEX_WHITESPACE = /^\s+/;
 const REGEX_FIRST_WORD_OR_CHAR = /^\w+|^./;
 const REGEX_CSS_PROPERTY_VALUE = /(^[^;]+)/;
 
 /**
  * This regex matches:
@@ -130,16 +131,17 @@ OutputParser.prototype = {
    *         parsed.
    */
   _parse: function(text, options={}) {
     text = text.trim();
     this.parsed.length = 0;
     let dirty = false;
     let matched = null;
     let nameValueSupported = false;
+    let i = 0;
 
     let trimMatchFromStart = function(match) {
       text = text.substr(match.length);
       dirty = true;
       matched = null;
     };
 
     while (text.length > 0) {
@@ -202,16 +204,24 @@ OutputParser.prototype = {
           let match = matched[0];
           trimMatchFromStart(match);
           this._appendTextNode(match);
           nameValueSupported = false;
         }
       }
 
       dirty = false;
+
+      // Prevent this loop from slowing down the browser with too
+      // many nodes being appended into output.
+      i++;
+      if (i > MAX_ITERATIONS) {
+        trimMatchFromStart(text);
+        this._appendTextNode(text);
+      }
     }
 
     return this._toDOM();
   },
 
   /**
    * Check if a CSS property supports a specific value.
    *
@@ -307,40 +317,40 @@ OutputParser.prototype = {
    * Append a text node to the output. If the previously output item was a text
    * node then we append the text to that node.
    *
    * @param  {String} text
    *         Text to append
    */
   _appendTextNode: function(text) {
     let lastItem = this.parsed[this.parsed.length - 1];
-
-    if (typeof lastItem !== "undefined" && lastItem.nodeName === "#text") {
-      lastItem.nodeValue += text;
+    if (typeof lastItem === "string") {
+      this.parsed[this.parsed.length - 1] = lastItem + text
     } else {
-      let win = Services.appShell.hiddenDOMWindow;
-      let doc = win.document;
-      let textNode = doc.createTextNode(text);
-      this.parsed.push(textNode);
+      this.parsed.push(text);
     }
   },
 
   /**
    * Take all output and append it into a single DocumentFragment.
    *
    * @return {DocumentFragment}
    *         Document Fragment
    */
   _toDOM: function() {
     let win = Services.appShell.hiddenDOMWindow;
     let doc = win.document;
     let frag = doc.createDocumentFragment();
 
     for (let item of this.parsed) {
-      frag.appendChild(item);
+      if (typeof item === "string") {
+        frag.appendChild(doc.createTextNode(item));
+      } else {
+        frag.appendChild(item);
+      }
     }
 
     this.parsed.length = 0;
     return frag;
   },
 
   /**
    * Check that a string represents a valid volor.
--- a/toolkit/modules/RemoteWebProgress.jsm
+++ b/toolkit/modules/RemoteWebProgress.jsm
@@ -104,16 +104,28 @@ RemoteWebProgressManager.prototype = {
     // We must check the Extended Validation (EV) state here, on the chrome
     // process, because NSS is needed for that determination.
     if (deserialized && deserialized.isExtendedValidation)
       aState |= Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL;
 
     return [deserialized, aState];
   },
 
+  _callProgressListeners: function(methodName, ...args) {
+    for (let p of this._progressListeners) {
+      if (p[methodName]) {
+        try {
+          p[methodName].apply(p, args);
+        } catch (ex) {
+          Cu.reportError("RemoteWebProgress failed to call " + methodName + ": " + ex + "\n");
+        }
+      }
+    }
+  },
+
   receiveMessage: function (aMessage) {
     let json = aMessage.json;
     let objects = aMessage.objects;
 
     // The top-level WebProgress is always the same, but because we don't
     // really have a concept of subframes/content we always creat a new object
     // for those.
     let webProgress = json.isTopLevel ? this._topLevelWebProgress
@@ -128,54 +140,46 @@ RemoteWebProgressManager.prototype = {
     webProgress._DOMWindow = objects.DOMWindow;
     webProgress._loadType = json.loadType;
 
     if (json.isTopLevel)
       this._browser._contentWindow = objects.contentWindow;
 
     switch (aMessage.name) {
     case "Content:StateChange":
-      for (let p of this._progressListeners) {
-        p.onStateChange(webProgress, request, json.stateFlags, json.status);
-      }
+      this._callProgressListeners("onStateChange", webProgress, request, json.stateFlags, json.status);
       break;
 
     case "Content:LocationChange":
       let location = newURI(json.location);
 
       if (json.isTopLevel) {
         this._browser.webNavigation._currentURI = location;
         this._browser.webNavigation.canGoBack = json.canGoBack;
         this._browser.webNavigation.canGoForward = json.canGoForward;
         this._browser._characterSet = json.charset;
         this._browser._documentURI = newURI(json.documentURI);
         this._browser._imageDocument = null;
       }
 
-      for (let p of this._progressListeners) {
-        p.onLocationChange(webProgress, request, location);
-      }
+      this._callProgressListeners("onLocationChange", webProgress, request, location);
       break;
 
     case "Content:SecurityChange":
       let [status, state] = this._fixSSLStatusAndState(json.status, json.state);
 
       if (json.isTopLevel) {
         // Invoking this getter triggers the generation of the underlying object,
         // which we need to access with ._securityUI, because .securityUI returns
         // a wrapper that makes _update inaccessible.
         void this._browser.securityUI;
         this._browser._securityUI._update(status, state);
       }
 
-      for (let p of this._progressListeners) {
-        p.onSecurityChange(webProgress, request, state);
-      }
+      this._callProgressListeners("onSecurityChange", webProgress, request, state);
       break;
 
     case "Content:StatusChange":
-      for (let p of this._progressListeners) {
-        p.onStatusChange(webProgress, request, json.status, json.message);
-      }
+      this._callProgressListeners("onStatusChange", webProgress, request, json.status, json.message);
       break;
     }
   }
 };