Bug 570588 - Move current versions of remote browser bindings and remote content script to mobile-browser [r=mbrubeck]
authorMark Finkle <mfinkle@mozilla.com>
Mon, 07 Jun 2010 22:44:06 -0400
changeset 66264 84726991b491719396f9d0f2f74104025e344cd5
parent 66263 2c96550f395e948bb6a0ffb1f881c2afa277d639
child 66265 5f1c8061e585f9228234e8a15164181ec95b5df6
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmbrubeck
bugs570588
Bug 570588 - Move current versions of remote browser bindings and remote content script to mobile-browser [r=mbrubeck]
mobile/chrome/content/bindings/browser.js
mobile/chrome/content/bindings/browser.xml
mobile/chrome/content/browser.css
mobile/chrome/content/content.js
mobile/chrome/jar.mn
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/content/bindings/browser.js
@@ -0,0 +1,243 @@
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+
+dump("!! remote browser loaded\n")
+
+let WebProgressListener = {
+  onStateChange: function onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+    let webProgress = Ci.nsIWebProgressListener;
+    let notifyFlags = 0;
+    if (aStateFlags & webProgress.STATE_IS_REQUEST)
+      notifyFlags |= Ci.nsIWebProgress.NOTIFY_STATE_REQUEST;
+    if (aStateFlags & webProgress.STATE_IS_DOCUMENT)
+      notifyFlags |= Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT;
+    if (aStateFlags & webProgress.STATE_IS_NETWORK)
+      notifyFlags |= Ci.nsIWebProgress.NOTIFY_STATE_NETWORK;
+    if (aStateFlags & webProgress.STATE_IS_WINDOW)
+      notifyFlags |= Ci.nsIWebProgress.NOTIFY_STATE_WINDOW;
+
+    let json = {
+      isRootWindow: aWebProgress.DOMWindow == content,
+      stateFlags: aStateFlags,
+      status: aStatus,
+      notifyFlags: notifyFlags
+    };
+    sendAsyncMessage("WebProgress:StateChange", json);
+  },
+
+  onProgressChange: function onProgressChange(aWebProgress, aRequest, aCurSelf, aMaxSelf, aCurTotal, aMaxTotal) {
+    let json = {
+      isRootWindow: aWebProgress.DOMWindow == content,
+      curSelf: aCurSelf,
+      maxSelf: aMaxSelf,
+      curTotal: aCurTotal,
+      maxTotal: aMaxTotal
+    };
+    sendAsyncMessage("WebProgress:ProgressChange", json);
+  },
+
+  onLocationChange: function onLocationChange(aWebProgress, aRequest, aLocationURI) {
+    let location = aLocationURI ? aLocationURI.spec : "";
+    let json = {
+      isRootWindow: aWebProgress.DOMWindow == content,
+      location: location,
+      canGoBack: docShell.canGoBack,
+      canGoForward: docShell.canGoForward
+    };
+    sendAsyncMessage("WebProgress:LocationChange", json);
+  },
+
+  onStatusChange: function onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
+    let json = {
+      isRootWindow: aWebProgress.DOMWindow == content,
+      status: aStatus,
+      message: aMessage
+    };
+    sendAsyncMessage("WebProgress:StatusChange", json);
+  },
+
+  onSecurityChange: function onSecurityChange(aWebProgress, aRequest, aState) {
+    let json = {
+      isRootWindow: aWebProgress.DOMWindow == content,
+      identity: this._getIdentityData(),
+      state: aState
+    };
+    sendAsyncMessage("WebProgress:SecurityChange", json);
+  },
+
+  QueryInterface: function QueryInterface(aIID) {
+    if (aIID.equals(Ci.nsIWebProgressListener) ||
+        aIID.equals(Ci.nsISupportsWeakReference) ||
+        aIID.equals(Ci.nsISupports)) {
+        return this;
+    }
+
+    throw Components.results.NS_ERROR_NO_INTERFACE;
+  },
+
+  /**
+   * Helper to parse out the important parts of the SSL cert for use in constructing
+   * identity UI strings
+   */
+  _getIdentityData: function() {
+    let result = {};
+    let status = docShell.securityUI.QueryInterface(Ci.nsISSLStatusProvider).SSLStatus;
+
+    if (status) {
+      status = status.QueryInterface(Ci.nsISSLStatus);
+      let cert = status.serverCert;
+
+      // Human readable name of Subject
+      result.subjectOrg = cert.organization;
+
+      // SubjectName fields, broken up for individual access
+      if (cert.subjectName) {
+        result.subjectNameFields = {};
+        cert.subjectName.split(",").forEach(function(v) {
+          var field = v.split("=");
+          if (field[1])
+            this[field[0]] = field[1];
+        }, result.subjectNameFields);
+
+        // Call out city, state, and country specifically
+        result.city = result.subjectNameFields.L;
+        result.state = result.subjectNameFields.ST;
+        result.country = result.subjectNameFields.C;
+      }
+
+      // Human readable name of Certificate Authority
+      result.caOrg =  cert.issuerOrganization || cert.issuerCommonName;
+
+      if (!this._overrideService)
+        this._overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(Ci.nsICertOverrideService);
+
+      // Check whether this site is a security exception. XPConnect does the right
+      // thing here in terms of converting _lastLocation.port from string to int, but
+      // the overrideService doesn't like undefined ports, so make sure we have
+      // something in the default case (bug 432241).
+      result.isException = !!this._overrideService.hasMatchingOverride(content.location.hostname, (content.location.port || 443), cert, {}, {});
+    }
+
+    return result;
+  }
+};
+
+let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
+webProgress.addProgressListener(WebProgressListener, Ci.nsIWebProgress.NOTIFY_ALL);
+
+let WebNavigation =  {
+  _webNavigation: docShell.QueryInterface(Ci.nsIWebNavigation),
+
+  init: function() {
+    addMessageListener("WebNavigation:GoBack", this);
+    addMessageListener("WebNavigation:GoForward", this);
+    addMessageListener("WebNavigation:GotoIndex", this);
+    addMessageListener("WebNavigation:LoadURI", this);
+    addMessageListener("WebNavigation:Reload", this);
+    addMessageListener("WebNavigation:Stop", this);
+  },
+
+  receiveMessage: function(message) {
+    switch (message.name) {
+      case "WebNavigation:GoBack":
+        this.goBack(message);
+        break;
+      case "WebNavigation:GoForward":
+        this.goForward(message);
+        break;
+      case "WebNavigation:GotoIndex":
+        this.gotoIndex(message);
+        break;
+      case "WebNavigation:LoadURI":
+        this.loadURI(message);
+        break;
+      case "WebNavigation:Reload":
+        this.reload(message);
+        break;
+      case "WebNavigation:Stop":
+        this.stop(message);
+        break;
+    }
+  },
+
+  goBack: function() {
+    this._webNavigation.goBack();
+  },
+
+  goForward: function() {
+    this._webNavigation.goForward();
+  },
+
+  gotoIndex: function(message) {
+    this._webNavigation.gotoIndex(message.index);
+  },
+
+  loadURI: function(message) {
+    let flags = message.json.flags || this._webNavigation.LOAD_FLAGS_NONE;
+    this._webNavigation.loadURI(message.json.uri, flags, null, null, null);
+  },
+
+  reload: function(message) {
+    let flags = message.json.flags || this._webNavigation.LOAD_FLAGS_NONE;
+    this._webNavigation.reload(flags);
+  },
+
+  stop: function(message) {
+    let flags = message.json.flags || this._webNavigation.STOP_ALL;
+    this._webNavigation.stop(flags);
+  }
+};
+
+WebNavigation.init();
+
+
+let DOMEvents =  {
+  init: function() {
+    addEventListener("DOMContentLoaded", this, false);
+    addEventListener("DOMTitleChanged", this, false);
+    addEventListener("DOMLinkAdded", this, false);
+    addEventListener("DOMWillOpenModalDialog", this, false);
+    addEventListener("DOMWindowClose", this, false);
+  },
+
+  handleEvent: function(aEvent) {
+    switch (aEvent.type) {
+      case "DOMContentLoaded":
+        let documentURIObject = content.document.documentURIObject;
+        if (documentURIObject.spec == "about:blank")
+          return;
+
+        sendAsyncMessage("DOMContentLoaded", { location: documentURIObject.spec });
+        break;
+      case "DOMTitleChanged":
+        sendAsyncMessage("DOMTitleChanged", { title: content.document.title });
+        break;
+      case "DOMWillOpenModalDialog":
+        sendAsyncMessage("DOMWillOpenModalDialog", { });
+        break;
+      case "DOMWindowClose":
+        sendAsyncMessage("DOMWindowClose", { });
+        break;
+      case "DOMLinkAdded":
+        let link = aEvent.originalTarget;
+        let ownerDoc = link.ownerDocument;
+        if (!link.href || !ownerDoc)
+          return;
+
+        let json = {
+          title: link.title,
+          href: link.href,
+          rel: link.rel,
+          type: link.type,
+          isRootDocument: ownerDoc.defaultView.frameElement ? false : true,
+          characterSet: content.document.characterSet,
+          disabled: link.disabled
+        };
+
+        sendAsyncMessage("DOMLinkAdded", json);
+        break;
+    }
+  }
+};
+
+DOMEvents.init();
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/content/bindings/browser.xml
@@ -0,0 +1,700 @@
+<?xml version="1.0"?>
+
+<!-- ***** BEGIN LICENSE BLOCK *****
+   - Version: MPL 1.1/GPL 2.0/LGPL 2.1
+   -
+   - The contents of this file are subject to the Mozilla Public License Version
+   - 1.1 (the "License"); you may not use this file except in compliance with
+   - the License. You may obtain a copy of the License at
+   - http://www.mozilla.org/MPL/
+   -
+   - Software distributed under the License is distributed on an "AS IS" basis,
+   - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+   - for the specific language governing rights and limitations under the
+   - License.
+   -
+   - The Original Code is this file as it was released on March 28, 2001.
+   -
+   - The Initial Developer of the Original Code is
+   - Peter Annema.
+   - Portions created by the Initial Developer are Copyright (C) 2001
+   - the Initial Developer. All Rights Reserved.
+   -
+   - Contributor(s):
+   -   Peter Annema <disttsc@bart.nl> (Original Author of <browser>)
+   -   Peter Parente <parente@cs.unc.edu>
+   -   Christopher Thomas <cst@yecc.com>
+   -   Michael Ventnor <m.ventnor@gmail.com>
+   -   Arpad Borsos <arpad.borsos@googlemail.com>
+   -
+   - Alternatively, the contents of this file may be used under the terms of
+   - either the GNU General Public License Version 2 or later (the "GPL"), or
+   - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+   - in which case the provisions of the GPL or the LGPL are applicable instead
+   - of those above. If you wish to allow use of your version of this file only
+   - under the terms of either the GPL or the LGPL, and not to allow others to
+   - use your version of this file under the terms of the MPL, indicate your
+   - decision by deleting the provisions above and replace them with the notice
+   - and other provisions required by the GPL or the LGPL. If you do not delete
+   - the provisions above, a recipient may use your version of this file under
+   - the terms of any one of the MPL, the GPL or the LGPL.
+   -
+   - ***** END LICENSE BLOCK ***** -->
+
+<!DOCTYPE bindings [
+  <!ENTITY % findBarDTD SYSTEM "chrome://global/locale/findbar.dtd" >
+  %findBarDTD;
+]>
+
+<bindings id="remoteBrowserBindings"
+          xmlns="http://www.mozilla.org/xbl"
+          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <binding id="remote-browser" extends="chrome://global/content/bindings/browser.xml#browser">
+    <implementation type="application/javascript" implements="nsIAccessibleProvider, nsIObserver, nsIDOMEventListener, nsIFrameMessageListener">
+      <property name="accessibleType" readonly="true">
+        <getter>
+          <![CDATA[
+            throw "Supports Remote?";
+          ]]>
+        </getter>
+      </property>
+
+      <property name="autoscrollEnabled">
+        <getter>
+          <![CDATA[
+            throw "Supports Remote?";
+          ]]>
+        </getter>
+      </property>
+
+      <field name="_searchEngines">[]</field>
+      <property name="searchEngines"
+                onget="return this._searchEngines"
+                readonly="true"/>
+
+      <property name="docShell"
+                onget="throw 'Not Remotable'"
+                readonly="true"/>
+
+      <property name="messageManager"
+                onget="return this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader.messageManager;"
+                readonly="true"/>
+
+      <field name="_contentTitle">null</field>
+      <field name="_contentDocument"><![CDATA[
+        ({
+          get title() { return this._browser._contentTitle; },
+          get location() { return null; },
+          set location(aURI) { this._browser.loadURI(aURI, null, null); },
+          get nodePrincipal() { throw "Not Remotable"; },
+
+          _browser: this,
+        })
+      ]]></field>
+
+      <field name="_ios">
+         Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
+      </field>
+
+      <field name="_webNavigation"><![CDATA[
+        ({
+          canGoBack: false,
+          canGoForward: false,
+          goBack: function() { this._sendSyncMessage("WebNavigation:GoBack", {}); },
+          goForward: function() { this._sendSyncMessage("WebNavigation:GoForward", {}); },
+          gotoIndex: function(aIndex) { this._sendSyncMessage("WebNavigation:GotoIndex", {index: aIndex}); },
+          loadURI: function(aURI, aLoadFlags, aReferrer, aPostData, aHeaders) {
+            this._currentURI = this._browser._ios.newURI(aURI, null, null);
+            this._browser._contentTitle = "";
+            this._sendSyncMessage("WebNavigation:LoadURI", {uri: aURI, flags: aLoadFlags});
+          },
+          reload: function(aReloadFlags) { this._sendSyncMessage("WebNavigation:Reload", {flags: aReloadFlags}); },
+          stop: function(aStopFlags) { this._sendSyncMessage("WebNavigation:Stop", {flags: aStopFlags}); },
+          get document() { return this._browser._contentDocument; },
+          get currentURI() {
+            if (!this._currentURI)
+               this._currentURI = this._browser._ios.newURI("about:blank", null, null);
+
+            return this._currentURI;
+          },
+          set currentURI(aURI) { this.loadURI(aURI.spec, null, null, null); },
+          referringURI: null,
+          get sessionHistory() { throw "Not Remoteable"; },
+
+          _currentURI: null,
+          _browser: this,
+          _sendSyncMessage: function(aMessage, aData) {
+            try {
+              this._browser.messageManager.sendAsyncMessage(aMessage, aData);
+            }
+            catch (e) {
+              Components.utils.reportError(e);
+            }
+         }
+        })
+      ]]></field>
+
+      <property name="webNavigation"
+                onget="return this._webNavigation;"
+                readonly="true"/>
+
+      <method name="receiveMessage">
+        <parameter name="aMessage"/>
+        <body><![CDATA[
+          switch (aMessage.name) {
+            case "DOMContentLoaded": {
+              let data = aMessage.json;
+              if (this.mIconURL == "") {
+                let documentURI = gIOService.newURI(data.location, null, null);
+                this.mIconURL = documentURI.prePath + "/favicon.ico";
+              }
+              break;
+            }
+            case "DOMTitleChanged": {
+              this._contentTitle = aMessage.json.title;
+
+              let event = document.createEvent("Events");
+              event.initEvent("DOMTitleChanged", true, true);
+              this.dispatchEvent(event);
+              break;
+            }
+            case "DOMWillOpenModalDialog": {
+              let event = document.createEvent("Events");
+              event.initEvent("DOMWillOpenModalDialog", true, true);
+              this.dispatchEvent(event);
+              break;
+            }
+            case "DOMWindowClose": {
+              let event = document.createEvent("Events");
+              event.initEvent("DOMWindowClose", true, true);
+              this.dispatchEvent(event);
+              break;
+            }
+            case "DOMLinkAdded": {
+              let link = aMessage.json;
+              if (!link.isRootDocument)
+                return;
+
+              let linkType = this._getLinkType(link);
+              switch(linkType) {
+                case "icon":
+                  let iconURI = gIOService.newURI(link.href, link.characterSet, null);
+                  if (!iconURI.schemeIs("javascript") && !gFaviconService.isFailedFavicon(iconURI)) {
+                    gFaviconService.setAndLoadFaviconForPage(this.currentURI, iconURI, true);
+                    this.mIconURL = iconURI.spec;
+                  }
+                  break;
+                case "search":
+                  this._searchEngines.push({ title: link.title, href: link.href });
+                  break;
+              }
+              break;
+            }
+         }
+        ]]></body>
+      </method>
+
+      <method name="_getLinkType">
+        <parameter name="aLink" />
+        <body><![CDATA[
+          let type = "";
+          if (/\bicon\b/i(aLink.rel)) {
+            type = "icon";
+          }
+          else if (/\bsearch\b/i(aLink.rel) && aLink.type && aLink.title) {
+            let linkType = aLink.type.replace(/^\s+|\s*(?:;.*)?$/g, "").toLowerCase();
+            if (linkType == "application/opensearchdescription+xml" && /^(?:https?|ftp):/i.test(aLink.href)) {
+              type = "search";
+            }
+          }
+
+          return type;
+        ]]></body>
+      </method>
+
+      <field name="_webBrowserFind">null</field>
+
+      <property name="webBrowserFind"
+                readonly="true">
+        <getter>
+        <![CDATA[
+          return this._webBrowserFind;
+        ]]>
+        </getter>
+      </property>
+
+      <field name="_fastFind">null</field>
+      <property name="fastFind"
+                readonly="true">
+        <getter>
+        <![CDATA[
+          return this._fastFind;
+        ]]>
+        </getter>
+      </property>
+
+      <field name="_webProgress"><![CDATA[
+        ({
+          _listeners: [],
+          _browser: this,
+
+          _init: function() {
+            this._browser.messageManager.addMessageListener("WebProgress:StateChange", this);
+            this._browser.messageManager.addMessageListener("WebProgress:ProgressChange", this);
+            this._browser.messageManager.addMessageListener("WebProgress:LocationChange", this);
+            this._browser.messageManager.addMessageListener("WebProgress:StatusChange", this);
+            this._browser.messageManager.addMessageListener("WebProgress:SecurityChange", this);
+          },
+
+          addProgressListener: function(aListener, aNotifyFlags) {
+            function hasFilter(item) {
+              return item.listener == aListener;
+            }
+        
+            if (this._listeners.some(hasFilter))
+              return;
+        
+            this._listeners.push({
+              listener: aListener,
+              flags: aNotifyFlags
+            });
+          },
+
+          removeProgressListener: function(aListener) {
+            function hasFilter(item) {
+              return item.listener != aListener;
+            }
+        
+            this._listeners = this._listeners.filter(hasFilter);
+          },
+          get DOMWindow() { throw "Not Remoteable" },
+          isLoadingDocument: false,
+          
+          receiveMessage: function(aMessage) {
+            let args;
+            switch (aMessage.name) {
+              case "WebProgress:StateChange":
+                args = [
+                  { isRootWindow: aMessage.json.isRootWindow, browser: this.browser },
+                  {},
+                  aMessage.json.stateFlags,
+                  aMessage.json.status
+                ];
+                this._notify(aMessage.json.notifyFlags, "onStateChange", args);
+                break;
+ 
+              case "WebProgress:ProgressChange":
+                args = [
+                  { isRootWindow: aMessage.json.isRootWindow, browser: this.browser },
+                  {},
+                  aMessage.json.curSelf,
+                  aMessage.json.maxSelf,
+                  aMessage.json.curTotal,
+                  aMessage.json.maxTotal
+                ];
+                this._notify(Components.interfaces.nsIWebProgress.NOTIFY_PROGRESS,
+                             "onProgressChange",
+                             args);
+                break;
+ 
+              case "WebProgress:LocationChange":
+                let currentURI = this._browser._ios.newURI(aMessage.json.location, null, null);
+                this._browser._webNavigation._currentURI = currentURI;
+                this._browser._webNavigation.canGoBack = aMessage.json.canGoBack;
+                this._browser._webNavigation.canGoForward = aMessage.json.canGoForward;
+
+                args = [
+                  { isRootWindow: aMessage.json.isRootWindow, browser: this.browser },
+                  {},
+                  currentURI
+                ];
+                this._notify(Components.interfaces.nsIWebProgress.NOTIFY_LOCATION,
+                             "onLocationChange",
+                             args);
+
+                this._browser._searchEngines = [];
+                break;
+ 
+              case "WebProgress:StatusChange":
+                args = [
+                  { isRootWindow: aMessage.json.isRootWindow, browser: this.browser },
+                  {},
+                  aMessage.json.status,
+                  aMessage.json.message
+                ];
+                this._notify(Components.interfaces.nsIWebProgress.NOTIFY_STATUS,
+                             "onStatusChange",
+                             args);
+                break;
+ 
+              case "WebProgress:SecurityChange":
+                args = [
+                  { isRootWindow: aMessage.json.isRootWindow, browser: this.browser, identity: aMessage.json.identity },
+                  {},
+                  aMessage.json.state
+                ];
+                this._notify(Components.interfaces.nsIWebProgress.NOTIFY_SECURITY,
+                             "onSecurityChange",
+                             args);
+                break;
+            }
+
+            return {};
+          },
+          
+          _notify: function(aFlags, aName, aArguments) {
+            this._listeners.forEach(function(item) {
+              if (item.flags & aFlags)
+                item.listener[aName].apply(item.listener, aArguments);
+            });
+          }
+        })
+      ]]></field>
+
+      <property name="webProgress"
+                readonly="true"
+                onget="return this._webProgress"/>
+
+      <property name="contentWindow"
+                readonly="true"
+                onget="throw 'Not Remoteable'"/>
+
+      <property name="sessionHistory"
+                onget="throw 'Not Remoteable'"
+                readonly="true"/>
+
+      <property name="markupDocumentViewer"
+                onget="throw 'Not Remoteable'"
+                readonly="true"/>
+
+      <property name="contentViewerEdit"
+                onget="throw 'Not Remoteable'"
+                readonly="true"/>
+
+      <property name="contentViewerFile"
+                onget="throw 'Not Remoteable'"
+                readonly="true"/>
+
+      <property name="documentCharsetInfo"
+                onget="throw 'Not Remoteable'"
+                readonly="true"/>
+
+      <method name="attachFormFill">
+        <body>
+          <![CDATA[
+          if (!this.mFormFillAttached && this.hasAttribute("autocompletepopup")) {
+            // hoop up the form fill autocomplete controller
+            var controller = Components.classes["@mozilla.org/satchel/form-fill-controller;1"].
+                               getService(Components.interfaces.nsIFormFillController);
+
+            var popup = document.getElementById(this.getAttribute("autocompletepopup"));
+            if (popup) {
+              controller.attachToBrowser(this.docShell, popup.QueryInterface(Components.interfaces.nsIAutoCompletePopup));
+              this.mFormFillAttached = true;
+            }
+          }
+          ]]>
+        </body>
+      </method>
+
+      <method name="detachFormFill">
+        <body>
+          <![CDATA[
+          if (this.mFormFillAttached) {
+            // hoop up the form fill autocomplete controller
+            var controller = Components.classes["@mozilla.org/satchel/form-fill-controller;1"].
+                               getService(Components.interfaces.nsIFormFillController);
+            controller.detachFromBrowser(this.docShell);
+            
+            this.mFormFillAttached = false;
+          }
+          ]]>
+        </body>
+      </method>
+      
+      <method name="findChildShell">
+        <parameter name="aDocShell"/>
+        <parameter name="aSoughtURI"/>
+        <body>
+          <![CDATA[
+            if (aDocShell.QueryInterface(Components.interfaces.nsIWebNavigation)
+                         .currentURI.spec == aSoughtURI.spec)
+              return aDocShell;
+            var node = aDocShell.QueryInterface(
+                                   Components.interfaces.nsIDocShellTreeNode);
+            for (var i = 0; i < node.childCount; ++i) {
+              var docShell = node.getChildAt(i);
+              docShell = this.findChildShell(docShell, aSoughtURI);
+              if (docShell)
+                return docShell;
+            }
+            return null;
+          ]]>
+        </body>
+      </method>
+
+      <method name="onPageShow">
+        <parameter name="aEvent"/>
+        <body>
+          <![CDATA[
+            this.attachFormFill();
+            if (this.pageReport) {
+              var i = 0;
+              while (i < this.pageReport.length) {
+                // Filter out irrelevant reports.
+                if (this.pageReport[i].requestingWindow &&
+                    (this.pageReport[i].requestingWindow.document ==
+                     this.pageReport[i].requestingDocument))
+                  i++;
+                else
+                  this.pageReport.splice(i, 1);
+              }
+              if (this.pageReport.length == 0) {
+                this.pageReport = null;
+                this.updatePageReport();
+              }
+            }
+         ]]>
+        </body>
+      </method>
+
+      <method name="onPageHide">
+        <parameter name="aEvent"/>
+        <body>
+          <![CDATA[
+            if (this.pageReport) {
+              this.pageReport = null;
+              this.updatePageReport();
+            }
+            // Delete the feeds cache if we're hiding the topmost page
+            // (as opposed to one of its iframes).
+            if (this.feeds && aEvent.target == this.contentDocument)
+              this.feeds = null;
+            if (!this.docShell || !this.fastFind)
+              return;
+            var tabBrowser = this.getTabBrowser();
+            if (!tabBrowser || tabBrowser.mCurrentBrowser == this)
+              this.fastFind.setDocShell(this.docShell);
+
+            if (this._scrollable)
+              this._autoScrollPopup.hidePopup();
+         ]]>
+        </body>
+      </method>
+
+      <method name="updatePageReport">
+        <body>
+          <![CDATA[
+            var event = document.createEvent("Events");
+            event.initEvent("DOMUpdatePageReport", true, true);
+            this.dispatchEvent(event);
+          ]]>
+        </body>
+      </method>
+
+      <method name="onPopupBlocked">
+        <parameter name="evt"/>
+        <body>
+          <![CDATA[
+            if (!this.pageReport) {
+              this.pageReport = new Array();
+            }
+
+            var obj = { requestingWindow: evt.requestingWindow,
+                        // Record the current document in the requesting window
+                        // before it can change.
+                        requestingDocument: evt.requestingWindow.document,
+                        popupWindowURI: evt.popupWindowURI,
+                        popupWindowFeatures: evt.popupWindowFeatures,
+                        popupWindowName: evt.popupWindowName };
+
+            this.pageReport.push(obj);
+            this.pageReport.reported = false;
+            this.updatePageReport();
+          ]]> 
+        </body>
+      </method>
+
+      <field name="pageReport">null</field>
+
+      <property name="securityUI">
+        <getter>
+          <![CDATA[
+            if (!this.docShell.securityUI) {
+              const SECUREBROWSERUI_CONTRACTID = "@mozilla.org/secure_browser_ui;1";
+              if (!this.hasAttribute("disablesecurity") &&
+                  SECUREBROWSERUI_CONTRACTID in Components.classes) {
+                var securityUI = Components.classes[SECUREBROWSERUI_CONTRACTID]
+                                           .createInstance(Components.interfaces.nsISecureBrowserUI);
+                securityUI.init(this.contentWindow);
+              }
+            }
+            
+            return this.docShell.securityUI;
+          ]]>
+        </getter>
+        <setter>
+          <![CDATA[
+            this.docShell.securityUI = val;
+          ]]>
+        </setter>
+      </property>
+
+      <constructor>
+        <![CDATA[
+          try {
+            if (!this.hasAttribute("disablehistory")) {
+              var os = Components.classes["@mozilla.org/observer-service;1"]
+                                 .getService(Components.interfaces.nsIObserverService);
+              os.addObserver(this, "browser:purge-session-history", false);
+              // wire up session history
+              this.webNavigation.sessionHistory = 
+                      Components.classes["@mozilla.org/browser/shistory;1"]
+                                .createInstance(Components.interfaces.nsISHistory);
+              // enable global history
+              this.docShell.QueryInterface(Components.interfaces.nsIDocShellHistory).useGlobalHistory = true;
+            }
+          }
+          catch (e) {
+            Components.utils.reportError(e);
+          }
+          try {
+            var securityUI = this.securityUI;
+          }
+          catch (e) {
+          }
+
+          // Listen for first load for lazy attachment to form fill controller
+          this.addEventListener("pageshow", this.onPageShow, true);
+          this.addEventListener("pagehide", this.onPageHide, true);
+          this.addEventListener("DOMPopupBlocked", this.onPopupBlocked, true);
+
+          messageManager.loadFrameScript("chrome://browser/content/bindings/browser.js", true);
+          //messageManager.addMessageListener("pageshow", this.onPageShow);
+          //messageManager.addMessageListener("pagehide", this.onPageShow);
+          //messageManager.addMessageListener("DOMPopupBlocked", this.onPopupBlocked);
+          messageManager.addMessageListener("DOMContentLoaded", this);
+          messageManager.addMessageListener("DOMTitleChanged", this);
+          messageManager.addMessageListener("DOMLinkAdded", this);
+
+          this.webProgress._init();
+        ]]>
+      </constructor>
+      
+      <destructor>
+        <![CDATA[
+          this.destroy();
+        ]]>
+      </destructor>
+
+      <!-- This is necessary because the destructor doesn't always get called when
+           we are removed from a tabbrowser. This will be explicitly called by tabbrowser -->
+      <method name="destroy">
+        <body>
+          <![CDATA[
+          if (this.mDestroyed)
+            return;
+          this.mDestroyed = true;
+
+          if (!this.hasAttribute("disablehistory")) {
+            var os = Components.classes["@mozilla.org/observer-service;1"]
+                               .getService(Components.interfaces.nsIObserverService);
+            try {
+              os.removeObserver(this, "browser:purge-session-history");
+            } catch (ex) {
+              // It's not clear why this sometimes throws an exception.
+            }
+          }
+
+          this.detachFormFill();
+          
+          this._fastFind = null;
+          this._webBrowserFind = null;
+          
+          // The feeds cache can keep the document inside this browser alive.
+          this.feeds = null;
+
+          this.lastURI = null;
+
+          this.removeEventListener("pageshow", this.onPageShow, true);
+          this.removeEventListener("pagehide", this.onPageHide, true);
+          this.removeEventListener("DOMPopupBlocked", this.onPopupBlocked, true);
+
+          if (this._autoScrollNeedsCleanup) {
+            // we polluted the global scope, so clean it up
+            this._autoScrollPopup.parentNode.removeChild(this._autoScrollPopup);
+          }
+          ]]>
+        </body>
+      </method>
+      
+      <method name="observe">
+        <parameter name="aSubject"/>
+        <parameter name="aTopic"/>
+        <parameter name="aState"/>
+        <body>
+          <![CDATA[
+            if (aTopic != "browser:purge-session-history" || !this.sessionHistory)
+              return;
+              
+            // place the entry at current index at the end of the history list, so it won't get removed
+            if (this.sessionHistory.index < this.sessionHistory.count - 1) {
+              var indexEntry = this.sessionHistory.getEntryAtIndex(this.sessionHistory.index, false);
+              this.sessionHistory.QueryInterface(Components.interfaces.nsISHistoryInternal);
+              indexEntry.QueryInterface(Components.interfaces.nsISHEntry);
+              this.sessionHistory.addEntry(indexEntry, true);
+            }
+            
+            var purge = this.sessionHistory.count;
+            if (this.currentURI != "about:blank")
+              --purge; // Don't remove the page the user's staring at from shistory
+            
+            if (purge > 0)
+              this.sessionHistory.PurgeHistory(purge);
+          ]]>
+        </body>
+      </method>
+
+      <method name="swapDocShells">
+        <parameter name="aOtherBrowser"/>
+        <body>
+        <![CDATA[
+          // We need to swap fields that are tied to our docshell or related to
+          // the loaded page
+          // Fields which are built as a result of notifactions (pageshow/hide,
+          // DOMLinkAdded/Removed, onStateChange) should not be swapped here,
+          // because these notifications are dispatched again once the docshells
+          // are swapped.
+          var fieldsToSwap = [ "_docShell", "_webBrowserFind", "_contentWindow", "_webNavigation"];
+
+          var ourTabBrowser = this.getTabBrowser();
+
+          // _fastFind is tied to the docshell if we don't have a tabbrowser
+          if ((ourTabBrowser != null) !=
+              (aOtherBrowser.getTabBrowser() != null))
+            throw "Unable to perform swap on <browsers> if one is in a <tabbrowser> and one is not";
+
+          if (!ourTabBrowser)
+            fieldsToSwap.push("_fastFind");
+
+          var ourFieldValues = {};
+          var otherFieldValues = {};
+          for each (var field in fieldsToSwap) {
+            ourFieldValues[field] = this[field];
+            otherFieldValues[field] = aOtherBrowser[field];
+          }
+          this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner)
+              .swapFrameLoaders(aOtherBrowser);
+          for each (var field in fieldsToSwap) {
+            this[field] = otherFieldValues[field];
+            aOtherBrowser[field] = ourFieldValues[field];
+          }
+        ]]>
+        </body>
+      </method>
+    </implementation>
+
+  </binding>
+
+</bindings>
--- a/mobile/chrome/content/browser.css
+++ b/mobile/chrome/content/browser.css
@@ -1,8 +1,12 @@
+/*browser[remote] {*/
+/*  -moz-binding: url("chrome://browser/content/bindings/browser.xml#remote-browser");*/
+/*}*/
+
 #urlbar-edit {
     -moz-binding: url("chrome://browser/content/bindings.xml#autocomplete-aligned");
 }
 
 #tabs {
     -moz-binding: url("chrome://browser/content/tabs.xml#tablist");
 }
 
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/content/content.js
@@ -0,0 +1,757 @@
+// This stays here because otherwise it's hard to tell if there's a parsing error
+dump("###################################### frame-content loaded\n");
+
+// how many milliseconds before the mousedown and the overlay of an element
+const kTapOverlayTimeout = 200;
+
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+let gIOService = Cc["@mozilla.org/network/io-service;1"]
+  .getService(Ci.nsIIOService);
+let gFocusManager = Cc["@mozilla.org/focus-manager;1"]
+  .getService(Ci.nsIFocusManager);
+let gPrefService = Cc["@mozilla.org/preferences-service;1"]
+  .getService(Ci.nsIPrefBranch2);
+
+let XULDocument = Ci.nsIDOMXULDocument;
+let HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement;
+let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement;
+let HTMLFrameElement = Ci.nsIDOMHTMLFrameElement;
+let HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement;
+let HTMLInputElement = Ci.nsIDOMHTMLInputElement;
+let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement;
+let HTMLLabelElement = Ci.nsIDOMHTMLLabelElement;
+let HTMLButtonElement = Ci.nsIDOMHTMLButtonElement;
+let HTMLOptGroupElement = Ci.nsIDOMHTMLOptGroupElement;
+let HTMLOptionElement = Ci.nsIDOMHTMLOptionElement;
+
+/** Send message to UI thread with browser guid as the first parameter. */
+function sendMessage(name) {
+  sendAsyncMessage(name, Array.prototype.slice.call(arguments, 1));
+}
+
+/** Watches for mouse click in content and redirect them to the best found target **/
+const ElementTouchHelper = {
+  get radius() {
+    delete this.radius;
+    return this.radius = { "top": gPrefService.getIntPref("browser.ui.touch.top"),
+                           "right": gPrefService.getIntPref("browser.ui.touch.right"),
+                           "bottom": gPrefService.getIntPref("browser.ui.touch.bottom"),
+                           "left": gPrefService.getIntPref("browser.ui.touch.left")
+                         };
+  },
+
+  get weight() {
+    delete this.weight;
+    return this.weight = { "visited": gPrefService.getIntPref("browser.ui.touch.weight.visited")
+                         };
+  },
+
+  /* Retrieve the closest element to a point by looking at borders position */
+  getClosest: function getClosest(aWindowUtils, aX, aY) {
+    let target = aWindowUtils.elementFromPoint(aX, aY,
+                                               true,   /* ignore root scroll frame*/
+                                               false); /* don't flush layout */
+
+    let nodes = aWindowUtils.nodesFromRect(aX, aY, this.radius.top,
+                                                   this.radius.right,
+                                                   this.radius.bottom,
+                                                   this.radius.left, true, false);
+
+    // return early if the click is just over a clickable element
+    if (this._isElementClickable(target, nodes))
+      return target;
+
+    let threshold = Number.POSITIVE_INFINITY;
+    for (let i = 0; i < nodes.length; i++) {
+      let current = nodes[i];
+      if (!current.mozMatchesSelector || !this._isElementClickable(current))
+        continue;
+
+      let rect = current.getBoundingClientRect();
+      let distance = this._computeDistanceFromRect(aX, aY, rect);
+
+      // increase a little bit the weight for already visited items
+      if (current && current.mozMatchesSelector("*:visited"))
+        distance *= (this.weight.visited / 100);
+
+      if (distance < threshold) {
+        target = current;
+        threshold = distance;
+      }
+    }
+
+    return target;
+  },
+
+  _isElementClickable: function _isElementClickable(aElement, aElementsInRect) {
+    let isClickable = this._hasMouseListener(aElement);
+
+    // If possible looks in the parents node to find a target
+    if (aElement && !isClickable && aElementsInRect) {
+      let parentNode = aElement.parentNode;
+      let count = aElementsInRect.length;
+      for (let i = 0; i < count && parentNode; i++) {
+        if (aElementsInRect[i] != parentNode)
+          continue;
+
+        isClickable = this._hasMouseListener(parentNode);
+        if (isClickable)
+          break;
+
+        parentNode = parentNode.parentNode;
+      }
+    }
+
+    return aElement && (isClickable || aElement.mozMatchesSelector("a,*:link,*:visited,*[role=button],button,input,select,label"));
+  },
+
+  _computeDistanceFromRect: function _computeDistanceFromRect(aX, aY, aRect) {
+    let x = 0, y = 0;
+    let xmost = aRect.left + aRect.width;
+    let ymost = aRect.top + aRect.height;
+
+    // compute horizontal distance from left/right border depending if X is
+    // before/inside/after the element's rectangle
+    if (aRect.left < aX && aX < xmost)
+      x = Math.min(xmost - aX, aX - aRect.left);
+    else if (aX < aRect.left)
+      x = aRect.left - aX;
+    else if (aX > xmost)
+      x = aX - xmost;
+
+    // compute vertical distance from top/bottom border depending if Y is
+    // above/inside/below the element's rectangle
+    if (aRect.top < aY && aY < ymost)
+      y = Math.min(ymost - aY, aY - aRect.top);
+    else if (aY < aRect.top)
+      y = aRect.top - aY;
+    if (aY > ymost)
+      y = aY - ymost;
+
+    return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
+  },
+
+  _els: Cc["@mozilla.org/eventlistenerservice;1"].getService(Ci.nsIEventListenerService),
+  _clickableEvents: ["mousedown", "mouseup", "click"],
+  _hasMouseListener: function _hasMouseListener(aElement) {
+    let els = this._els;
+    let listeners = els.getListenerInfoFor(aElement, {});
+    for (let i = 0; i < listeners.length; i++) {
+      if (this._clickableEvents.indexOf(listeners[i].type) != -1)
+        return true;
+    }
+  }
+};
+
+/**
+ * @param x,y Browser coordinates
+ * @return Element at position, null if no active browser or no element found
+ */
+function elementFromPoint(x, y) {
+  // browser's elementFromPoint expect browser-relative client coordinates.
+  // subtract browser's scroll values to adjust
+  let cwu = Util.getWindowUtils(content);
+  let scroll = Util.getScrollOffset(content);
+  x = x - scroll.x
+  y = y - scroll.y;
+  let elem = ElementTouchHelper.getClosest(cwu, x, y);
+
+  // step through layers of IFRAMEs and FRAMES to find innermost element
+  while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
+    // adjust client coordinates' origin to be top left of iframe viewport
+    let rect = elem.getBoundingClientRect();
+    x = x - rect.left;
+    y = y - rect.top;
+    let windowUtils = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+    elem = ElementTouchHelper.getClosest(windowUtils, x, y);
+  }
+
+  return elem;
+}
+
+
+function getBoundingContentRect(contentElem) {
+  if (!contentElem)
+    return new Rect(0, 0, 0, 0);
+
+  let document = contentElem.ownerDocument;
+  while(document.defaultView.frameElement)
+    document = document.defaultView.frameElement.ownerDocument;
+
+  let offset = Util.getScrollOffset(content);
+  let r = contentElem.getBoundingClientRect();
+
+  // step out of iframes and frames, offsetting scroll values
+  for (let frame = contentElem.ownerDocument.defaultView; frame != content; frame = frame.parent) {
+    // adjust client coordinates' origin to be top left of iframe viewport
+    let rect = frame.frameElement.getBoundingClientRect();
+    let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
+    let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
+    offset.add(rect.left + parseInt(left), rect.top + parseInt(top));
+  }
+
+  return new Rect(r.left + offset.x, r.top + offset.y, r.width, r.height);
+}
+
+
+/** Reponsible for sending messages about viewport size changes and painting. */
+function Coalescer() {
+  this._pendingDirtyRect = new Rect(0, 0, 0, 0);
+  this._pendingSizeChange = null;
+  this._timer = new Util.Timeout(this);
+  // XXX When moving back and forward in docShell history, MozAfterPaint does not get called properly and
+  // some dirty rectangles are never flagged properly.  To fix this, coalescer will fake a paint event that
+  // dirties the entire viewport.
+  this._incremental = false;
+}
+
+Coalescer.prototype = {
+  notify: function notify() {
+    this.flush();
+  },
+
+  handleEvent: function handleEvent(e) {
+    if (e.type == "MozAfterPaint") {
+      var win = e.originalTarget;
+      var scrollOffset = Util.getScrollOffset(win);
+      this.dirty(scrollOffset, e.clientRects);
+    } else if (e.type == "MozScrolledAreaChanged") {
+      // XXX if it's possible to get a scroll area change with the same values,
+      // it would be optimal if this didn't send the same message twice.
+      var doc = e.originalTarget;
+      var win = doc.defaultView;
+      var scrollOffset = Util.getScrollOffset(win);
+      if (win.parent != win)
+	// We are only interested in root scroll pane changes
+	return;
+      this.sizeChange(scrollOffset, e.x, e.y, e.width, e.height);
+    }
+  },
+
+  /** Next scroll size change event will invalidate all previous content. See constructor. */
+  emptyPage: function emptyPage() {
+    this._incremental = false;
+  },
+
+  startCoalescing: function startCoalescing() {
+    this._timer.interval(1000);
+  },
+  
+  stopCoalescing: function stopCoalescing() {
+    this._timer.flush();
+  },
+
+  sizeChange: function sizeChange(scrollOffset, x, y, width, height) {
+    // Adjust width and height from the incoming event properties so that we
+    // ignore changes to width and height contributed by growth in page
+    // quadrants other than x > 0 && y > 0.
+    var x = x + scrollOffset.x;
+    var y = y + scrollOffset.y;
+    this._pendingSizeChange = {
+      width: width + (x < 0 ? x : 0),
+      height: height + (y < 0 ? y : 0)
+    };
+
+    // Clear any pending dirty rectangles since entire viewport will be invalidated
+    // anyways.
+    var rect = this._pendingDirtyRect;
+    rect.top = rect.bottom;
+    rect.left = rect.right;
+
+    if (!this._timer.isPending())
+      this.flush()
+  },
+
+  dirty: function dirty(scrollOffset, clientRects) {
+    if (!this._pendingSizeChange) {
+      var unionRect = this._pendingDirtyRect;
+      for (var i = clientRects.length - 1; i >= 0; i--) {
+        var e = clientRects.item(i);
+        unionRect.expandToContain(new Rect(
+          e.left + scrollOffset.x, e.top + scrollOffset.y, e.width, e.height));
+      }
+
+      if (!this._timer.isPending())
+        this.flush()
+    }
+  },
+
+  flush: function flush() {
+    var dirtyRect = this._pendingDirtyRect;
+    var sizeChange = this._pendingSizeChange;
+    if (sizeChange) {
+      sendMessage("FennecMozScrolledAreaChanged", sizeChange.width, sizeChange.height);
+      if (!this._incremental)
+        sendMessage("FennecMozAfterPaint", [new Rect(0, 0, sizeChange.width, sizeChange.height)]);
+      this._pendingSizeChange = null;
+      // After first size change has been issued, assume subsequent size changes are only incremental
+      // changes to the current page.
+      this._incremental = true;
+    }
+    else if (!dirtyRect.isEmpty()) {
+      // No size change has occurred, but areas have been dirtied.
+      sendMessage("FennecMozAfterPaint", [dirtyRect]);
+      dirtyRect.top = dirtyRect.bottom;
+      dirtyRect.left = dirtyRect.right;
+    }
+  }
+};
+
+
+/**
+ * Responsible for sending messages about security, location, and page load state.
+ * @param loadingController Object with methods startLoading and stopLoading
+ */
+function ProgressController(loadingController) {
+  this._webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
+  this._overrideService = null;
+  this._hostChanged = false;
+  this._state = null;
+  this._loadingController = loadingController || this._defaultLoadingController;
+}
+
+ProgressController.prototype = {
+  // Default loading callbacks do nothing
+  _defaultLoadingController: {
+    startLoading: function() {},
+    stopLoading: function() {}
+  },
+
+  onStateChange: function onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+    // ignore notification that aren't about the main document (iframes, etc)
+    var win = aWebProgress.DOMWindow;
+    if (win != win.parent)
+      return;
+
+    // If you want to observe other state flags, be sure they're listed in the
+    // Tab._createBrowser's call to addProgressListener
+    if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
+      if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
+        this._loadingController.startLoading();
+      }
+      else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+        this._loadingController.stopLoading();
+      }
+    }
+  },
+
+  /** This method is called to indicate progress changes for the currently loading page. */
+  onProgressChange: function onProgressChange(aWebProgress, aRequest, aCurSelf, aMaxSelf, aCurTotal, aMaxTotal) {
+  },
+
+  /** This method is called to indicate a change to the current location. */
+  onLocationChange: function onLocationChange(aWebProgress, aRequest, aLocationURI) {
+  },
+
+  /**
+   * This method is called to indicate a status changes for the currently
+   * loading page.  The message is already formatted for display.
+   */
+  onStatusChange: function onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
+  },
+
+  /** This method is called when the security state of the browser changes. */
+  onSecurityChange: function onSecurityChange(aWebProgress, aRequest, aState) {
+  },
+
+  QueryInterface: function QueryInterface(aIID) {
+    if (aIID.equals(Ci.nsIWebProgressListener) ||
+        aIID.equals(Ci.nsISupportsWeakReference) ||
+        aIID.equals(Ci.nsISupports)) {
+        return this;
+    }
+
+    throw Components.results.NS_ERROR_NO_INTERFACE;
+  },
+
+  start: function start() {
+    let flags = Ci.nsIWebProgress.NOTIFY_STATE_NETWORK;
+    let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+      .getInterface(Ci.nsIWebProgress);
+    webProgress.addProgressListener(this, flags);
+  },
+
+  stop: function stop() {
+    let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+      .getInterface(Ci.nsIWebProgress);
+    webProgress.removeProgressListener(this);
+  }
+};
+
+
+/**
+ * Responsible for opening up form assistant and sending messages to the FormHelper when user
+ * types keys to navigate.
+ */
+function ContentFormManager() {
+  this._navigator = null;
+
+  addMessageListener("FennecClosedFormAssist", this);
+  addMessageListener("FennecFormPrevious", this);
+  addMessageListener("FennecFormNext", this);
+  addMessageListener("FennecFormChoiceSelect", this);
+  addMessageListener("FennecFormChoiceChange", this);
+}
+
+ContentFormManager.prototype = {
+  formAssist: function(element) {
+    if (!element)
+      return false;
+
+    let wrapper = new BasicWrapper(element);
+    if (!wrapper.canAssist())
+      return false;
+
+    let navigationEnabled = gPrefService.getBoolPref("formhelper.enabled");
+    if (!navigationEnabled && !wrapper.hasChoices())
+      return false;
+
+    let navigator = new FormNavigator(this, element, navigationEnabled);
+    if (!navigator.getCurrent())
+      return false;
+
+    sendMessage("FennecFormAssist", navigator.getJSON());
+
+    if (!this._navigator)
+      addEventListener("keyup", this, false);
+    this._navigator = navigator;
+
+    return true;
+  },
+
+  closeFormAssistant: function() {
+    if (this._navigator) {
+      sendMessage("FennecCloseFormAssist");
+      this.closedFormAssistant();
+    }
+  },
+
+  closedFormAssistant: function() {
+    if (this._navigator) {
+      removeEventListener("keyup", this, false);
+      this._navigator = null;
+    }
+  },
+
+  receiveMessage: Util.receiveMessage,
+
+  receiveFennecClosedFormAssist: function() {
+    this.closedFormAssistant();
+  },
+
+  receiveFennecFormPrevious: function() {
+    if (this._navigator) {
+      this._navigator.goToPrevious();
+      sendMessage("FennecFormAssist", this._navigator.getJSON());
+    }
+  },
+
+  receiveFennecFormNext: function() {
+    if (this._navigator) {
+      this._navigator.goToNext();
+      sendMessage("FennecFormAssist", this._navigator.getJSON());
+    }
+  },
+
+  receiveFennecFormChoiceSelect: function(message, index, selected, clearAll) {
+    if (this._navigator) {
+      let current = this._navigator.getCurrent();
+      if (current)
+        current.choiceSelect(index, selected, clearAll);
+    }
+  },
+
+  receiveFennecFormChoiceChange: function() {
+    if (this._navigator) {
+      let current = this._navigator.getCurrent();
+      if (current)
+        current.choiceChange();
+    }
+  },
+
+  handleEvent: function formHelperHandleEvent(aEvent) {
+    let currentWrapper = this._navigator.getCurrent();
+    let currentElement = currentWrapper.element;
+
+    switch (aEvent.keyCode) {
+      case aEvent.DOM_VK_DOWN:
+        if (currentElement instanceof HTMLTextAreaElement) {
+          let existSelection = currentElement.selectionEnd - currentElement.selectionStart;
+          let isEnd = (currentElement.textLength == currentElement.selectionEnd);
+          if (!isEnd || existSelection)
+            return;
+        }
+
+        this._navigator.goToNext();
+        break;
+
+      case aEvent.DOM_VK_UP:
+        if (currentElement instanceof HTMLTextAreaElement) {
+          let existSelection = currentElement.selectionEnd - currentElement.selectionStart;
+          let isStart = (currentElement.selectionEnd == 0);
+          if (!isStart || existSelection)
+            return;
+        }
+
+        this._navigator.goToPrevious();
+        break;
+
+      case aEvent.DOM_VK_RETURN:
+        break;
+
+      default:
+        let target = aEvent.target;
+        if (currentWrapper.canAutocomplete())
+          sendMessage("FennecFormAutocomplete", currentWrapper.getAutocompleteSuggestions());
+        break;
+    }
+
+    let caretRect = currentWrapper.getCaretRect();
+    if (!caretRect.isEmpty()) {
+      sendMessage("FennecCaretRect", caretRect);
+    }
+  },
+
+  _getRectForCaret: function _getRectForCaret() {
+    let currentElement = this.getCurrent();
+    let rect = currentElement.getCaretRect();
+    return null;
+  },
+  
+};
+
+
+/**
+ * Responsible for iterating through form elements.
+ * The navigable list is generated on instantiation, so construct new FormNavigators when
+ * the current document is changed or the list of form elements needs to be regenerated.
+ */
+function FormNavigator(manager, element, showNavigation) {
+  this._manager = manager;
+  this._showNavigation = showNavigation;
+  
+  if (showNavigation) {
+    this._wrappers = [];
+    this._currentIndex = -1;
+    this._getAllWrappers(element);
+  } else {
+    this._wrappers = [element];
+    this._currentIndex = 0;
+  }
+}
+
+FormNavigator.prototype = {
+  endNavigation: function() {
+    this._manager.closedFormAssistant();
+  },
+
+  getCurrent: function() {
+    return this._wrappers[this._currentIndex];
+  },
+
+  getJSON: function() {
+    return {
+      hasNext: !!this.getNext(),
+      hasPrevious: !!this.getPrevious(),
+      current: this.getCurrent().getJSON(),
+      showNavigation: this._showNavigation
+    };
+  },
+
+  getPrevious: function() {
+    return this._wrappers[this._currentIndex - 1];
+  },
+
+  getNext: function() {
+    return this._wrappers[this._currentIndex + 1];
+  },
+
+  goToPrevious: function() {
+    return this._setIndex(this._currentIndex - 1);
+  },
+
+  goToNext: function() {
+    return this._setIndex(this._currentIndex + 1);
+  },
+
+  _getAllWrappers: function(element) {
+    // XXX good candidate for tracing if possible.  The tough ones are length and
+    // canNavigateTo / isVisible.
+
+    let document = element.ownerDocument;
+    if (!document)
+      return;
+
+    let elements = this._wrappers;
+
+    // get all the documents
+    let documents = [document];
+    let iframes = document.querySelectorAll("iframe, frame");
+    for (let i = 0; i < iframes.length; i++)
+      documents.push(iframes[i].contentDocument);
+
+    for (let i = 0; i < documents.length; i++) {
+      let nodes = documents[i].querySelectorAll("input, button, select, textarea, [role=button]");
+      nodes = this._filterRadioButtons(nodes);
+
+      for (let j = 0; j < nodes.length; j++) {
+        let node = nodes[j];
+        let wrapper = new BasicWrapper(node);
+        if (wrapper.canNavigateTo() && wrapper.isVisible()) {
+          elements.push(wrapper);
+          if (node == element)
+            this._setIndex(elements.length - 1);
+        }
+      }
+    }
+
+    function orderByTabIndex(a, b) {
+      // for an explanation on tabbing navigation see
+      // http://www.w3.org/TR/html401/interact/forms.html#h-17.11.1
+      // In resume tab index navigation order is 1, 2, 3, ..., 32767, 0
+      if (a.tabIndex == 0 || b.tabIndex == 0)
+        return b.tabIndex;
+
+      return a.tabIndex > b.tabIndex;
+    }
+    elements = elements.sort(orderByTabIndex);
+  },
+
+  /**
+   * For each radio button group, remove all but the checked button
+   * if there is one, or the first button otherwise.
+   */
+  _filterRadioButtons: function(nodes) {
+    // First pass: Find the checked or first element in each group.
+    let chosenRadios = {};
+    for (let i=0; i < nodes.length; i++) {
+      let node = nodes[i];
+      if (node.type == "radio" && (!chosenRadios.hasOwnProperty(node.name) || node.checked))
+        chosenRadios[node.name] = node;
+    }
+
+    // Second pass: Exclude all other radio buttons from the list.
+    var result = [];
+    for (let i=0; i < nodes.length; i++) {
+      let node = nodes[i];
+      if (node.type == "radio" && chosenRadios[node.name] != node)
+        continue;
+      result.push(node);
+    }
+    return result;
+  },
+
+  _setIndex: function(i) {
+    let element = this._wrappers[i];
+    if (element) {
+      gFocusManager.setFocus(element.element, Ci.nsIFocusManager.FLAG_NOSCROLL);
+      this._currentIndex = i;
+      return true;
+    } else {
+      return false;
+    }
+  }
+};
+
+
+/** Can't think of a good description of this class.  It probably does too much? */
+function Content() {
+  this._iconURI = null;
+
+  addMessageListener("FennecBlur", this);
+  addMessageListener("FennecFocus", this);
+  addMessageListener("FennecMousedown", this);
+  addMessageListener("FennecMouseup", this);
+  addMessageListener("FennecCancelMouse", this);
+
+  this._coalescer = new Coalescer();
+  addEventListener("MozAfterPaint", this._coalescer, false);
+  addEventListener("MozScrolledAreaChanged", this._coalescer, false);
+
+  this._progressController = new ProgressController(this);
+  this._progressController.start();
+
+  this._contentFormManager = new ContentFormManager();
+
+  this._mousedownTimeout = new Util.Timeout();
+}
+
+Content.prototype = {
+  handleEvent: Util.handleEvent,
+
+  receiveMessage: Util.receiveMessage,
+
+  receiveFennecFocus: function receiveFennecFocus() {
+    docShell.isOffScreenBrowser = true;
+    this._selected = true;
+  },
+
+  receiveFennecBlur: function receiveFennecBlur() {
+    docShell.isOffScreenBrowser = false;
+    this._selected = false;
+  },
+
+  _sendMouseEvent: function _sendMouseEvent(name, element, x, y) {
+    let windowUtils = Util.getWindowUtils(content);
+    let scrollOffset = Util.getScrollOffset(content);
+
+    // the element can be out of the cX/cY point because of the touch radius
+    let rect = getBoundingContentRect(element);
+    if (!rect.isEmpty() && !(element instanceof HTMLHtmlElement) ||
+        x < rect.left || x > rect.right ||
+        y < rect.top || y > rect.bottom) {
+      let point = rect.center();
+      x = point.x;
+      y = point.y;
+    }
+
+    windowUtils.sendMouseEvent(name, x - scrollOffset.x, y - scrollOffset.y, 0, 1, 0, true);
+  },
+
+  receiveFennecMousedown: function receiveFennecMousedown(message, x, y) {
+    this._mousedownTimeout.once(kTapOverlayTimeout, function() {
+      let element = elementFromPoint(x, y);
+      gFocusManager.setFocus(element, Ci.nsIFocusManager.FLAG_NOSCROLL);
+    });
+  },
+
+  receiveFennecMouseup: function receiveFennecMouseup(message, x, y) {
+    this._mousedownTimeout.flush();
+
+    let element = elementFromPoint(x, y);
+    if (!this._contentFormManager.formAssist(element)) {
+      this._sendMouseEvent("mousedown", element, x, y);
+      this._sendMouseEvent("mouseup", element, x, y);
+    }
+  },
+
+  receiveFennecCancelMouse: function receiveFennecCancelMouse() {
+    this._mousedownTimeout.clear();
+    // XXX there must be a better way than this to cancel the mouseover/focus?
+    this._sendMouseEvent("mouseup", null, -1000, -1000);
+    let element = content.document.activeElement;
+    if (element)
+      element.blur();
+  },
+
+  startLoading: function startLoading() {
+    this._loading = true;
+    this._iconURI = null;
+    this._coalescer.emptyPage();
+    this._coalescer.startCoalescing();
+  },
+
+  stopLoading: function stopLoading() {
+    this._loading = false;
+    this._coalescer.stopCoalescing();
+    sendMessage("FennecMetadata", Util.getViewportMetadata(content));
+  },
+
+  isSelected: function isSelected() {
+    return this._selected;
+  }
+};
+
+
+let contentObject = new Content();
--- a/mobile/chrome/jar.mn
+++ b/mobile/chrome/jar.mn
@@ -15,20 +15,23 @@ chrome.jar:
   content/config.js                    (content/config.js)
   content/aboutCertError.xhtml         (content/aboutCertError.xhtml)
   content/aboutCertError.css           (content/aboutCertError.css)
   content/aboutHome.xhtml              (content/aboutHome.xhtml)
   content/languages.properties         (content/languages.properties)
 * content/browser.xul                  (content/browser.xul)
 * content/browser.js                   (content/browser.js)
 * content/browser-ui.js                (content/browser-ui.js)
+  content/content.js                   (content/content.js)
   content/commandUtil.js               (content/commandUtil.js)
   content/bindings.xml                 (content/bindings.xml)
   content/tabs.xml                     (content/tabs.xml)
   content/bindings/checkbox.xml        (content/bindings/checkbox.xml)
+  content/bindings/browser.xml         (content/bindings/browser.xml)
+  content/bindings/browser.js          (content/bindings/browser.js)
   content/notification.xml             (content/notification.xml)
   content/bindings/extensions.xml      (content/bindings/extensions.xml)
   content/bindings/downloads.xml       (content/bindings/downloads.xml)
   content/bindings/console.xml         (content/bindings/console.xml)
   content/bindings/dialog.xml          (content/bindings/dialog.xml)
   content/bindings/pageaction.xml      (content/bindings/pageaction.xml)
   content/bindings/setting.xml         (content/bindings/setting.xml)
   content/browser.css                  (content/browser.css)