Merge fx-team to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Tue, 15 Jul 2014 17:26:32 -0700
changeset 215002 869971ad9fd64529113755e571fc88ae2901559f
parent 214992 95f2ad2787b32a3c5e493bad48ece712cbf4de3e (current diff)
parent 215001 e9d78c3d2eb584b74266b6579cbb1e5ee45005fd (diff)
child 215017 d49bbb2b851f2a8740889a62f5007568bbb1d3b7
child 215083 ae7ad782c7125c6bc37eb731f8d65ed15f28a477
child 215130 1fef40c21b5e9dfae647122e211ed81b96a515be
push id3857
push userraliiev@mozilla.com
push dateTue, 02 Sep 2014 16:39:23 +0000
treeherdermozilla-beta@5638b907b505 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone33.0a1
first release with
nightly linux32
869971ad9fd6 / 33.0a1 / 20140716030202 / files
nightly linux64
869971ad9fd6 / 33.0a1 / 20140716030202 / files
nightly mac
869971ad9fd6 / 33.0a1 / 20140716030202 / files
nightly win32
869971ad9fd6 / 33.0a1 / 20140716030202 / files
nightly win64
869971ad9fd6 / 33.0a1 / 20140716030202 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c a=merge
addon-sdk/source/test/test-packaging.js
--- a/addon-sdk/source/lib/sdk/deprecated/unit-test.js
+++ b/addon-sdk/source/lib/sdk/deprecated/unit-test.js
@@ -446,17 +446,17 @@ TestRunner.prototype = {
     if (ms === undefined)
       ms = this.DEFAULT_PAUSE_TIMEOUT;
 
     var self = this;
 
     function tiredOfWaiting() {
       self._logTestFailed("timed out");
       if ("testMessage" in self.console) {
-        self.console.testMessage(false, false, self.test.name, "Timed out");
+        self.console.testMessage(false, false, self.test.name, "Test timed out");
       }
       else {
         self.console.error("fail:", "Timed out")
       }
       if (self.waitUntilCallback) {
         self.waitUntilCallback(true);
         self.waitUntilCallback = null;
       }
--- a/addon-sdk/source/lib/sdk/panel.js
+++ b/addon-sdk/source/lib/sdk/panel.js
@@ -68,17 +68,18 @@ let displayContract = contract({
 let panelContract = contract(merge({
   // contentStyle* / contentScript* are sharing the same validation constraints,
   // so they can be mostly reused, except for the messages.
   contentStyle: merge(Object.create(loaderContract.rules.contentScript), {
     msg: 'The `contentStyle` option must be a string or an array of strings.'
   }),
   contentStyleFile: merge(Object.create(loaderContract.rules.contentScriptFile), {
     msg: 'The `contentStyleFile` option must be a local URL or an array of URLs'
-  })
+  }),
+  contextMenu: boolean
 }, displayContract.rules, loaderContract.rules));
 
 
 function isDisposed(panel) !views.has(panel);
 
 let panels = new WeakMap();
 let models = new WeakMap();
 let views = new WeakMap();
@@ -129,16 +130,17 @@ const Panel = Class({
   ],
   extends: WorkerHost(workerFor),
   setup: function setup(options) {
     let model = merge({
       defaultWidth: 320,
       defaultHeight: 240,
       focus: true,
       position: Object.freeze({}),
+      contextMenu: false
     }, panelContract(options));
     models.set(this, model);
 
     if (model.contentStyle || model.contentStyleFile) {
       styles.set(this, Style({
         uri: model.contentStyleFile,
         source: model.contentStyle
       }));
@@ -146,16 +148,19 @@ const Panel = Class({
 
     // Setup view
     let view = domPanel.make();
     panels.set(view, this);
     views.set(this, view);
 
     // Load panel content.
     domPanel.setURL(view, model.contentURL);
+    
+    // Allow context menu
+    domPanel.allowContextMenu(view, model.contextMenu);
 
     setupAutoHide(this);
 
     // Setup listeners.
     setListeners(this, options);
     let worker = new Worker(stripListeners(options));
     workers.set(this, worker);
 
@@ -183,17 +188,25 @@ const Panel = Class({
   get height() modelFor(this).height,
   set height(value) this.resize(this.width, value),
 
   /* Public API: Panel.focus */
   get focus() modelFor(this).focus,
 
   /* Public API: Panel.position */
   get position() modelFor(this).position,
-
+  
+  /* Public API: Panel.contextMenu */
+  get contextMenu() modelFor(this).contextMenu,
+  set contextMenu(allow) {
+    let model = modelFor(this);
+    model.contextMenu = panelContract({ contextMenu: allow }).contextMenu;
+    domPanel.allowContextMenu(viewFor(this), model.contextMenu);
+  },
+    
   get contentURL() modelFor(this).contentURL,
   set contentURL(value) {
     let model = modelFor(this);
     model.contentURL = panelContract({ contentURL: value }).contentURL;
     domPanel.setURL(viewFor(this), model.contentURL);
     // Detach worker so that messages send will be queued until it's
     // reatached once panel content is ready.
     workerFor(this).detach();
@@ -221,17 +234,18 @@ const Panel = Class({
     let anchorView = getNodeView(anchor || options.position || model.position);
 
     options = merge({
       position: model.position,
       width: model.width,
       height: model.height,
       defaultWidth: model.defaultWidth,
       defaultHeight: model.defaultHeight,
-      focus: model.focus
+      focus: model.focus,
+      contextMenu: model.contextMenu
     }, displayContract(options));
 
     if (!isDisposed(this))
       domPanel.show(view, options, anchorView);
 
     return this;
   },
 
--- a/addon-sdk/source/lib/sdk/panel/utils.js
+++ b/addon-sdk/source/lib/sdk/panel/utils.js
@@ -206,17 +206,17 @@ function shimDefaultStyle(panel) {
       if (node) node.style.padding = 0;
   });
 }
 
 function show(panel, options, anchor) {
   // Prevent the panel from getting focus when showing up
   // if focus is set to false
   panel.setAttribute("noautofocus", !options.focus);
-
+  
   let window = anchor && getOwnerBrowserWindow(anchor);
   let { document } = window ? window : getMostRecentBrowserWindow();
   attach(panel, document);
 
   open(panel, options, anchor);
 }
 exports.show = show
 
@@ -399,8 +399,18 @@ let getContentFrame = panel =>
     panel.backgroundFrame
 exports.getContentFrame = getContentFrame;
 
 function getContentDocument(panel) getContentFrame(panel).contentDocument
 exports.getContentDocument = getContentDocument;
 
 function setURL(panel, url) getContentFrame(panel).setAttribute("src", url)
 exports.setURL = setURL;
+
+function allowContextMenu(panel, allow) {
+  if(allow) {
+    panel.setAttribute("context", "contentAreaContextMenu");
+  } 
+  else {
+    panel.removeAttribute("context");
+  }
+}
+exports.allowContextMenu = allowContextMenu;
--- a/addon-sdk/source/lib/sdk/test/harness.js
+++ b/addon-sdk/source/lib/sdk/test/harness.js
@@ -13,16 +13,17 @@ const { serializeStack, parseStack  } = 
 const { setTimeout } = require('../timers');
 const { PlainTextConsole } = require("../console/plain-text");
 const { when: unload } = require("../system/unload");
 const { format, fromException }  = require("../console/traceback");
 const system = require("../system");
 const memory = require('../deprecated/memory');
 const { gc: gcPromise } = require('./memory');
 const { defer } = require('../core/promise');
+const { extend } = require('../core/heritage');
 
 // Trick manifest builder to make it think we need these modules ?
 const unit = require("../deprecated/unit-test");
 const test = require("../../test");
 const url = require("../url");
 
 function emptyPromise() {
   let { promise, resolve } = defer();
@@ -448,35 +449,35 @@ var consoleListener = {
     var pointless = [err for each (err in POINTLESS_ERRORS)
                          if (message.indexOf(err) >= 0)];
     if (pointless.length == 0 && message)
       testConsole.log(message);
   }
 };
 
 function TestRunnerConsole(base, options) {
-  this.__proto__ = {
+  let proto = extend(base, {
     errorsLogged: 0,
     warn: function warn() {
       this.errorsLogged++;
       base.warn.apply(base, arguments);
     },
     error: function error() {
       this.errorsLogged++;
       base.error.apply(base, arguments);
     },
     info: function info(first) {
       if (options.verbose)
         base.info.apply(base, arguments);
       else
         if (first == "pass:")
           print(".");
     },
-    __proto__: base
-  };
+  });
+  return Object.create(proto);
 }
 
 function stringify(arg) {
   try {
     return String(arg);
   }
   catch(ex) {
     return "<toString() error>";
--- a/addon-sdk/source/lib/sdk/test/loader.js
+++ b/addon-sdk/source/lib/sdk/test/loader.js
@@ -14,28 +14,30 @@ let defaultGlobals = override(require('.
   console: console
 });
 
 function CustomLoader(module, globals, packaging, overrides={}) {
   let options = packaging || require("@loader/options");
   options = override(options, {
     id: overrides.id || options.id,
     globals: override(defaultGlobals, globals || {}),
-    modules: override(options.modules || {}, {
+    modules: override(override(options.modules || {}, overrides.modules || {}), {
       'sdk/addon/window': addonWindow
     })
   });
 
   let loaderModule = options.isNative ? '../../toolkit/loader' : '../loader/cuddlefish';
   let { Loader } = require(loaderModule);
   let loader = Loader(options);
   let wrapper = Object.create(loader, descriptor({
     require: Require(loader, module),
     sandbox: function(id) {
       let requirement = loader.resolve(id, module.id);
+      if (!requirement)
+        requirement = id;
       let uri = resolveURI(requirement, loader.mapping);
       return loader.sandboxes[uri];
     },
     unload: function(reason) {
       unload(loader, reason);
     }
   }));
   ensure(wrapper);
@@ -68,23 +70,23 @@ exports.LoaderWithHookedConsole = functi
     messages.push({ type: type, msg: msg, innerID: innerID });
     if (callback)
       callback(type, msg, innerID);
   }
 
   return {
     loader: CustomLoader(module, {
       console: new HookedPlainTextConsole(hook, null, null)
-    }, override(require("@loader/options"), {
+    }, null, {
       modules: {
         'sdk/console/plain-text': {
           PlainTextConsole: HookedPlainTextConsole.bind(null, hook)
         }
       }
-    })),
+    }),
     messages: messages
   };
 }
 
 // Same than LoaderWithHookedConsole with lower level, instead we get what is
 // actually printed to the command line console
 exports.LoaderWithHookedConsole2 = function (module, callback) {
   let messages = [];
@@ -107,16 +109,16 @@ exports.LoaderWithFilteredConsole = func
   function hook(type, innerID, msg) {
     if (callback && callback(type, msg, innerID) == false)
       return;
     console[type](msg);
   }
 
   return CustomLoader(module, {
     console: new HookedPlainTextConsole(hook, null, null)
-  }, override(require("@loader/options"), {
+  }, null, {
     modules: {
       'sdk/console/plain-text': {
         PlainTextConsole: HookedPlainTextConsole.bind(null, hook)
       }
     }
-  }));
+  });
 }
--- a/addon-sdk/source/lib/toolkit/loader.js
+++ b/addon-sdk/source/lib/toolkit/loader.js
@@ -709,17 +709,18 @@ const Loader = iced(function Loader(opti
     metadata, sharedGlobal, sharedGlobalBlacklist
   } = override({
     paths: {},
     modules: {},
     globals: {
       console: console
     },
     resolve: options.isNative ?
-      exports.nodeResolve :
+      // Make the returned resolve function have the same signature
+      (id, requirer) => exports.nodeResolve(id, requirer, { rootURI: rootURI }) :
       exports.resolve,
     sharedGlobalBlacklist: ["sdk/indexed-db"]
   }, options);
 
   // We create an identity object that will be dispatched on an unload
   // event as subject. This way unload listeners will be able to assert
   // which loader is unloaded. Please note that we intentionally don't
   // use `loader` as subject to prevent a loader access leakage through
--- a/addon-sdk/source/python-lib/cuddlefish/__init__.py
+++ b/addon-sdk/source/python-lib/cuddlefish/__init__.py
@@ -229,16 +229,21 @@ parser_groups = (
         (("", "--output-file",), dict(dest="output_file",
                                       help="Where to put the finished .xpi",
                                       default=None,
                                       cmds=['xpi'])),
         (("", "--manifest-overload",), dict(dest="manifest_overload",
                                       help="JSON file to overload package.json properties",
                                       default=None,
                                       cmds=['xpi'])),
+        (("", "--abort-on-missing-module",), dict(dest="abort_on_missing",
+                                      help="Abort if required module is missing",
+                                      action="store_true",
+                                      default=False,
+                                      cmds=['test', 'run', 'xpi', 'testpkgs'])),
         ]
      ),
 
     ("Internal Command-Specific Options", [
         (("", "--addons",), dict(dest="addons",
                                  help=("paths of addons to install, "
                                        "comma-separated"),
                                  metavar=None,
@@ -646,17 +651,18 @@ def run(arguments=sys.argv[1:], target_c
     if options.manifest_overload:
         for k, v in packaging.load_json_file(options.manifest_overload).items():
             target_cfg[k] = v
 
     # At this point, we're either building an XPI or running Jetpack code in
     # a Mozilla application (which includes running tests).
 
     use_main = False
-    inherited_options = ['verbose', 'enable_e10s', 'parseable', 'check_memory']
+    inherited_options = ['verbose', 'enable_e10s', 'parseable', 'check_memory',
+                         'abort_on_missing']
     enforce_timeouts = False
 
     if command == "xpi":
         use_main = True
     elif command == "test":
         if 'tests' not in target_cfg:
             target_cfg['tests'] = []
         inherited_options.extend(['iterations', 'filter', 'profileMemory',
@@ -741,19 +747,19 @@ def run(arguments=sys.argv[1:], target_c
     loader_modules = [("addon-sdk", "lib", "sdk/loader/cuddlefish", cuddlefish_js_path)]
     scan_tests = command == "test"
     test_filter_re = None
     if scan_tests and options.filter:
         test_filter_re = options.filter
         if ":" in options.filter:
             test_filter_re = options.filter.split(":")[0]
     try:
-        manifest = build_manifest(target_cfg, pkg_cfg, deps,
-                                  scan_tests, test_filter_re,
-                                  loader_modules)
+        manifest = build_manifest(target_cfg, pkg_cfg, deps, scan_tests,
+                                  test_filter_re, loader_modules,
+                                  abort_on_missing=options.abort_on_missing)
     except ModuleNotFoundError, e:
         print str(e)
         sys.exit(1)
     except BadChromeMarkerError, e:
         # An error had already been displayed on stderr in manifest code
         sys.exit(1)
     used_deps = manifest.get_used_packages()
     if command == "test":
@@ -783,16 +789,20 @@ def run(arguments=sys.argv[1:], target_c
     # When cfx is run from sdk root directory, we will strip sdk modules and
     # override them with local modules.
     # So that integration tools will continue to work and use local modules
     if os.getcwd() == env_root:
         options.bundle_sdk = True
         options.force_use_bundled_sdk = False
         options.overload_modules = True
 
+    if options.pkgdir == env_root:
+        options.bundle_sdk = True
+        options.overload_modules = True
+
     extra_environment = {}
     if command == "test":
         # This should be contained in the test runner package.
         harness_options['main'] = 'sdk/test/runner'
         harness_options['mainPath'] = 'sdk/test/runner'
     else:
         harness_options['main'] = target_cfg.get('main')
         harness_options['mainPath'] = manifest.top_path
--- a/addon-sdk/source/python-lib/cuddlefish/manifest.py
+++ b/addon-sdk/source/python-lib/cuddlefish/manifest.py
@@ -176,28 +176,29 @@ class ModuleInfo:
     def __repr__(self):
         return "ModuleInfo [%s %s %s] (%s, %s)" % (self.package.name,
                                                    self.section,
                                                    self.name,
                                                    self.js, self.docs)
 
 class ManifestBuilder:
     def __init__(self, target_cfg, pkg_cfg, deps, extra_modules,
-                 stderr=sys.stderr):
+                 stderr=sys.stderr, abort_on_missing=False):
         self.manifest = {} # maps (package,section,module) to ManifestEntry
         self.target_cfg = target_cfg # the entry point
         self.pkg_cfg = pkg_cfg # all known packages
         self.deps = deps # list of package names to search
         self.used_packagenames = set()
         self.stderr = stderr
         self.extra_modules = extra_modules
         self.modules = {} # maps ModuleInfo to URI in self.manifest
         self.datamaps = {} # maps package name to DataMap instance
         self.files = [] # maps manifest index to (absfn,absfn) js/docs pair
         self.test_modules = [] # for runtime
+        self.abort_on_missing = abort_on_missing # cfx eol
 
     def build(self, scan_tests, test_filter_re):
         """
         process the top module, which recurses to process everything it reaches
         """
         if "main" in self.target_cfg:
             top_mi = self.find_top(self.target_cfg)
             top_me = self.process_module(top_mi)
@@ -411,16 +412,22 @@ class ManifestBuilder:
                 looked_in = [] # populated by subroutines
                 them_me = self.find_req_for(mi, reqname, looked_in, locations)
                 if them_me is None:
                     if mi.section == "tests":
                         # tolerate missing modules in tests, because
                         # test-securable-module.js, and the modules/red.js
                         # that it imports, both do that intentionally
                         continue
+                    if not self.abort_on_missing:
+                        # print a warning, but tolerate missing modules
+                        # unless cfx --abort-on-missing-module flag was set
+                        print >>self.stderr, "Warning: missing module: %s" % reqname
+                        me.add_requirement(reqname, reqname)
+                        continue
                     lineno = locations.get(reqname) # None means define()
                     if lineno is None:
                         reqtype = "define"
                     else:
                         reqtype = "require"
                     err = ModuleNotFoundError(reqtype, reqname,
                                               mi.js, lineno, looked_in)
                     raise err
@@ -628,17 +635,17 @@ class ManifestBuilder:
                     maybe_docs = os.path.join(pkg.root_dir, "docs",
                                               basename+".md")
                     if section == "lib" and os.path.exists(maybe_docs):
                         docs = maybe_docs
                     return ModuleInfo(pkg, section, name, js, docs)
         return None
 
 def build_manifest(target_cfg, pkg_cfg, deps, scan_tests,
-                   test_filter_re=None, extra_modules=[]):
+                   test_filter_re=None, extra_modules=[], abort_on_missing=False):
     """
     Perform recursive dependency analysis starting from entry_point,
     building up a manifest of modules that need to be included in the XPI.
     Each entry will map require() names to the URL of the module that will
     be used to satisfy that dependency. The manifest will be used by the
     runtime's require() code.
 
     This returns a ManifestBuilder object, with two public methods. The
@@ -654,17 +661,18 @@ def build_manifest(target_cfg, pkg_cfg, 
 
      * local disk name
      * name in the XPI
 
     note: we don't build the XPI here, but our manifest is passed to the
     code which does, so it knows what to copy into the XPI.
     """
 
-    mxt = ManifestBuilder(target_cfg, pkg_cfg, deps, extra_modules)
+    mxt = ManifestBuilder(target_cfg, pkg_cfg, deps, extra_modules,
+                          abort_on_missing=abort_on_missing)
     mxt.build(scan_tests, test_filter_re)
     return mxt
 
 
 
 COMMENT_PREFIXES = ["//", "/*", "*", "dump("]
 
 REQUIRE_RE = r"(?<![\'\"])require\s*\(\s*[\'\"]([^\'\"]+?)[\'\"]\s*\)"
--- a/addon-sdk/source/python-lib/cuddlefish/tests/test_linker.py
+++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_linker.py
@@ -43,30 +43,46 @@ class Basic(unittest.TestCase):
         # target_cfg.dependencies is not provided, so we'll search through
         # all known packages (everything in 'deps').
         m = manifest.build_manifest(target_cfg, pkg_cfg, deps, scan_tests=False)
         m = m.get_harness_options_manifest(False)
 
         def assertReqIs(modname, reqname, path):
             reqs = m["one/%s" % modname]["requirements"]
             self.failUnlessEqual(reqs[reqname], path)
+
         assertReqIs("main", "sdk/panel", "sdk/panel")
         assertReqIs("main", "two.js", "one/two")
         assertReqIs("main", "./two", "one/two")
         assertReqIs("main", "sdk/tabs.js", "sdk/tabs")
         assertReqIs("main", "./subdir/three", "one/subdir/three")
         assertReqIs("two", "main", "one/main")
         assertReqIs("subdir/three", "../main", "one/main")
 
         target_cfg.dependencies = []
+
+        try:
+            # this should now work, as we ignore missing modules by default
+            m = manifest.build_manifest(target_cfg, pkg_cfg, deps, scan_tests=False)
+            m = m.get_harness_options_manifest(False)
+
+            assertReqIs("main", "sdk/panel", "sdk/panel")
+            # note that with "addon-sdk" dependency present,
+            # "sdk/tabs.js" mapped to "sdk/tabs", but without,
+            # we just get the default (identity) mapping
+            assertReqIs("main", "sdk/tabs.js", "sdk/tabs.js")
+        except Exception, e:
+            self.fail("Must not throw from build_manifest() if modules are missing")
+
         # now, because .dependencies *is* provided, we won't search 'deps',
-        # so we'll get a link error
+        # and stop_on_missing is True, we'll get a link error
         self.assertRaises(manifest.ModuleNotFoundError,
                           manifest.build_manifest,
-                          target_cfg, pkg_cfg, deps, scan_tests=False)
+                          target_cfg, pkg_cfg, deps, scan_tests=False,
+                          abort_on_missing=True)
 
     def test_main_in_deps(self):
         target_cfg = self.get_pkg("three")
         package_path = [get_linker_files_dir("three-deps")]
         pkg_cfg = packaging.build_config(ROOT, target_cfg,
                                          packagepath=package_path)
         deps = packaging.get_deps_for_targets(pkg_cfg,
                                               [target_cfg.name, "addon-sdk"])
--- a/addon-sdk/source/test/addons/e10s-tabs/lib/main.js
+++ b/addon-sdk/source/test/addons/e10s-tabs/lib/main.js
@@ -1,13 +1,19 @@
 /* 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/. */
 'use strict';
 
 const { merge } = require('sdk/util/object');
+const { get } = require('sdk/preferences/service');
 
 merge(module.exports, require('./test-tab'));
 merge(module.exports, require('./test-tab-events'));
 merge(module.exports, require('./test-tab-observer'));
 merge(module.exports, require('./test-tab-utils'));
 
+// e10s tests should not ride the train to aurora
+if (get('app.update.channel') !== 'nightly') {
+  module.exports = {};
+}
+
 require('sdk/test/runner').runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/packaging/main.js
@@ -0,0 +1,46 @@
+/* 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/. */
+"use strict";
+
+var options = require("@loader/options");
+
+exports.testPackaging = function(assert) {
+  assert.equal(options.metadata.description,
+                   "Add-on development made easy.",
+                   "packaging metadata should be available");
+  try {
+    options.metadata.description = 'new description';
+    assert.fail('should not have been able to set options.metadata property');
+  }
+  catch (e) {}
+
+  assert.equal(options.metadata.description,
+                   "Add-on development made easy.",
+                   "packaging metadata should be frozen");
+
+  assert.equal(options.metadata.permissions['private-browsing'], undefined,
+                   "private browsing metadata should be undefined");
+  assert.equal(options.metadata['private-browsing'], undefined,
+                   "private browsing metadata should be be frozen");
+  assert.equal(options['private-browsing'], undefined,
+                   "private browsing metadata should be be frozen");
+
+  try {
+    options.metadata['private-browsing'] = true;
+    assert.fail('should not have been able to set options.metadata property');
+  }
+  catch(e) {}
+  assert.equal(options.metadata['private-browsing'], undefined,
+                   "private browsing metadata should be be frozen");
+
+  try {
+    options.metadata.permissions['private-browsing'] = true;
+    assert.fail('should not have been able to set options.metadata.permissions property');
+  }
+  catch (e) {}
+  assert.equal(options.metadata.permissions['private-browsing'], undefined,
+                   "private browsing metadata should be be frozen");
+};
+
+require("sdk/test/runner").runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/packaging/package.json
@@ -0,0 +1,4 @@
+{
+  "id": "test-packaging",
+  "description": "Add-on development made easy."
+}
--- a/addon-sdk/source/test/addons/self/main.js
+++ b/addon-sdk/source/test/addons/self/main.js
@@ -12,9 +12,13 @@ exports["test self.data.load"] = assert 
                "# hello world",
                "paths work");
 
   assert.equal(self.data.load("./data.md").trim(),
                "# hello world",
                "relative paths work");
 };
 
+exports["test self.id"] = assert => {
+  assert.equal(self.id, "test-self@jetpack", "self.id should be correct.");
+};
+
 require("sdk/test/runner").runTestsFromModule(module);
--- a/addon-sdk/source/test/event/helpers.js
+++ b/addon-sdk/source/test/event/helpers.js
@@ -1,16 +1,65 @@
 /* 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/. */
 
 "use strict";
 
 const { on, once, off, emit, count } = require("sdk/event/core");
 
+const { setImmediate, setTimeout } = require("sdk/timers");
+const { defer } = require("sdk/core/promise");
+
+/**
+ * Utility function that returns a promise once the specified event's `type`
+ * is emitted on the given `target`, or the delay specified is passed.
+ *
+ * @param {Object|Number} [target]
+ *    The delay to wait, or the object that receives the event.
+ *    If not given, the function returns a promise that will be resolved
+ *    as soon as possible.
+ * @param {String} [type]
+ *    A string representing the event type to waiting for.
+ * @param {Boolean} [capture]
+ *    If `true`, `capture` indicates that the user wishes to initiate capture.
+ *
+ * @returns {Promise}
+ *    A promise resolved once the delay given is passed, or the object
+ *    receives the event specified
+ */
+const wait = (target, type, capture) => {
+  let { promise, resolve, reject } = defer();
+
+  if (!arguments.length) {
+    setImmediate(resolve);
+  }
+  else if (typeof(target) === "number") {
+    setTimeout(resolve, target);
+  }
+  else if (typeof(target.once) === "function") {
+    target.once(type, resolve);
+  }
+  else if (typeof(target.addEventListener) === "function") {
+    target.addEventListener(type, function listener(...args) {
+      this.removeEventListener(type, listener, capture);
+      resolve(...args);
+    }, capture);
+  }
+  else if (typeof(target) === "object" && target !== null) {
+    once(target, type, resolve);
+  }
+  else {
+    reject('Invalid target given.');
+  }
+
+  return promise;
+};
+exports.wait = wait;
+
 function scenario(setup) {
   return function(unit) {
     return function(assert) {
       let actual = [];
       let input = {};
       unit(input, function(output, events, expected, message) {
         let result = setup(output, expected, actual);
 
@@ -51,9 +100,9 @@ exports.ignoreNew = scenario(function(ou
 exports.FIFO = scenario(function(target, expected, actual) {
   on(target, "data", function($) actual.push($ + "#1"));
   on(target, "data", function($) actual.push($ + "#2"));
   on(target, "data", function($) actual.push($ + "#3"));
 
   return expected.reduce(function(result, value) {
     return result.concat(value + "#1", value + "#2", value + "#3");
   }, []);
-});
+});
\ No newline at end of file
--- a/addon-sdk/source/test/test-content-script.js
+++ b/addon-sdk/source/test/test-content-script.js
@@ -131,16 +131,17 @@ exports["test Create Proxy Test With Eve
   worker.port.on("foo", function () {
     assert.pass("You can use events");
     // And terminate your test with helper.done:
     helper.done();
   });
 
 });
 
+/* Disabled due to bug 1038432
 // Bug 714778: There was some issue around `toString` functions
 //             that ended up being shared between content scripts
 exports["test Shared To String Proxies"] = createProxyTest("", function(helper) {
 
   let worker = helper.createWorker(
     'new ' + function ContentScriptScope() {
       // We ensure that `toString` can't be modified so that nothing could
       // leak to/from the document and between content scripts
@@ -160,17 +161,17 @@ exports["test Shared To String Proxies"]
                "document.location.toString is different for each content script");
         assert(document.location.toString() == "data:text/html;charset=utf-8,",
                "Second document.location.toString()");
         done();
       }
     );
   });
 });
-
+*/
 
 // Ensure that postMessage is working correctly across documents with an iframe
 let html = '<iframe id="iframe" name="test" src="data:text/html;charset=utf-8," />';
 exports["test postMessage"] = createProxyTest(html, function (helper, assert) {
   let ifWindow = helper.xrayWindow.document.getElementById("iframe").contentWindow;
   // Listen without proxies, to check that it will work in regular case
   // simulate listening from a web document.
   ifWindow.addEventListener("message", function listener(event) {
deleted file mode 100644
--- a/addon-sdk/source/test/test-packaging.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/* 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/. */
- "use strict";
-
-var options = require("@loader/options");
-
-exports.testPackaging = function(assert) {
-  assert.equal(options.metadata.description,
-                   "Add-on development made easy.",
-                   "packaging metadata should be available");
-  try {
-    options.metadata.description = 'new description';
-    assert.fail('should not have been able to set options.metadata property');
-  }
-  catch (e) {}
-
-  assert.equal(options.metadata.description,
-                   "Add-on development made easy.",
-                   "packaging metadata should be frozen");
-
-  assert.equal(options.metadata.permissions['private-browsing'], undefined,
-                   "private browsing metadata should be undefined");
-  assert.equal(options.metadata['private-browsing'], undefined,
-                   "private browsing metadata should be be frozen");
-  assert.equal(options['private-browsing'], undefined,
-                   "private browsing metadata should be be frozen");
-
-  try {
-    options.metadata['private-browsing'] = true;
-    assert.fail('should not have been able to set options.metadata property');
-  }
-  catch(e) {}
-  assert.equal(options.metadata['private-browsing'], undefined,
-                   "private browsing metadata should be be frozen");
-
-  try {
-    options.metadata.permissions['private-browsing'] = true;
-    assert.fail('should not have been able to set options.metadata.permissions property');
-  }
-  catch (e) {}
-  assert.equal(options.metadata.permissions['private-browsing'], undefined,
-                   "private browsing metadata should be be frozen");
-};
-
-require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-panel.js
+++ b/addon-sdk/source/test/test-panel.js
@@ -7,26 +7,28 @@ module.metadata = {
   'engines': {
     'Firefox': '*'
   }
 };
 
 const { Cc, Ci } = require("chrome");
 const { Loader } = require('sdk/test/loader');
 const { LoaderWithHookedConsole } = require("sdk/test/loader");
-const timer = require("sdk/timers");
+const { setTimeout } = require("sdk/timers");
 const self = require('sdk/self');
 const { open, close, focus, ready } = require('sdk/window/helpers');
 const { isPrivate } = require('sdk/private-browsing');
 const { isWindowPBSupported, isGlobalPBSupported } = require('sdk/private-browsing/utils');
 const { defer, all } = require('sdk/core/promise');
 const { getMostRecentBrowserWindow } = require('sdk/window/utils');
 const { getWindow } = require('sdk/panel/window');
 const { pb } = require('./private-browsing/helper');
 const { URL } = require('sdk/url');
+const { wait } = require('./event/helpers');
+
 const fixtures = require('./fixtures')
 
 const SVG_URL = fixtures.url('mofo_logo.SVG');
 const CSS_URL = fixtures.url('css-include-file.css');
 
 const Isolate = fn => '(' + fn + ')()';
 
 function ignorePassingDOMNodeWarning(type, message) {
@@ -539,17 +541,17 @@ function makeEventOrderTest(options) {
 
     let panel = Panel({ contentURL: "about:buildconfig" });
 
     function expect(event, cb) {
       expectedEvents.push(event);
       panel.on(event, function() {
         assert.equal(event, expectedEvents.shift());
         if (cb)
-          timer.setTimeout(cb, 1);
+          setTimeout(cb, 1);
       });
       return {then: expect};
     }
 
     options.test(assert, done, expect, panel);
   }
 }
 
@@ -1051,16 +1053,146 @@ exports['test panel CSS list'] = functio
         loader.unload();
       }).then(done, assert.fail);
     }
   });
 
   panel.show();
 };
 
+exports['test panel contextmenu validation'] = function(assert) {
+  const loader = Loader(module);
+  const { Panel } = loader.require('sdk/panel');
+
+  let panel = Panel({});
+
+  assert.equal(panel.contextMenu, false,
+    'contextMenu option is `false` by default');
+
+  panel.destroy();
+
+  panel = Panel({
+    contextMenu: false
+  });
+
+  assert.equal(panel.contextMenu, false,
+    'contextMenu option is `false`');
+
+  panel.contextMenu = true;
+
+  assert.equal(panel.contextMenu, true,
+    'contextMenu option accepts boolean values');
+
+  panel.destroy();
+
+  panel = Panel({
+    contextMenu: true
+  });
+
+  assert.equal(panel.contextMenu, true,
+    'contextMenu option is `true`');
+
+  panel.contextMenu = false;
+
+  assert.equal(panel.contextMenu, false,
+    'contextMenu option accepts boolean values');
+  
+  assert.throws(() =>
+    Panel({contextMenu: 1}),
+    /The option "contextMenu" must be one of the following types: boolean, undefined, null/,
+    'contextMenu only accepts boolean or nil values');
+
+  panel = Panel();
+
+  assert.throws(() =>
+    panel.contextMenu = 1,
+    /The option "contextMenu" must be one of the following types: boolean, undefined, null/,
+    'contextMenu only accepts boolean or nil values');
+
+  loader.unload();
+}
+
+exports['test panel contextmenu enabled'] = function*(assert) {
+  const loader = Loader(module);
+  const { Panel } = loader.require('sdk/panel');
+  const { getActiveView } = loader.require('sdk/view/core');
+  const { getContentDocument } = loader.require('sdk/panel/utils');
+
+  let contextmenu = getMostRecentBrowserWindow().
+                      document.getElementById("contentAreaContextMenu");
+
+  let panel = Panel({contextMenu: true});
+
+  panel.show();
+
+  yield wait(panel, 'show');
+
+  let view = getActiveView(panel);
+  let window = getContentDocument(view).defaultView;
+
+  let { sendMouseEvent } = window.QueryInterface(Ci.nsIInterfaceRequestor).
+                                    getInterface(Ci.nsIDOMWindowUtils);
+
+  yield ready(window);
+
+  assert.equal(contextmenu.state, 'closed',
+    'contextmenu must be closed');
+
+  sendMouseEvent('contextmenu', 20, 20, 2, 1, 0);
+
+  yield wait(contextmenu, 'popupshown');
+
+  assert.equal(contextmenu.state, 'open',
+    'contextmenu is opened');
+
+  contextmenu.hidePopup();
+
+  loader.unload();
+}
+
+exports['test panel contextmenu disabled'] = function*(assert) {
+  const loader = Loader(module);
+  const { Panel } = loader.require('sdk/panel');
+  const { getActiveView } = loader.require('sdk/view/core');
+  const { getContentDocument } = loader.require('sdk/panel/utils');
+
+  let contextmenu = getMostRecentBrowserWindow().
+                      document.getElementById("contentAreaContextMenu");
+  let listener = () => assert.fail('popupshown should never be called');
+
+  let panel = Panel();
+
+  panel.show();
+
+  yield wait(panel, 'show');
+
+  let view = getActiveView(panel);
+  let window = getContentDocument(view).defaultView;
+
+  let { sendMouseEvent } = window.QueryInterface(Ci.nsIInterfaceRequestor).
+                                    getInterface(Ci.nsIDOMWindowUtils);
+
+  yield ready(window);
+
+  assert.equal(contextmenu.state, 'closed',
+    'contextmenu must be closed');
+  
+  sendMouseEvent('contextmenu', 20, 20, 2, 1, 0);
+
+  contextmenu.addEventListener('popupshown', listener);
+
+  yield wait(1000);
+
+  contextmenu.removeEventListener('popupshown', listener);
+
+  assert.equal(contextmenu.state, 'closed',
+    'contextmenu was never open');
+
+  loader.unload();
+}
 
 if (isWindowPBSupported) {
   exports.testGetWindow = function(assert, done) {
     let activeWindow = getMostRecentBrowserWindow();
     open(null, { features: {
       toolbar: true,
       chrome: true,
       private: true
--- a/addon-sdk/source/test/test-plain-text-console.js
+++ b/addon-sdk/source/test/test-plain-text-console.js
@@ -229,16 +229,18 @@ exports.testPlainTextConsoleBoundMethods
 
   restorePrefs();
 };
 
 exports.testConsoleInnerID = function(assert) {
   let Console = require("sdk/console/plain-text").PlainTextConsole;
   let { log, info, warn, error, debug, exception, trace } = new Console(function() {}, "test ID");
 
+  prefs.set(SDK_LOG_LEVEL_PREF, "all");
+
   let messages = [];
   function onMessage({ subject }) {
     let message = subject.wrappedJSObject;
     messages.push({ msg: message.arguments[0], type: message.level, innerID: message.innerID });
   }
 
   const system = require("sdk/system/events");
   system.on("console-api-log-event", onMessage);
@@ -248,16 +250,18 @@ exports.testConsoleInnerID = function(as
   error("Test error");
 
   assert.equal(messages.length, 3, "Should see 3 log events");
   assert.deepEqual(messages[0], { msg: "Test log", type: "log", innerID: "test ID" }, "Should see the right event");
   assert.deepEqual(messages[1], { msg: "Test warning", type: "warn", innerID: "test ID" }, "Should see the right event");
   assert.deepEqual(messages[2], { msg: "Test error", type: "error", innerID: "test ID" }, "Should see the right event");
 
   system.off("console-api-log-event", onMessage);
+
+  restorePrefs();
 };
 
 function restorePrefs() {
   if (HAS_ORIGINAL_ADDON_LOG_LEVEL)
     prefs.set(ADDON_LOG_LEVEL_PREF, ORIGINAL_ADDON_LOG_LEVEL);
   else
     prefs.reset(ADDON_LOG_LEVEL_PREF);
 
--- a/addon-sdk/source/test/test-require.js
+++ b/addon-sdk/source/test/test-require.js
@@ -46,18 +46,22 @@ function checkError (assert, name, e) {
   else {
     assert.equal(msg.indexOf('Error: you must provide a module name when calling require() from '), 0);
     assert.ok(msg.indexOf("test-require") !== -1, msg);
   }
 
   // we'd also like to assert that the right filename
   // and linenumber is in the stacktrace
   let tb = traceback.fromException(e);
-  // Get the second to last frame, as the last frame is inside
-  // toolkit/loader
-  let lastFrame = tb[tb.length-2];
+
+  // The last frame may be inside a loader
+  let lastFrame = tb[tb.length - 1];
+  if (lastFrame.fileName.indexOf("toolkit/loader.js") !== -1 ||
+      lastFrame.fileName.indexOf("sdk/loader/cuddlefish.js") !== -1)
+    lastFrame = tb[tb.length - 2];
+
   assert.ok(lastFrame.fileName.indexOf("test-require.js") !== -1,
                           'Filename found in stacktrace');
   assert.equal(lastFrame.lineNumber, REQUIRE_LINE_NO,
                           'stacktrace has correct line number');
 }
 
 require('test').run(exports);
--- a/addon-sdk/source/test/test-self.js
+++ b/addon-sdk/source/test/test-self.js
@@ -35,37 +35,16 @@ exports.testSelf = function(assert) {
                                                             : "startup";
   assert.equal(self.loadReason, testLoadReason,
                "self.loadReason is either startup or install on test runs");
 
   assert.equal(self.isPrivateBrowsingSupported, false,
                'usePrivateBrowsing property is false by default');
 };
 
-exports.testSelfID = function(assert, done) {
-  var self = require("sdk/self");
-  // We can't assert anything about the ID inside the unit test right now,
-  // because the ID we get depends upon how the test was invoked. The idea
-  // is that it is supposed to come from the main top-level package's
-  // package.json file, from the "id" key.
-  assert.equal(typeof(self.id), "string", "self.id is a string");
-  assert.ok(self.id.length > 0);
-
-  AddonManager.getAddonByID(self.id, function(addon) {
-    if (!addon) {
-      assert.fail("did not find addon with self.id");
-    }
-    else {
-      assert.pass("found addon with self.id");
-    }
-
-    done();
-  });
-}
-
 exports.testSelfHandlesLackingLoaderOptions = function (assert) {
   let root = module.uri.substr(0, module.uri.lastIndexOf('/'));
   let uri = root + '/fixtures/loader/self/';
   let sdkPath = loaderOptions.paths[''] + 'sdk';
   let loader = Loader({ paths: { '': uri, 'sdk': sdkPath }});
   let program = main(loader, 'main');
   let self = program.self;
   assert.pass("No errors thrown when including sdk/self without loader options");
--- a/content/media/webrtc/MediaEngineTabVideoSource.cpp
+++ b/content/media/webrtc/MediaEngineTabVideoSource.cpp
@@ -17,50 +17,44 @@
 #include "ImageContainer.h"
 #include "Layers.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsIDOMDocument.h"
 #include "nsITabSource.h"
 #include "VideoUtils.h"
 #include "nsServiceManagerUtils.h"
 #include "nsIPrefService.h"
+#include "MediaTrackConstraints.h"
 
 namespace mozilla {
 
 using namespace mozilla::gfx;
+using dom::ConstrainLongRange;
 
 NS_IMPL_ISUPPORTS(MediaEngineTabVideoSource, nsIDOMEventListener, nsITimerCallback)
 
 MediaEngineTabVideoSource::MediaEngineTabVideoSource()
 : mMonitor("MediaEngineTabVideoSource")
 {
 }
 
 nsresult
 MediaEngineTabVideoSource::StartRunnable::Run()
 {
   mVideoSource->Draw();
-  nsCOMPtr<nsPIDOMWindow> privateDOMWindow = do_QueryInterface(mVideoSource->mWindow);
-  if (privateDOMWindow) {
-    privateDOMWindow->GetChromeEventHandler()->AddEventListener(NS_LITERAL_STRING("MozAfterPaint"), mVideoSource, false);
-  } else {
-    mVideoSource->mTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
-    mVideoSource->mTimer->InitWithCallback(mVideoSource, mVideoSource->mTimePerFrame, nsITimer:: TYPE_REPEATING_SLACK);
-  }
+  mVideoSource->mTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+  mVideoSource->mTimer->InitWithCallback(mVideoSource, mVideoSource->mTimePerFrame, nsITimer:: TYPE_REPEATING_SLACK);
   mVideoSource->mTabSource->NotifyStreamStart(mVideoSource->mWindow);
   return NS_OK;
 }
 
 nsresult
 MediaEngineTabVideoSource::StopRunnable::Run()
 {
   nsCOMPtr<nsPIDOMWindow> privateDOMWindow = do_QueryInterface(mVideoSource->mWindow);
-  if (privateDOMWindow && privateDOMWindow->GetChromeEventHandler()) {
-    privateDOMWindow->GetChromeEventHandler()->RemoveEventListener(NS_LITERAL_STRING("MozAfterPaint"), mVideoSource, false);
-  }
 
   if (mVideoSource->mTimer) {
     mVideoSource->mTimer->Cancel();
     mVideoSource->mTimer = nullptr;
   }
   mVideoSource->mTabSource->NotifyStreamStop(mVideoSource->mWindow);
   return NS_OK;
 }
@@ -71,31 +65,24 @@ MediaEngineTabVideoSource::HandleEvent(n
   return NS_OK;
 }
 
 NS_IMETHODIMP
 MediaEngineTabVideoSource::Notify(nsITimer*) {
   Draw();
   return NS_OK;
 }
+#define LOGTAG "TabVideo"
 
 nsresult
 MediaEngineTabVideoSource::InitRunnable::Run()
 {
-  nsresult rv;
-  nsCOMPtr<nsIPrefService> prefs = do_GetService("@mozilla.org/preferences-service;1", &rv);
-  NS_ENSURE_SUCCESS(rv, rv);
-  nsCOMPtr<nsIPrefBranch> branch = do_QueryInterface(prefs);
-  if (!branch)
-    return NS_OK;
-  branch->GetIntPref("media.tabstreaming.width", &mVideoSource->mBufW);
-  branch->GetIntPref("media.tabstreaming.height", &mVideoSource->mBufH);
-  branch->GetIntPref("media.tabstreaming.time_per_frame", &mVideoSource->mTimePerFrame);
   mVideoSource->mData = (unsigned char*)malloc(mVideoSource->mBufW * mVideoSource->mBufH * 4);
 
+  nsresult rv;
   mVideoSource->mTabSource = do_GetService(NS_TABSOURCESERVICE_CONTRACTID, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsIDOMWindow> win;
   rv = mVideoSource->mTabSource->GetTabToStream(getter_AddRefs(win));
   NS_ENSURE_SUCCESS(rv, rv);
   if (!win)
     return NS_OK;
@@ -114,19 +101,51 @@ MediaEngineTabVideoSource::GetName(nsASt
 
 void
 MediaEngineTabVideoSource::GetUUID(nsAString_internal& aUuid)
 {
   aUuid.AssignLiteral(MOZ_UTF16("uuid"));
 }
 
 nsresult
-MediaEngineTabVideoSource::Allocate(const VideoTrackConstraintsN&,
-                                    const MediaEnginePrefs&)
+MediaEngineTabVideoSource::Allocate(const VideoTrackConstraintsN& aConstraints,
+                                    const MediaEnginePrefs& aPrefs)
 {
+
+  ConstrainLongRange cWidth(aConstraints.mRequired.mWidth);
+  ConstrainLongRange cHeight(aConstraints.mRequired.mHeight);
+
+  if (aConstraints.mAdvanced.WasPassed()) {
+    const auto& advanced = aConstraints.mAdvanced.Value();
+    for (uint32_t i = 0; i < advanced.Length(); i++) {
+      if (cWidth.mMax >= advanced[i].mWidth.mMin && cWidth.mMin <= advanced[i].mWidth.mMax &&
+	  cHeight.mMax >= advanced[i].mHeight.mMin && cHeight.mMin <= advanced[i].mHeight.mMax) {
+	cWidth.mMin = std::max(cWidth.mMin, advanced[i].mWidth.mMin);
+	cHeight.mMin = std::max(cHeight.mMin, advanced[i].mHeight.mMin);
+      }
+    }
+  }
+
+  mBufW = aPrefs.GetWidth(false);
+  mBufH = aPrefs.GetHeight(false);
+
+  if (cWidth.mMin > mBufW) {
+    mBufW = cWidth.mMin;
+  } else if (cWidth.mMax < mBufW) {
+    mBufW = cWidth.mMax;
+  }
+
+  if (cHeight.mMin > mBufH) {
+    mBufH = cHeight.mMin;
+  } else if (cHeight.mMax < mBufH) {
+    mBufH = cHeight.mMax;
+  }
+
+  mTimePerFrame = aPrefs.mFPS ? 1000 / aPrefs.mFPS : aPrefs.mFPS;
+
   return NS_OK;
 }
 
 nsresult
 MediaEngineTabVideoSource::Deallocate()
 {
   return NS_OK;
 }
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -385,16 +385,20 @@
         <provider android:name="org.mozilla.gecko.db.HomeProvider"
                   android:authorities="@ANDROID_PACKAGE_NAME@.db.home"
                   android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
 
         <provider android:name="org.mozilla.gecko.db.ReadingListProvider"
                   android:authorities="@ANDROID_PACKAGE_NAME@.db.readinglist"
                   android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
 
+        <provider android:name="org.mozilla.gecko.db.SearchHistoryProvider"
+                  android:authorities="@ANDROID_PACKAGE_NAME@.db.searchhistory"
+                  android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
+
         <service
             android:exported="false"
             android:name="org.mozilla.gecko.updater.UpdateService"
             android:process="@MANGLED_ANDROID_PACKAGE_NAME@.UpdateService">
         </service>
 
         <service
             android:exported="false"
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -20,16 +20,17 @@ import org.json.JSONObject;
 
 import org.mozilla.gecko.DynamicToolbar.PinReason;
 import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
+import org.mozilla.gecko.db.BrowserContract.SearchHistory;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.SuggestedSites;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.LoadFaviconTask;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
@@ -524,16 +525,17 @@ public class BrowserApp extends GeckoApp
 
         mFindInPageBar = (FindInPageBar) findViewById(R.id.find_in_page);
         mMediaCastingBar = (MediaCastingBar) findViewById(R.id.media_casting);
 
         EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener)this,
             "Menu:Update",
             "Reader:Added",
             "Reader:FaviconRequest",
+            "Search:Keyword",
             "Prompt:ShowTop",
             "Accounts:Exist");
 
         EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener)this,
             "Accounts:Create",
             "CharEncoding:Data",
             "CharEncoding:State",
             "Feedback:LastUrl",
@@ -931,16 +933,17 @@ public class BrowserApp extends GeckoApp
             mBrowserHealthReporter.uninit();
             mBrowserHealthReporter = null;
         }
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener)this,
             "Menu:Update",
             "Reader:Added",
             "Reader:FaviconRequest",
+            "Search:Keyword",
             "Prompt:ShowTop",
             "Accounts:Exist");
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener)this,
             "Accounts:Create",
             "CharEncoding:Data",
             "CharEncoding:State",
             "Feedback:LastUrl",
@@ -1378,16 +1381,18 @@ public class BrowserApp extends GeckoApp
                 }
 
             } else if (event.equals("Reader:Added")) {
                 final int result = message.getInt("result");
                 handleReaderAdded(result, messageToReadingListContentValues(message));
             } else if (event.equals("Reader:FaviconRequest")) {
                 final String url = message.getString("url");
                 handleReaderFaviconRequest(url);
+            } else if (event.equals("Search:Keyword")) {
+                storeSearchQuery(message.getString("query"));
             } else if (event.equals("Prompt:ShowTop")) {
                 // Bring this activity to front so the prompt is visible..
                 Intent bringToFrontIntent = new Intent();
                 bringToFrontIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.BROWSER_INTENT_CLASS_NAME);
                 bringToFrontIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
                 startActivity(bringToFrontIntent);
             } else if (event.equals("Accounts:Exist")) {
                 final String kind = message.getString("kind");
@@ -1801,16 +1806,37 @@ public class BrowserApp extends GeckoApp
             message.put("location", where);
             message.put("identifier", identifier);
             EventDispatcher.getInstance().dispatchEvent(message, null);
         } catch (Exception e) {
             Log.w(LOGTAG, "Error recording search.", e);
         }
     }
 
+    /**
+     * Store search query in SearchHistoryProvider.
+     *
+     * @param query
+     *        a search query to store. We won't store empty queries.
+     */
+    private void storeSearchQuery(String query) {
+        if (TextUtils.isEmpty(query)) {
+            return;
+        }
+
+        final ContentValues values = new ContentValues();
+        values.put(SearchHistory.QUERY, query);
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                getContentResolver().insert(SearchHistory.CONTENT_URI, values);
+            }
+        });
+    }
+
     void filterEditingMode(String searchTerm, AutocompleteHandler handler) {
         if (TextUtils.isEmpty(searchTerm)) {
             hideBrowserSearch();
         } else {
             showBrowserSearch();
             mBrowserSearch.filter(searchTerm, handler);
         }
     }
@@ -2844,16 +2870,17 @@ public class BrowserApp extends GeckoApp
             openUrlAndStopEditing(url);
         }
     }
 
     // BrowserSearch.OnSearchListener
     @Override
     public void onSearch(SearchEngine engine, String text) {
         recordSearch(engine, "barsuggest");
+        storeSearchQuery(text);
         openUrlAndStopEditing(text, engine.name);
     }
 
     // BrowserSearch.OnEditSuggestionListener
     @Override
     public void onEditSuggestion(String suggestion) {
         mBrowserToolbar.onEditSuggestion(suggestion);
     }
--- a/mobile/android/base/resources/layout/remote_tabs_setup_panel.xml
+++ b/mobile/android/base/resources/layout/remote_tabs_setup_panel.xml
@@ -1,17 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android">
 
     <LinearLayout android:id="@+id/remote_tabs_setup_containing_layout"
-                  style="@style/TabsPanelFrame"
+                  style="@style/TabsPanelFrame.RemoteTabs"
                   android:layout_width="match_parent"
                   android:layout_height="match_parent">
 
         <LinearLayout style="@style/TabsPanelSection"
                       android:layout_width="match_parent"
                       android:layout_height="wrap_content">
 
            <TextView style="@style/TabsPanelItem.TextAppearance.Header"
--- a/mobile/android/base/resources/layout/remote_tabs_verification_panel.xml
+++ b/mobile/android/base/resources/layout/remote_tabs_verification_panel.xml
@@ -1,17 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android">
 
     <LinearLayout android:id="@+id/remote_tabs_verification_containing_layout"
-                  style="@style/TabsPanelFrame"
+                  style="@style/TabsPanelFrame.RemoteTabs"
                   android:layout_width="match_parent"
                   android:layout_height="match_parent">
 
         <LinearLayout style="@style/TabsPanelSection"
                       android:layout_width="match_parent"
                       android:layout_height="wrap_content">
 
             <TextView style="@style/TabsPanelItem.TextAppearance.Header.FXAccounts"
--- a/mobile/android/base/resources/values-large-land-v11/styles.xml
+++ b/mobile/android/base/resources/values-large-land-v11/styles.xml
@@ -59,18 +59,24 @@
         <item name="android:paddingLeft">0dp</item>
         <item name="android:paddingRight">0dp</item>
     </style>
 
     <style name="TabsPanelFrame.PrivateTabs">
         <!-- We set values in tablet portrait. -->
     </style>
 
+    <style name="TabsPanelFrame.RemoteTabs">
+        <item name="android:paddingLeft">0dp</item>
+        <item name="android:paddingRight">0dp</item>
+    </style>
+
     <style name="TabsPanelSection" parent="TabsPanelSectionBase">
-        <!-- To override the values-land style. -->
+        <item name="android:layout_marginLeft">20dp</item>
+        <item name="android:layout_marginRight">20dp</item>
     </style>
 
     <style name="TabsPanelSection.PrivateTabs">
         <!-- We set values in tablet portrait. -->
     </style>
 
     <style name="TabsPanelSection.PrivateTabs.Header">
         <!-- We set values in tablet portrait. -->
@@ -81,18 +87,16 @@
         <!-- To override the values-land style. -->
     </style>
 
     <style name="TabsPanelItem.Button" parent="TabsPanelItem.ButtonBase">
         <item name="android:paddingTop">12dp</item>
         <item name="android:paddingBottom">12dp</item>
         <item name="android:paddingLeft">6dp</item>
         <item name="android:paddingRight">6dp</item>
-        <item name="android:layout_marginLeft">12dp</item>
-        <item name="android:layout_marginRight">12dp</item>
         <item name="android:textSize">16sp</item>
     </style>
 
     <style name="TabsPanelItem.TextAppearance.Header.FXAccounts">
         <item name="android:visibility">gone</item>
     </style>
 
     <style name="TabsPanelItem.TextAppearance.Header.PrivateTabs">
--- a/mobile/android/base/resources/values-large-v11/styles.xml
+++ b/mobile/android/base/resources/values-large-v11/styles.xml
@@ -90,16 +90,21 @@
     </style>
 
     <style name="Widget.HomeBanner">
         <item name="android:paddingLeft">32dp</item>
         <item name="android:paddingRight">32dp</item>
     </style>
 
     <!-- Tabs panel -->
+    <style name="TabsPanelFrame.RemoteTabs" parent="TabsPanelFrameBase">
+        <item name="android:paddingLeft">112dp</item>
+        <item name="android:paddingRight">112dp</item>
+    </style>
+
     <style name="TabsPanelFrame.PrivateTabs">
         <item name="android:orientation">horizontal</item>
         <item name="android:paddingTop">0dp</item>
     </style>
 
     <style name="TabsPanelSection.PrivateTabs">
         <item name="android:layout_weight">1</item>
     </style>
@@ -113,24 +118,9 @@
     </style>
 
     <style name="TabsPanelItem.TextAppearance.Linkified.LearnMore">
         <item name="android:layout_height">match_parent</item>
         <item name="android:gravity">center</item>
         <item name="android:layout_gravity">center</item>
     </style>
 
-    <!-- Tabs panel -->
-    <style name="TabsPanelFrame" parent="TabsPanelFrameBase">
-        <item name="android:paddingLeft">84dp</item>
-        <item name="android:paddingRight">84dp</item>
-    </style>
-
-    <style name="TabsPanelItem.Button" parent="TabsPanelItem.ButtonBase">
-        <item name="android:paddingTop">18dp</item>
-        <item name="android:paddingBottom">18dp</item>
-        <item name="android:paddingLeft">9dp</item>
-        <item name="android:paddingRight">9dp</item>
-        <item name="android:layout_marginLeft">90dp</item>
-        <item name="android:layout_marginRight">90dp</item>
-    </style>
-
 </resources>
--- a/mobile/android/base/resources/values-xlarge-land-v11/styles.xml
+++ b/mobile/android/base/resources/values-xlarge-land-v11/styles.xml
@@ -24,19 +24,22 @@
     <style name="Widget.Home.HistoryTabWidget">
         <item name="android:showDividers">beginning|middle|end</item>
         <item name="android:dividerPadding">0dp</item>
         <item name="android:paddingLeft">100dp</item>
         <item name="android:paddingTop">30dp</item>
     </style>
 
     <!-- Tabs panel -->
+    <style name="TabsPanelFrame.RemoteTabs" parent="TabsPanelFrameBase">
+        <item name="android:paddingLeft">0dp</item>
+        <item name="android:paddingRight">0dp</item>
+    </style>
+
     <style name="TabsPanelItem.Button" parent="TabsPanelItem.ButtonBase">
         <item name="android:paddingTop">12dp</item>
         <item name="android:paddingBottom">12dp</item>
         <item name="android:paddingLeft">6dp</item>
         <item name="android:paddingRight">6dp</item>
-        <item name="android:layout_marginLeft">12dp</item>
-        <item name="android:layout_marginRight">12dp</item>
         <item name="android:textSize">16dp</item>
     </style>
 
 </resources>
--- a/mobile/android/base/resources/values-xlarge-v11/styles.xml
+++ b/mobile/android/base/resources/values-xlarge-v11/styles.xml
@@ -26,18 +26,14 @@
 
     <style name="Widget.Home.HistoryTabWidget">
         <item name="android:showDividers">beginning|middle|end</item>
         <item name="android:dividerPadding">0dp</item>
         <item name="android:paddingTop">30dp</item>
     </style>
 
     <!-- Tabs panel -->
-    <style name="TabsPanelItem.Button" parent="TabsPanelItem.ButtonBase">
-        <item name="android:paddingTop">18dp</item>
-        <item name="android:paddingBottom">18dp</item>
-        <item name="android:paddingLeft">9dp</item>
-        <item name="android:paddingRight">9dp</item>
-        <item name="android:layout_marginLeft">180dp</item>
-        <item name="android:layout_marginRight">180dp</item>
+    <style name="TabsPanelFrame.RemoteTabs" parent="TabsPanelFrameBase">
+        <item name="android:paddingLeft">212dp</item>
+        <item name="android:paddingRight">212dp</item>
     </style>
 
 </resources>
--- a/mobile/android/base/toolbar/PageActionLayout.java
+++ b/mobile/android/base/toolbar/PageActionLayout.java
@@ -10,40 +10,45 @@ import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSObject;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.widget.GeckoPopupMenu;
+
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.drawable.Drawable;
+import android.os.Bundle;
 import android.util.AttributeSet;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 
+import java.util.Iterator;
+import java.util.List;
 import java.util.UUID;
 import java.util.ArrayList;
 
 public class PageActionLayout extends LinearLayout implements NativeEventListener,
                                                               View.OnClickListener,
                                                               View.OnLongClickListener {
     private static final String MENU_BUTTON_KEY = "MENU_BUTTON_KEY";
     private static final int DEFAULT_PAGE_ACTIONS_SHOWN = 2;
 
-    private ArrayList<PageAction> mPageActionList;
+    private final Context mContext;
+    private final LinearLayout mLayout;
+    private final List<PageAction> mPageActionList;
+
     private GeckoPopupMenu mPageActionsMenu;
-    private Context mContext;
-    private LinearLayout mLayout;
 
     // By default it's two, can be changed by calling setNumberShown(int)
     private int mMaxVisiblePageActions;
 
     public PageActionLayout(Context context, AttributeSet attrs) {
         super(context, attrs);
         mContext = context;
         mLayout = this;
@@ -53,91 +58,117 @@ public class PageActionLayout extends Li
         refreshPageActionIcons();
 
         EventDispatcher.getInstance().registerGeckoThreadListener(this,
             "PageActions:Add",
             "PageActions:Remove");
     }
 
     private void setNumberShown(int count) {
+        ThreadUtils.assertOnUiThread();
+
         mMaxVisiblePageActions = count;
 
-        for(int index = 0; index < count; index++) {
-            if ((this.getChildCount() - 1) < index) {
+        for (int index = 0; index < count; index++) {
+            if ((getChildCount() - 1) < index) {
                 mLayout.addView(createImageButton());
             }
         }
     }
 
     public void onDestroy() {
         EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
             "PageActions:Add",
             "PageActions:Remove");
     }
 
     @Override
-    public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
+    public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) {
+        // NativeJSObject cannot be used off of the Gecko thread, so convert it to a Bundle.
+        final Bundle bundle = message.toBundle();
+
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                handleUiMessage(event, bundle);
+            }
+        });
+    }
+
+    private void handleUiMessage(final String event, final Bundle message) {
+        ThreadUtils.assertOnUiThread();
+
         if (event.equals("PageActions:Add")) {
             final String id = message.getString("id");
             final String title = message.getString("title");
-            final String imageURL = message.optString("icon", null);
-            final boolean mImportant = message.getBoolean("important");
+            final String imageURL = message.getString("icon");
+            final boolean important = message.getBoolean("important");
 
             addPageAction(id, title, imageURL, new OnPageActionClickListeners() {
                 @Override
                 public void onClick(String id) {
                     GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PageActions:Clicked", id));
                 }
 
                 @Override
                 public boolean onLongClick(String id) {
                     GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PageActions:LongClicked", id));
                     return true;
                 }
-            }, mImportant);
+            }, important);
         } else if (event.equals("PageActions:Remove")) {
             final String id = message.getString("id");
 
             removePageAction(id);
         }
     }
 
-    private void addPageAction(final String id, final String title, final String imageData, final OnPageActionClickListeners mOnPageActionClickListeners, boolean mImportant) {
-        final PageAction pageAction = new PageAction(id, title, null, mOnPageActionClickListeners, mImportant);
+    private void addPageAction(final String id, final String title, final String imageData,
+            final OnPageActionClickListeners onPageActionClickListeners, boolean important) {
+        ThreadUtils.assertOnUiThread();
+
+        final PageAction pageAction = new PageAction(id, title, null, onPageActionClickListeners, important);
 
         int insertAt = mPageActionList.size();
-        while(insertAt > 0 && mPageActionList.get(insertAt-1).isImportant()) {
-          insertAt--;
+        while (insertAt > 0 && mPageActionList.get(insertAt - 1).isImportant()) {
+            insertAt--;
         }
         mPageActionList.add(insertAt, pageAction);
 
         BitmapUtils.getDrawable(mContext, imageData, new BitmapUtils.BitmapLoader() {
             @Override
             public void onBitmapFound(final Drawable d) {
                 if (mPageActionList.contains(pageAction)) {
                     pageAction.setDrawable(d);
                     refreshPageActionIcons();
                 }
             }
         });
     }
 
     private void removePageAction(String id) {
-        for(int i = 0; i < mPageActionList.size(); i++) {
-            if (mPageActionList.get(i).getID().equals(id)) {
-                mPageActionList.remove(i);
+        ThreadUtils.assertOnUiThread();
+
+        final Iterator<PageAction> iter = mPageActionList.iterator();
+        while (iter.hasNext()) {
+            final PageAction pageAction = iter.next();
+            if (pageAction.getID().equals(id)) {
+                iter.remove();
                 refreshPageActionIcons();
                 return;
             }
         }
     }
 
     private ImageButton createImageButton() {
+        ThreadUtils.assertOnUiThread();
+
+        final int width = mContext.getResources().getDimensionPixelSize(R.dimen.page_action_button_width);
         ImageButton imageButton = new ImageButton(mContext, null, R.style.UrlBar_ImageButton_Icon);
-        imageButton.setLayoutParams(new LayoutParams(mContext.getResources().getDimensionPixelSize(R.dimen.page_action_button_width), LayoutParams.MATCH_PARENT));
+        imageButton.setLayoutParams(new LayoutParams(width, LayoutParams.MATCH_PARENT));
         imageButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
         imageButton.setOnClickListener(this);
         imageButton.setOnLongClickListener(this);
         return imageButton;
     }
 
     @Override
     public void onClick(View v) {
@@ -158,122 +189,115 @@ public class PageActionLayout extends Li
             showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1);
             return true;
         } else {
             return getPageActionWithId(buttonClickedId).onLongClick();
         }
     }
 
     private void setActionForView(final ImageButton view, final PageAction pageAction) {
+        ThreadUtils.assertOnUiThread();
+
         if (pageAction == null) {
             view.setTag(null);
-            ThreadUtils.postToUiThread(new Runnable() {
-                @Override
-                public void run () {
-                    view.setImageDrawable(null);
-                    view.setVisibility(View.GONE);
-                    view.setContentDescription(null);
-                }
-            });
+            view.setImageDrawable(null);
+            view.setVisibility(View.GONE);
+            view.setContentDescription(null);
             return;
         }
 
         view.setTag(pageAction.getID());
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run () {
-                view.setImageDrawable(pageAction.getDrawable());
-                view.setVisibility(View.VISIBLE);
-                view.setContentDescription(pageAction.getTitle());
-            }
-        });
+        view.setImageDrawable(pageAction.getDrawable());
+        view.setVisibility(View.VISIBLE);
+        view.setContentDescription(pageAction.getTitle());
     }
 
     private void refreshPageActionIcons() {
+        ThreadUtils.assertOnUiThread();
+
         final Resources resources = mContext.getResources();
-        for(int index = 0; index < this.getChildCount(); index++) {
-            final ImageButton v = (ImageButton)this.getChildAt(index);
-            final PageAction pageAction = getPageActionForViewAt(index);
+        for (int i = 0; i < this.getChildCount(); i++) {
+            final ImageButton v = (ImageButton) this.getChildAt(i);
+            final PageAction pageAction = getPageActionForViewAt(i);
 
-            // If there are more pageactions then buttons, set the menu icon. Otherwise set the page action's icon if there is a page action.
-            if (index == (this.getChildCount() - 1) && mPageActionList.size() > mMaxVisiblePageActions) {
+            // If there are more page actions than buttons, set the menu icon.
+            // Otherwise, set the page action's icon if there is a page action.
+            if ((i == this.getChildCount() - 1) && (mPageActionList.size() > mMaxVisiblePageActions)) {
                 v.setTag(MENU_BUTTON_KEY);
-                ThreadUtils.postToUiThread(new Runnable() {
-                    @Override
-                    public void run () {
-                        v.setImageDrawable(resources.getDrawable(R.drawable.icon_pageaction));
-                        v.setVisibility((pageAction != null) ? View.VISIBLE : View.GONE);
-                        v.setContentDescription(resources.getString(R.string.page_action_dropmarker_description));
-                    }
-                });
+                v.setImageDrawable(resources.getDrawable(R.drawable.icon_pageaction));
+                v.setVisibility((pageAction != null) ? View.VISIBLE : View.GONE);
+                v.setContentDescription(resources.getString(R.string.page_action_dropmarker_description));
             } else {
                 setActionForView(v, pageAction);
             }
         }
     }
 
     private PageAction getPageActionForViewAt(int index) {
+        ThreadUtils.assertOnUiThread();
+
         /**
          * We show the user the most recent pageaction added since this keeps the user aware of any new page actions being added
          * Also, the order of the pageAction is important i.e. if a page action is added, instead of shifting the pagactions to the
          * left to make space for the new one, it would be more visually appealing to have the pageaction appear in the blank space.
          *
          * buttonIndex is needed for this reason because every new View added to PageActionLayout gets added to the right of its neighbouring View.
          * Hence the button on the very leftmost has the index 0. We want our pageactions to start from the rightmost
          * and hence we maintain the insertion order of the child Views which is essentially the reverse of their index
          */
 
-        int buttonIndex = (this.getChildCount() - 1) - index;
-        int totalVisibleButtons = ((mPageActionList.size() < this.getChildCount()) ? mPageActionList.size() : this.getChildCount());
+        final int buttonIndex = (this.getChildCount() - 1) - index;
 
         if (mPageActionList.size() > buttonIndex) {
             // Return the pageactions starting from the end of the list for the number of visible pageactions.
-            return mPageActionList.get((mPageActionList.size() - totalVisibleButtons) + buttonIndex);
+            final int buttonCount = Math.min(mPageActionList.size(), getChildCount());
+            return mPageActionList.get((mPageActionList.size() - buttonCount) + buttonIndex);
         }
         return null;
     }
 
     private PageAction getPageActionWithId(String id) {
-        for(int i = 0; i < mPageActionList.size(); i++) {
-            PageAction pageAction = mPageActionList.get(i);
+        ThreadUtils.assertOnUiThread();
+
+        for (PageAction pageAction : mPageActionList) {
             if (pageAction.getID().equals(id)) {
                 return pageAction;
             }
         }
         return null;
     }
 
-    private void showMenu(View mPageActionButton, int toShow) {
+    private void showMenu(View pageActionButton, int toShow) {
+        ThreadUtils.assertOnUiThread();
+
         if (mPageActionsMenu == null) {
-            mPageActionsMenu = new GeckoPopupMenu(mPageActionButton.getContext(), mPageActionButton);
+            mPageActionsMenu = new GeckoPopupMenu(pageActionButton.getContext(), pageActionButton);
             mPageActionsMenu.inflate(0);
             mPageActionsMenu.setOnMenuItemClickListener(new GeckoPopupMenu.OnMenuItemClickListener() {
                 @Override
                 public boolean onMenuItemClick(MenuItem item) {
                     int id = item.getItemId();
-                    for(int i = 0; i < mPageActionList.size(); i++) {
+                    for (int i = 0; i < mPageActionList.size(); i++) {
                         PageAction pageAction = mPageActionList.get(i);
                         if (pageAction.key() == id) {
                             pageAction.onClick();
                             return true;
                         }
                     }
                     return false;
                 }
             });
         }
         Menu menu = mPageActionsMenu.getMenu();
         menu.clear();
 
-        for(int i = 0; i < mPageActionList.size(); i++) {
-            if (i < toShow) {
-                PageAction pageAction = mPageActionList.get(i);
-                MenuItem item = menu.add(Menu.NONE, pageAction.key(), Menu.NONE, pageAction.getTitle());
-                item.setIcon(pageAction.getDrawable());
-            }
+        for (int i = 0; i < mPageActionList.size() && i < toShow; i++) {
+            PageAction pageAction = mPageActionList.get(i);
+            MenuItem item = menu.add(Menu.NONE, pageAction.key(), Menu.NONE, pageAction.getTitle());
+            item.setIcon(pageAction.getDrawable());
         }
         mPageActionsMenu.show();
     }
 
     private static interface OnPageActionClickListeners {
         public void onClick(String id);
         public boolean onLongClick(String id);
     }
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -1590,16 +1590,17 @@ var BrowserApp = {
         // perform a keyword search on the selected tab.
         this.selectedTab.userSearch = aData;
 
         let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
         sendMessageToJava({
           type: "Search:Keyword",
           identifier: engine.identifier,
           name: engine.name,
+          query: aData
         });
         break;
 
       case "Browser:Quit":
         this.quit();
         break;
 
       case "SaveAs:PDF":
--- a/mobile/android/config/proguard.cfg
+++ b/mobile/android/config/proguard.cfg
@@ -14,16 +14,18 @@
 -keep public class * extends android.app.Service
 -keep public class * extends android.app.backup.BackupAgentHelper
 -keep public class * extends android.content.BroadcastReceiver
 -keep public class * extends android.content.ContentProvider
 -keep public class * extends android.preference.Preference
 -keep public class * extends org.mozilla.gecko.sync.syncadapter.SyncAdapter
 -keep class org.mozilla.gecko.sync.syncadapter.SyncAdapter
 
+-keep public class * extends android.support.v4.app.Fragment
+
 # Preserve all native method names and the names of their classes.
 -keepclasseswithmembernames class * {
     native <methods>;
 }
 
 -keepclasseswithmembers class * {
     public <init>(android.content.Context, android.util.AttributeSet, int);
 }
--- a/mobile/android/search/java/org/mozilla/search/Constants.java
+++ b/mobile/android/search/java/org/mozilla/search/Constants.java
@@ -16,9 +16,12 @@ package org.mozilla.search;
  */
 public class Constants {
 
     public static final String POSTSEARCH_FRAGMENT = "org.mozilla.search.POSTSEARCH_FRAGMENT";
     public static final String PRESEARCH_FRAGMENT = "org.mozilla.search.PRESEARCH_FRAGMENT";
     public static final String SEARCH_FRAGMENT = "org.mozilla.search.SEARCH_FRAGMENT";
 
     public static final String AUTOCOMPLETE_ROW_LIMIT = "5";
+
+    public static final String YAHOO_WEB_SEARCH_BASE_URL = "https://search.yahoo.com/search?p=";
+    public static final String YAHOO_WEB_SEARCH_RESULTS_FILTER = "//search.yahoo.com";
 }
--- a/mobile/android/search/java/org/mozilla/search/MainActivity.java
+++ b/mobile/android/search/java/org/mozilla/search/MainActivity.java
@@ -34,33 +34,33 @@ public class MainActivity extends Fragme
         super.onCreate(stateBundle);
         setContentView(R.layout.search_activity_main);
         startPresearch();
     }
 
     @Override
     public void onSearch(String s) {
         startPostsearch();
-        ((PostSearchFragment) getSupportFragmentManager().findFragmentById(R.id.gecko))
-                .setUrl("https://search.yahoo.com/search?p=" + Uri.encode(s));
+        ((PostSearchFragment) getSupportFragmentManager().findFragmentById(R.id.postsearch))
+                .startSearch(s);
     }
 
     private void startPresearch() {
         if (state != State.PRESEARCH) {
             state = State.PRESEARCH;
-            findViewById(R.id.gecko).setVisibility(View.INVISIBLE);
+            findViewById(R.id.postsearch).setVisibility(View.INVISIBLE);
             findViewById(R.id.presearch).setVisibility(View.VISIBLE);
         }
     }
 
     private void startPostsearch() {
         if (state != State.POSTSEARCH) {
             state = State.POSTSEARCH;
             findViewById(R.id.presearch).setVisibility(View.INVISIBLE);
-            findViewById(R.id.gecko).setVisibility(View.VISIBLE);
+            findViewById(R.id.postsearch).setVisibility(View.VISIBLE);
         }
     }
 
     @Override
     public void onBackPressed() {
         if (state == State.POSTSEARCH) {
             startPresearch();
         } else {
--- a/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java
+++ b/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java
@@ -1,100 +1,69 @@
 /* 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.search;
 
 import android.content.Intent;
+import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Bundle;
 import android.support.v4.app.Fragment;
-import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-
-import org.mozilla.gecko.GeckoView;
-import org.mozilla.gecko.GeckoViewChrome;
-import org.mozilla.gecko.GeckoViewContent;
-import org.mozilla.gecko.PrefsHelper;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
 
 public class PostSearchFragment extends Fragment {
 
     private static final String LOGTAG = "PostSearchFragment";
-    private GeckoView geckoView;
+    private WebView webview;
+
+    public PostSearchFragment() {}
 
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
                              Bundle savedInstanceState) {
         View mainView = inflater.inflate(R.layout.search_activity_detail, container, false);
 
 
-        geckoView = (GeckoView) mainView.findViewById(R.id.gecko_view);
-
-        geckoView.setChromeDelegate(new MyGeckoViewChrome());
-        geckoView.setContentDelegate(new SearchGeckoView());
-
-        PrefsHelper.setPref("privacy.clearOnShutdown.cache", true);
-        PrefsHelper.setPref("privacy.clearOnShutdown.cookies", true);
-
-
-        if (null == geckoView.getCurrentBrowser()) {
-            // This pageload allows Fennec to be loaded in a background fragment.
-            // Without supplying a URL, it doesn't look like Fennec will get loaded?
-            geckoView.addBrowser("https://search.yahoo.com/search?p=firefox%20android");
-
-        }
-
+        webview = (WebView) mainView.findViewById(R.id.webview);
+        webview.setWebViewClient(new WebViewClient(){
+            @Override
+            public void onPageStarted(WebView view, String url, Bitmap favicon) {
+                if (isSearchResultsPage(url)) {
+                    super.onPageStarted(view, url, favicon);
+                } else {
+                    webview.stopLoading();
+                    Intent i = new Intent(Intent.ACTION_VIEW);
+                    i.setData(Uri.parse(url));
+                    startActivity(i);
+                }
+            }
+        });
         return mainView;
     }
 
-
-    public void setUrl(String url) {
-        if (null == geckoView.getCurrentBrowser()) {
-            geckoView.addBrowser(url);
-        } else {
-            geckoView.getCurrentBrowser().loadUrl(url);
-        }
-    }
-
-
-    private static class MyGeckoViewChrome extends GeckoViewChrome {
-        @Override
-        public void onReady(GeckoView view) {
-            Log.i(LOGTAG, "Gecko is ready");
-
-            PrefsHelper.setPref("devtools.debugger.remote-enabled", true);
-
-            // The Gecko libraries have finished loading and we can use the rendering engine.
-            // Let's add a browser (required) and load a page into it.
-        }
-
+    /**
+     * Test if a given URL is a page of search results.
+     * <p>
+     * Search results pages will be shown in the embedded view.  Other pages are
+     * opened in external browsers.
+     *
+     * @param url to test.
+     * @return true if <code>url</code> is a page of search results.
+     */
+    protected boolean isSearchResultsPage(String url) {
+        return url.contains(Constants.YAHOO_WEB_SEARCH_RESULTS_FILTER);
     }
 
-
-    private class SearchGeckoView extends GeckoViewContent {
-
-        @Override
-        public void onPageStart(GeckoView geckoView, GeckoView.Browser browser, String s) {
-            Log.i("OnPageStart", s);
-            // Only load this page if it's the Yahoo search page that we're using.
-            // TODO: Make this check more robust, and allow for other search providers.
-            if (s.contains("//search.yahoo.com")) {
-                super.onPageStart(geckoView, browser, s);
-
+    public void startSearch(String query) {
+        setUrl(Constants.YAHOO_WEB_SEARCH_BASE_URL + Uri.encode(query));
+    }
 
-            } else {
-                browser.stop();
-                Intent i = new Intent(Intent.ACTION_VIEW);
-                i.setData(Uri.parse(s));
-                startActivity(i);
-            }
-        }
-
-        @Override
-        public void onPageShow(GeckoView geckoView, GeckoView.Browser browser) {
-
-        }
+    public void setUrl(String url) {
+        webview.loadUrl(url);
     }
 }
--- a/mobile/android/search/res/layout/search_activity_detail.xml
+++ b/mobile/android/search/res/layout/search_activity_detail.xml
@@ -1,20 +1,16 @@
 <!-- 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/. -->
 
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                 xmlns:tools="http://schemas.android.com/tools"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
-                android:paddingBottom="@dimen/activity_vertical_margin"
-                android:paddingLeft="@dimen/activity_horizontal_margin"
-                android:paddingRight="@dimen/activity_horizontal_margin"
-                android:paddingTop="@dimen/activity_vertical_margin"
                 tools:context="org.mozilla.search.PostSearchFragment">
 
-    <org.mozilla.gecko.GeckoView
-        android:id="@+id/gecko_view"
-        android:layout_width="fill_parent"
-        android:layout_height="fill_parent"/>
 
+    <WebView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:id="@+id/webview"/>
 </RelativeLayout>
--- a/mobile/android/search/res/layout/search_activity_main.xml
+++ b/mobile/android/search/res/layout/search_activity_main.xml
@@ -9,17 +9,17 @@
     android:layout_height="match_parent"
     android:orientation="vertical"
     tools:context=".MainActivity">
 
     <fragment
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:name="org.mozilla.search.PostSearchFragment"
-        android:id="@+id/gecko"
+        android:id="@+id/postsearch"
         />
 
     <fragment
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:name="org.mozilla.search.PreSearchFragment"
         android:id="@+id/presearch"
         />
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -352,20 +352,16 @@ pref("media.peerconnection.capture_delay
 pref("media.getusermedia.playout_delay", 50);
 #endif
 #else
 #ifdef ANDROID
 pref("media.navigator.enabled", true);
 #endif
 #endif
 
-pref("media.tabstreaming.width", 320);
-pref("media.tabstreaming.height", 240);
-pref("media.tabstreaming.time_per_frame", 40);
-
 // TextTrack support
 pref("media.webvtt.enabled", true);
 pref("media.webvtt.regions.enabled", false);
 
 // AudioTrack and VideoTrack support
 pref("media.track.enabled", false);
 
 // Whether to enable MediaSource support