Merge fx-team to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Tue, 02 Sep 2014 19:28:14 -0700
changeset 203126 e58842c764dd10913e29f54e3cecd7654499a830
parent 203112 0b63b2b7733f241f6b12b745b989337375ee3caf (current diff)
parent 203125 2dd9b2f2ef8fa99d631a4897027e0cf33f0e15e4 (diff)
child 203127 5e9826980be5999a66f6c3f8c9d2a95762fa6788
child 203134 fc096d338bc411a2084f4e1b429d3738ca5a395f
child 203221 53ab51cc4eb0a8b475ff1e8e1db8e59688c8b97e
child 203328 413f70cd2040ae387a0a50fdd4f996b1cdd230e4
push id27421
push userkwierso@gmail.com
push dateWed, 03 Sep 2014 02:33:37 +0000
treeherdermozilla-central@e58842c764dd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone35.0a1
first release with
nightly linux32
e58842c764dd / 35.0a1 / 20140903030206 / files
nightly linux64
e58842c764dd / 35.0a1 / 20140903030206 / files
nightly mac
e58842c764dd / 35.0a1 / 20140903030206 / files
nightly win32
e58842c764dd / 35.0a1 / 20140903030206 / files
nightly win64
e58842c764dd / 35.0a1 / 20140903030206 / 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
browser/themes/linux/social/chat-icons.png
browser/themes/osx/social/chat-icons.png
browser/themes/osx/social/chat-icons@2x.png
browser/themes/windows/social/chat-icons.png
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1577,18 +1577,22 @@ pref("shumway.disabled", true);
 // The maximum amount of decoded image data we'll willingly keep around (we
 // might keep around more than this, but we'll try to get down to this value).
 // (This is intentionally on the high side; see bug 746055.)
 pref("image.mem.max_decoded_image_kb", 256000);
 
 // Enable by default development builds up until early beta
 #ifdef EARLY_BETA_OR_EARLIER
 pref("loop.enabled", true);
+pref("loop.throttled", false);
 #else
-pref("loop.enabled", false);
+pref("loop.enabled", true);
+pref("loop.throttled", true);
+pref("loop.soft_start_ticket_number", -1);
+pref("loop.soft_start_hostname", "soft-start.loop-dev.stage.mozaws.net");
 #endif
 
 pref("loop.server", "https://loop.services.mozilla.com");
 pref("loop.seenToS", "unseen");
 pref("loop.legal.ToS_url", "https://accounts.firefox.com/legal/terms");
 pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/");
 pref("loop.do_not_disturb", false);
 pref("loop.ringtone", "chrome://browser/content/loop/shared/sounds/Firefox-Long.ogg");
--- a/browser/base/content/browser-loop.js
+++ b/browser/base/content/browser-loop.js
@@ -44,16 +44,22 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       if (!Services.prefs.getBoolPref("loop.enabled")) {
         this.toolbarButton.node.hidden = true;
         return;
       }
 
       // Add observer notifications before the service is initialized
       Services.obs.addObserver(this, "loop-status-changed", false);
 
+      // If we're throttled, check to see if it's our turn to be unthrottled
+      if (Services.prefs.getBoolPref("loop.throttled")) {
+        this.toolbarButton.node.hidden = true;
+        MozLoopService.checkSoftStart(this.toolbarButton.node);
+        return;
+      }
 
       MozLoopService.initialize();
       this.updateToolbarState();
     },
 
     uninit: function() {
       Services.obs.removeObserver(this, "loop-status-changed");
     },
--- a/browser/base/content/socialchat.xml
+++ b/browser/base/content/socialchat.xml
@@ -132,17 +132,19 @@
         PopupNotifications._reshowNotifications(this.content.popupnotificationanchor,
                                                 this.content);
         ]]></body>
       </method>
 
       <method name="swapDocShells">
         <parameter name="aTarget"/>
         <body><![CDATA[
-          aTarget.setAttribute('label', this.contentDocument.title);
+          aTarget.setAttribute("label", this.contentDocument.title);
+          if (this.getAttribute("dark") == "true")
+            aTarget.setAttribute("dark", "true");
           aTarget.src = this.src;
           aTarget.content.setAttribute("origin", this.content.getAttribute("origin"));
           aTarget.content.popupnotificationanchor.className = this.content.popupnotificationanchor.className;
           this.content.swapDocShells(aTarget.content);
         ]]></body>
       </method>
 
       <method name="onTitlebarClick">
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -5,16 +5,21 @@
 "use strict";
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 // Invalid auth token as per
 // https://github.com/mozilla-services/loop-server/blob/45787d34108e2f0d87d74d4ddf4ff0dbab23501c/loop/errno.json#L6
 const INVALID_AUTH_TOKEN = 110;
 
+// Ticket numbers are 24 bits in length.
+// The highest valid ticket number is 16777214 (2^24 - 2), so that a "now
+// serving" number of 2^24 - 1 is greater than it.
+const MAX_SOFT_START_TICKET_NUMBER = 16777214;
+
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/osfile.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm");
 
 this.EXPORTED_SYMBOLS = ["MozLoopService"];
@@ -44,16 +49,21 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
                                   "resource:///modules/loop/MozLoopPushHandler.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
+XPCOMUtils.defineLazyServiceGetter(this, "gDNSService",
+                                   "@mozilla.org/network/dns-service;1",
+                                   "nsIDNSService");
+
+
 // The current deferred for the registration process. This is set if in progress
 // or the registration was successful. This is null if a registration attempt was
 // unsuccessful.
 let gRegisteredDeferred = null;
 let gPushHandler = null;
 let gHawkClient = null;
 let gRegisteredLoopServer = false;
 let gLocalizedStrings =  null;
@@ -449,16 +459,18 @@ let MozLoopServiceInternal = {
       // in about:blank and then get lost.
       // Sadly we can't use chatbox.promiseChatLoaded() as promise chaining
       // involves event loop spins, which means it might be too late.
       // Have we already done it?
       if (chatbox.contentWindow.navigator.mozLoop) {
         return;
       }
 
+      chatbox.setAttribute("dark", true);
+
       chatbox.addEventListener("DOMContentLoaded", function loaded(event) {
         if (event.target != chatbox.contentDocument) {
           return;
         }
         chatbox.removeEventListener("DOMContentLoaded", loaded, true);
 
         let window = chatbox.contentWindow;
         injectLoopAPI(window);
@@ -627,51 +639,157 @@ this.MozLoopService = {
 
   resetFxA: function() {
     gFxAOAuthClientPromise = null;
     gFxAOAuthClient = null;
     gFxAOAuthTokenData = null;
   },
 #endif
 
+  _DNSService: gDNSService,
+
   set initializeTimerFunc(value) {
     gInitializeTimerFunc = value;
   },
 
   /**
    * Initialized the loop service, and starts registration with the
    * push and loop servers.
    */
   initialize: function() {
     // Don't do anything if loop is not enabled.
-    if (!Services.prefs.getBoolPref("loop.enabled")) {
+    if (!Services.prefs.getBoolPref("loop.enabled") ||
+        Services.prefs.getBoolPref("loop.throttled")) {
       return;
     }
 
     // If expiresTime is in the future then kick-off registration.
     if (MozLoopServiceInternal.urlExpiryTimeIsInFuture()) {
       gInitializeTimerFunc();
     }
   },
 
   /**
+   * If we're operating the service in "soft start" mode, and this browser
+   * isn't already activated, check whether it's time for it to become active.
+   * If so, activate the loop service.
+   *
+   * @param {Object} buttonNode DOM node representing the Loop button -- if we
+   *                            change from inactive to active, we need this
+   *                            in order to unhide the Loop button.
+   * @param {Function} doneCb   [optional] Callback that is called when the
+   *                            check has completed.
+   */
+  checkSoftStart(buttonNode, doneCb) {
+    if (!Services.prefs.getBoolPref("loop.throttled")) {
+      if (typeof(doneCb) == "function") {
+        doneCb(new Error("Throttling is not active"));
+      }
+      return;
+    }
+
+    if (Services.io.offline) {
+      if (typeof(doneCb) == "function") {
+        doneCb(new Error("Cannot check soft-start value: browser is offline"));
+      }
+      return;
+    }
+
+    let ticket = Services.prefs.getIntPref("loop.soft_start_ticket_number");
+    if (!ticket || ticket > MAX_SOFT_START_TICKET_NUMBER || ticket < 0) {
+      // Ticket value isn't valid (probably isn't set up yet) -- pick a random
+      // number from 1 to MAX_SOFT_START_TICKET_NUMBER, inclusive, and write it
+      // into prefs.
+      ticket = Math.floor(Math.random() * MAX_SOFT_START_TICKET_NUMBER) + 1;
+      // Floating point numbers can be imprecise, so we need to deal with
+      // the case that Math.random() effectively rounds to 1.0
+      if (ticket > MAX_SOFT_START_TICKET_NUMBER) {
+        ticket = MAX_SOFT_START_TICKET_NUMBER;
+      }
+      Services.prefs.setIntPref("loop.soft_start_ticket_number", ticket);
+    }
+
+    let onLookupComplete = (request, record, status) => {
+      // We don't bother checking errors -- if the DNS query fails,
+      // we just don't activate this time around. We'll check again on
+      // next startup.
+      if (!Components.isSuccessCode(status)) {
+        if (typeof(doneCb) == "function") {
+          doneCb(new Error("Error in DNS Lookup: " + status));
+        }
+        return;
+      }
+
+      let address = record.getNextAddrAsString().split(".");
+      if (address.length != 4) {
+        if (typeof(doneCb) == "function") {
+          doneCb(new Error("Invalid IP address"));
+        }
+        return;
+      }
+
+      if (address[0] != 127) {
+        if (typeof(doneCb) == "function") {
+          doneCb(new Error("Throttling IP address is not on localhost subnet"));
+        }
+        return
+      }
+
+      // Can't use bitwise operations here because JS treats all bitwise
+      // operations as 32-bit *signed* integers.
+      let now_serving = ((parseInt(address[1]) * 0x10000) +
+                         (parseInt(address[2]) * 0x100) +
+                         parseInt(address[3]));
+
+      if (now_serving > ticket) {
+        // Hot diggity! It's our turn! Activate the service.
+        console.log("MozLoopService: Activating Loop via soft-start");
+        Services.prefs.setBoolPref("loop.throttled", false);
+        buttonNode.hidden = false;
+        this.initialize();
+      }
+      if (typeof(doneCb) == "function") {
+        doneCb(null);
+      }
+    };
+
+    // We use DNS to propagate the slow-start value, since it has well-known
+    // scaling properties. Ideally, this would use something more semantic,
+    // like a TXT record; but we don't support TXT in our DNS resolution (see
+    // Bug 14328), so we instead treat the lowest 24 bits of the IP address
+    // corresponding to our "slow start DNS name" as a 24-bit integer. To
+    // ensure that these addresses aren't routable, the highest 8 bits must
+    // be "127" (reserved for localhost).
+    let host = Services.prefs.getCharPref("loop.soft_start_hostname");
+    let task = this._DNSService.asyncResolve(host,
+                                             this._DNSService.RESOLVE_DISABLE_IPV6,
+                                             onLookupComplete,
+                                             Services.tm.mainThread);
+  },
+
+
+  /**
    * Starts registration of Loop with the push server, and then will register
    * with the Loop server. It will return early if already registered.
    *
    * @param {Object} mockPushHandler Optional, test-only mock push handler. Used
    *                                 to allow mocking of the MozLoopPushHandler.
    * @returns {Promise} a promise that is resolved with no params on completion, or
    *          rejected with an error code or string.
    */
   register: function(mockPushHandler) {
     // Don't do anything if loop is not enabled.
     if (!Services.prefs.getBoolPref("loop.enabled")) {
       throw new Error("Loop is not enabled");
     }
 
+    if (Services.prefs.getBoolPref("loop.throttled")) {
+      throw new Error("Loop is disabled by the soft-start mechanism");
+    }
+
     return MozLoopServiceInternal.promiseRegisteredWithServers(mockPushHandler);
   },
 
   /**
    * Used to note a call url expiry time. If the time is later than the current
    * latest expiry time, then the stored expiry time is increased. For times
    * sooner, this function is a no-op; this ensures we always have the latest
    * expiry time for a url.
--- a/browser/components/loop/test/mochitest/browser.ini
+++ b/browser/components/loop/test/mochitest/browser.ini
@@ -7,11 +7,12 @@ support-files =
 [browser_CardDavImporter.js]
 [browser_fxa_login.js]
 skip-if = !debug
 [browser_loop_fxa_server.js]
 [browser_LoopContacts.js]
 [browser_mozLoop_appVersionInfo.js]
 [browser_mozLoop_prefs.js]
 [browser_mozLoop_doNotDisturb.js]
+[browser_mozLoop_softStart.js]
 skip-if = buildapp == 'mulet'
 [browser_toolbarbutton.js]
 [browser_mozLoop_pluralStrings.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_softStart.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const SOFT_START_HOSTNAME = "soft-start.example.invalid";
+
+let MockDNSService = {
+  RESOLVE_DISABLE_IPV6: 32,
+  nowServing: 0,
+  resultCode: 0,
+  ipFirstOctet: 127,
+
+  getNowServingAddress: function() {
+    let ip = this.ipFirstOctet + "." +
+             ((this.nowServing >>> 16) & 0xFF) + "." +
+             ((this.nowServing >>> 8) & 0xFF) + "." +
+             ((this.nowServing) & 0xFF);
+    info("Using 'now serving' of " + this.nowServing + " (" + ip + ")");
+    return ip;
+  },
+
+  asyncResolve: function(host, flags, callback) {
+    let mds = this;
+    Assert.equal(flags, this.RESOLVE_DISABLE_IPV6,
+                 "AAAA lookup should be disabled");
+    Assert.equal(host, SOFT_START_HOSTNAME,
+                 "Configured hostname should be used");
+    callback(null,
+             {getNextAddrAsString: mds.getNowServingAddress.bind(mds)},
+             this.resultCode);
+  }
+};
+
+// We need an unfrozen copy of the LoopService so we can monkeypatch it.
+let LoopService = {};
+for (var prop in MozLoopService) {
+  if (MozLoopService.hasOwnProperty(prop)) {
+    LoopService[prop] = MozLoopService[prop];
+  }
+}
+LoopService._DNSService = MockDNSService;
+
+let MockButton = {
+  hidden: true
+};
+
+let runCheck = function(expectError) {
+  return new Promise((resolve, reject) => {
+    LoopService.checkSoftStart(MockButton, error => {
+      if ((!!error) != (!!expectError)) {
+        reject(error);
+      } else {
+        resolve(error);
+      }
+    })
+  });
+}
+
+add_task(function* test_mozLoop_softStart() {
+  // Set associated variables to proper values
+  Services.prefs.setBoolPref("loop.throttled", true);
+  Services.prefs.setCharPref("loop.soft_start_hostname", SOFT_START_HOSTNAME);
+  Services.prefs.setIntPref("loop.soft_start_ticket_number", -1);
+
+  let throttled;
+  let ticket;
+
+  info("Ensure that we pick a valid ticket number.");
+  yield runCheck();
+  throttled = Services.prefs.getBoolPref("loop.throttled");
+  ticket = Services.prefs.getIntPref("loop.soft_start_ticket_number");
+  Assert.equal(MockButton.hidden, true, "Button should still be hidden");
+  Assert.equal(throttled, true, "Feature should still be throttled");
+  Assert.notEqual(ticket, -1, "Ticket should be changed");
+  Assert.ok((ticket < 16777214 && ticket > 0), "Ticket should be in range");
+
+  // Try some "interesting" ticket numbers
+  for (ticket of [1, 256, 65535, 10000000, 16777214]) {
+    MockButton.hidden = true;
+    Services.prefs.setBoolPref("loop.throttled", true);
+    Services.prefs.setBoolPref("loop.soft_start", true);
+    Services.prefs.setIntPref("loop.soft_start_ticket_number", ticket);
+
+    info("Ensure that we don't activate when the now serving " +
+         "number is less than our value.");
+    MockDNSService.nowServing = ticket - 1;
+    yield runCheck();
+    throttled = Services.prefs.getBoolPref("loop.throttled");
+    Assert.equal(MockButton.hidden, true, "Button should still be hidden");
+    Assert.equal(throttled, true, "Feature should still be throttled");
+
+    info("Ensure that we don't activate when the now serving " +
+         "number is equal to our value");
+    MockDNSService.nowServing = ticket;
+    yield runCheck();
+    throttled = Services.prefs.getBoolPref("loop.throttled");
+    Assert.equal(MockButton.hidden, true, "Button should still be hidden");
+    Assert.equal(throttled, true, "Feature should still be throttled");
+
+    info("Ensure that we *do* activate when the now serving " +
+         "number is greater than our value");
+    MockDNSService.nowServing = ticket + 1;
+    yield runCheck();
+    throttled = Services.prefs.getBoolPref("loop.throttled");
+    Assert.equal(MockButton.hidden, false, "Button should not be hidden");
+    Assert.equal(throttled, false, "Feature should be unthrottled");
+  }
+
+  info("Check DNS error behavior");
+  MockDNSService.nowServing = 0;
+  MockDNSService.resultCode = 0x80000000;
+  Services.prefs.setBoolPref("loop.throttled", true);
+  Services.prefs.setBoolPref("loop.soft_start", true);
+  MockButton.hidden = true;
+  yield runCheck(true);
+  throttled = Services.prefs.getBoolPref("loop.throttled");
+  Assert.equal(MockButton.hidden, true, "Button should be hidden");
+  Assert.equal(throttled, true, "Feature should be throttled");
+
+  info("Check DNS misconfiguration behavior");
+  MockDNSService.nowServing = ticket + 1;
+  MockDNSService.resultCode = 0;
+  MockDNSService.ipFirstOctet = 6;
+  Services.prefs.setBoolPref("loop.throttled", true);
+  Services.prefs.setBoolPref("loop.soft_start", true);
+  MockButton.hidden = true;
+  yield runCheck(true);
+  throttled = Services.prefs.getBoolPref("loop.throttled");
+  Assert.equal(MockButton.hidden, true, "Button should be hidden");
+  Assert.equal(throttled, true, "Feature should be throttled");
+});
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -151,17 +151,17 @@ browser.jar:
   skin/classic/browser/preferences/in-content/icons.png       (../shared/incontentprefs/icons.png)
   skin/classic/browser/preferences/in-content/icons@2x.png    (../shared/incontentprefs/icons@2x.png)
   skin/classic/browser/preferences/applications.css   (preferences/applications.css)
   skin/classic/browser/preferences/aboutPermissions.css (preferences/aboutPermissions.css)
   skin/classic/browser/social/services-16.png         (social/services-16.png)
   skin/classic/browser/social/services-64.png         (social/services-64.png)
   skin/classic/browser/social/share-button.png        (social/share-button.png)
   skin/classic/browser/social/share-button-active.png (social/share-button-active.png)
-  skin/classic/browser/social/chat-icons.png          (social/chat-icons.png)
+  skin/classic/browser/social/chat-icons.svg          (../shared/social/chat-icons.svg)
   skin/classic/browser/social/gear_default.png        (../shared/social/gear_default.png)
   skin/classic/browser/social/gear_clicked.png        (../shared/social/gear_clicked.png)
   skin/classic/browser/tabbrowser/alltabs.png         (tabbrowser/alltabs.png)
   skin/classic/browser/tabbrowser/alltabs-inverted.png (tabbrowser/alltabs-inverted.png)
   skin/classic/browser/tabbrowser/connecting.png      (tabbrowser/connecting.png)
   skin/classic/browser/tabbrowser/loading.png         (tabbrowser/loading.png)
   skin/classic/browser/tabbrowser/tab-active-middle.png     (tabbrowser/tab-active-middle.png)
   skin/classic/browser/tabbrowser/tab-arrow-left.png        (tabbrowser/tab-arrow-left.png)
deleted file mode 100644
index cc895f93c29f2b3601ddd1af3bf3a9a042eda15c..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -255,18 +255,17 @@ browser.jar:
   skin/classic/browser/preferences/in-content/icons.png       (../shared/incontentprefs/icons.png)
   skin/classic/browser/preferences/in-content/icons@2x.png    (../shared/incontentprefs/icons@2x.png)
   skin/classic/browser/preferences/applications.css         (preferences/applications.css)
   skin/classic/browser/preferences/aboutPermissions.css     (preferences/aboutPermissions.css)
   skin/classic/browser/social/services-16.png               (social/services-16.png)
   skin/classic/browser/social/services-16@2x.png            (social/services-16@2x.png)
   skin/classic/browser/social/services-64.png               (social/services-64.png)
   skin/classic/browser/social/services-64@2x.png            (social/services-64@2x.png)
-  skin/classic/browser/social/chat-icons.png                             (social/chat-icons.png)
-  skin/classic/browser/social/chat-icons@2x.png                          (social/chat-icons@2x.png)
+  skin/classic/browser/social/chat-icons.svg                             (../shared/social/chat-icons.svg)
   skin/classic/browser/social/gear_default.png                           (../shared/social/gear_default.png)
   skin/classic/browser/social/gear_clicked.png                           (../shared/social/gear_clicked.png)
   skin/classic/browser/tabbrowser/alltabs-box-bkgnd-icon.png             (tabbrowser/alltabs-box-bkgnd-icon.png)
   skin/classic/browser/tabbrowser/alltabs-box-bkgnd-icon-inverted.png    (tabbrowser/alltabs-box-bkgnd-icon-inverted.png)
   skin/classic/browser/tabbrowser/alltabs-box-bkgnd-icon-inverted@2x.png (tabbrowser/alltabs-box-bkgnd-icon-inverted@2x.png)
   skin/classic/browser/tabbrowser/newtab.png                             (tabbrowser/newtab.png)
   skin/classic/browser/tabbrowser/newtab@2x.png                          (tabbrowser/newtab@2x.png)
   skin/classic/browser/tabbrowser/newtab-inverted.png                    (tabbrowser/newtab-inverted.png)
deleted file mode 100644
index cc895f93c29f2b3601ddd1af3bf3a9a042eda15c..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 4afab888b14686c8856f651928f20bc6776f92aa..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/social/chat-icons.svg
@@ -0,0 +1,45 @@
+<?xml version="1.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/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+     xmlns:xlink="http://www.w3.org/1999/xlink"
+     x="0px" y="0px"
+     viewBox="-3 -3 16 16"
+     enable-background="new 0 0 16 16"
+     xml:space="preserve">
+<style>
+use:not(:target) {
+  display: none;
+}
+
+use {
+  fill: #c1c1c1;
+}
+
+use[id$="-active"] {
+  fill: #c1c1c1;
+}
+
+use[id$="-disabled"] {
+  fill: #c1c1c1;
+}
+</style>
+<defs>
+  <polygon id="close-shape" fill-rule="evenodd" clip-rule="evenodd" points="10,1.717 8.336,0.049 5.024,3.369 1.663,0 0,1.668
+    3.36,5.037 0.098,8.307 1.762,9.975 5.025,6.705 8.311,10 9.975,8.332 6.688,5.037"/>
+  <path id="dropdown-shape" fill-rule="evenodd" clip-rule="evenodd" d="M9,3L4.984,7L1,3H9z"/>
+  <polygon id="expand-shape" fill-rule="evenodd" clip-rule="evenodd" points="10,0 4.838,0 6.506,1.669 0,8.175 1.825,10 8.331,3.494
+    10,5.162"/>
+  <rect id="minimize-shape" y="3.6" fill-rule="evenodd" clip-rule="evenodd" width="10" height="2.8"/>
+</defs>
+<use id="close"               xlink:href="#close-shape"/>
+<use id="close-active"        xlink:href="#close-shape"/>
+<use id="close-disabled"      xlink:href="#close-shape"/>
+<use id="expand"              xlink:href="#expand-shape"/>
+<use id="expand-active"       xlink:href="#expand-shape"/>
+<use id="expand-disabled"     xlink:href="#expand-shape"/>
+<use id="minimize"            xlink:href="#minimize-shape"/>
+<use id="minimize-active"     xlink:href="#minimize-shape"/>
+<use id="minimize-disabled"   xlink:href="#minimize-shape"/>
+</svg>
--- a/browser/themes/shared/social/chat.inc.css
+++ b/browser/themes/shared/social/chat.inc.css
@@ -42,150 +42,80 @@
   max-height: 16px;
   max-width: 16px;
   padding: 0;
 }
 
 .chat-toolbarbutton {
   -moz-appearance: none;
   border: none;
-  padding: 0;
+  padding: 0 3px;
   margin: 0;
   background: none;
-  width: 16px;
+}
+
+.chat-toolbarbutton:hover {
+  background-color: rgba(255,255,255,.35);
+}
+
+.chat-toolbarbutton:hover:active {
+  background-color: rgba(255,255,255,.5);
 }
 
 .chat-toolbarbutton > .toolbarbutton-text {
   display: none;
 }
 
 .chat-toolbarbutton > .toolbarbutton-icon {
-  width: inherit;
+  width: 16px;
+  height: 16px;
 }
 
 .chat-close-button {
-  list-style-image: url('chrome://browser/skin/social/chat-icons.png');
-  -moz-image-region: rect(0, 16px, 16px, 0);
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#close");
 }
 
-.chat-close-button:hover {
-  -moz-image-region: rect(0, 32px, 16px, 16px);
-}
-
-.chat-close-button:hover:active {
-  -moz-image-region: rect(0, 48px, 16px, 32px);
+.chat-close-button:-moz-any(:hover,:hover:active) {
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#close-active");
 }
 
 .chat-minimize-button {
-  list-style-image: url('chrome://browser/skin/social/chat-icons.png');
-  -moz-image-region: rect(16px, 16px, 32px, 0);
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#minimize");
 }
 
-.chat-minimize-button:hover {
-  -moz-image-region: rect(16px, 32px, 32px, 16px);
-}
-
-.chat-minimize-button:hover:active {
-  -moz-image-region: rect(16px, 48px, 32px, 32px);
+.chat-minimize-button:-moz-any(:hover,:hover:active) {
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#minimize-active");
 }
 
 .chat-swap-button {
-  list-style-image: url('chrome://browser/skin/social/chat-icons.png');
-  -moz-image-region: rect(48px, 16px, 64px, 0);
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#expand");
+  transform: rotate(180deg);
 }
 
-.chat-swap-button:hover {
-  -moz-image-region: rect(48px, 32px, 64px, 16px);
-}
-
-.chat-swap-button:hover:active {
-  -moz-image-region: rect(48px, 48px, 64px, 32px);
+.chat-swap-button:-moz-any(:hover,:hover:active) {
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#expand-active");
 }
 
 chatbar > chatbox > .chat-titlebar > .chat-swap-button {
-  list-style-image: url('chrome://browser/skin/social/chat-icons.png');
-  -moz-image-region: rect(32px, 16px, 48px, 0);
-}
-
-chatbar > chatbox > .chat-titlebar > .chat-swap-button:hover {
-  -moz-image-region: rect(32px, 32px, 48px, 16px);
-}
-
-chatbar > chatbox > .chat-titlebar > .chat-swap-button:hover:active {
-  -moz-image-region: rect(32px, 48px, 48px, 32px);
-}
-
-@media (min-resolution: 2dppx) {
-  .chat-close-button {
-    list-style-image: url('chrome://browser/skin/social/chat-icons@2x.png');
-    -moz-image-region: rect(0, 32px, 32px, 0);
-  }
-
-  .chat-close-button:hover {
-    -moz-image-region: rect(0, 64px, 32px, 32px);
-  }
-
-  .chat-close-button:hover:active {
-    -moz-image-region: rect(0, 96px, 32px, 64px);
-  }
-
-  .chat-minimize-button {
-    list-style-image: url('chrome://browser/skin/social/chat-icons@2x.png');
-    -moz-image-region: rect(32px, 32px, 64px, 0);
-  }
-
-  .chat-minimize-button:hover {
-    -moz-image-region: rect(32px, 64px, 64px, 32px);
-  }
-
-  .chat-minimize-button:hover:active {
-    -moz-image-region: rect(32px, 96px, 64px, 64px);
-  }
-
-  .chat-swap-button {
-    list-style-image: url('chrome://browser/skin/social/chat-icons@2x.png');
-    -moz-image-region: rect(96px, 32px, 128px, 0);
-  }
-
-  .chat-swap-button:hover {
-    -moz-image-region: rect(96px, 64px, 128px, 32px);
-  }
-
-  .chat-swap-button:hover:active {
-    -moz-image-region: rect(96px, 96px, 128px, 64px);
-  }
-
-  chatbar > chatbox > .chat-titlebar > .chat-swap-button {
-    list-style-image: url('chrome://browser/skin/social/chat-icons@2x.png');
-    -moz-image-region: rect(64px, 32px, 96px, 0);
-  }
-
-  chatbar > chatbox > .chat-titlebar > .chat-swap-button:hover {
-    -moz-image-region: rect(64px, 64px, 96px, 32px);
-  }
-
-  chatbar > chatbox > .chat-titlebar > .chat-swap-button:hover:active {
-    -moz-image-region: rect(64px, 96px, 96px, 64px);
-  }
+  transform: none;
 }
 
 .chat-title {
   font-weight: bold;
   color: black;
   text-shadow: none;
   cursor: inherit;
 }
 
 .chat-titlebar {
-  height: 20px;
-  min-height: 20px;
+  height: 30px;
+  min-height: 30px;
   width: 100%;
   margin: 0;
-  padding: 2px;
-  -moz-padding-start: 6px;
+  padding: 7px 6px;
   border: none;
   border-bottom: 1px solid #ccc;
   cursor: pointer;
 }
 
 .chat-titlebar > .notification-anchor-icon {
   margin-left: 2px;
   margin-right: 2px;
@@ -197,16 +127,28 @@ chatbar > chatbox > .chat-titlebar > .ch
 
 .chat-titlebar[activity] {
   background-image: radial-gradient(ellipse closest-side at center, rgb(255,255,255), rgba(255,255,255,0));
   background-repeat: no-repeat;
   background-size: 100% 20px;
   background-position: 0 -10px;
 }
 
+chatbox[dark=true] > .chat-titlebar,
+chatbox[dark=true] > .chat-titlebar[selected] {
+  border-bottom: none;
+  background-color: #000;
+  background-image: none;
+}
+
+chatbox[dark=true] > .chat-titlebar > hbox > .chat-title {
+  font-weight: normal;
+  color: #c1c1c1;
+}
+
 .chat-frame {
   padding: 0;
   margin: 0;
   overflow: hidden;
 }
 
 .chatbar-button {
   list-style-image: url("chrome://browser/skin/social/services-16.png");
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -177,17 +177,17 @@ browser.jar:
 *       skin/classic/browser/preferences/in-content/preferences.css  (preferences/in-content/preferences.css)
         skin/classic/browser/preferences/in-content/favicon.ico      (../shared/incontentprefs/favicon.ico)
         skin/classic/browser/preferences/in-content/icons.png        (../shared/incontentprefs/icons.png)
         skin/classic/browser/preferences/in-content/icons@2x.png     (../shared/incontentprefs/icons@2x.png)
         skin/classic/browser/preferences/applications.css            (preferences/applications.css)
         skin/classic/browser/preferences/aboutPermissions.css        (preferences/aboutPermissions.css)
         skin/classic/browser/social/services-16.png                  (social/services-16.png)
         skin/classic/browser/social/services-64.png                  (social/services-64.png)
-        skin/classic/browser/social/chat-icons.png                   (social/chat-icons.png)
+        skin/classic/browser/social/chat-icons.svg                   (../shared/social/chat-icons.svg)
         skin/classic/browser/social/gear_default.png                 (../shared/social/gear_default.png)
         skin/classic/browser/social/gear_clicked.png                 (../shared/social/gear_clicked.png)
         skin/classic/browser/tabbrowser/newtab.png                   (tabbrowser/newtab.png)
         skin/classic/browser/tabbrowser/newtab-inverted.png          (tabbrowser/newtab-inverted.png)
         skin/classic/browser/tabbrowser/connecting.png               (tabbrowser/connecting.png)
         skin/classic/browser/tabbrowser/loading.png                  (tabbrowser/loading.png)
         skin/classic/browser/tabbrowser/tab-active-middle.png        (tabbrowser/tab-active-middle.png)
         skin/classic/browser/tabbrowser/tab-active-middle@2x.png     (tabbrowser/tab-active-middle@2x.png)
@@ -596,17 +596,17 @@ browser.jar:
 *       skin/classic/aero/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css)
         skin/classic/aero/browser/preferences/in-content/favicon.ico      (../shared/incontentprefs/favicon.ico)
         skin/classic/aero/browser/preferences/in-content/icons.png       (../shared/incontentprefs/icons.png)
         skin/classic/aero/browser/preferences/in-content/icons@2x.png    (../shared/incontentprefs/icons@2x.png)
         skin/classic/aero/browser/preferences/applications.css       (preferences/applications.css)
         skin/classic/aero/browser/preferences/aboutPermissions.css   (preferences/aboutPermissions.css)
         skin/classic/aero/browser/social/services-16.png             (social/services-16.png)
         skin/classic/aero/browser/social/services-64.png             (social/services-64.png)
-        skin/classic/aero/browser/social/chat-icons.png              (social/chat-icons.png)
+        skin/classic/aero/browser/social/chat-icons.svg              (../shared/social/chat-icons.svg)
         skin/classic/aero/browser/social/gear_default.png            (../shared/social/gear_default.png)
         skin/classic/aero/browser/social/gear_clicked.png            (../shared/social/gear_clicked.png)
         skin/classic/aero/browser/tabbrowser/newtab.png              (tabbrowser/newtab.png)
         skin/classic/aero/browser/tabbrowser/newtab-inverted.png     (tabbrowser/newtab-inverted.png)
         skin/classic/aero/browser/tabbrowser/connecting.png          (tabbrowser/connecting.png)
         skin/classic/aero/browser/tabbrowser/loading.png             (tabbrowser/loading.png)
         skin/classic/aero/browser/tabbrowser/tab-active-middle.png   (tabbrowser/tab-active-middle.png)
         skin/classic/aero/browser/tabbrowser/tab-active-middle@2x.png (tabbrowser/tab-active-middle@2x.png)
deleted file mode 100644
index cc895f93c29f2b3601ddd1af3bf3a9a042eda15c..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/dom/events/EventStateManager.cpp
+++ b/dom/events/EventStateManager.cpp
@@ -2699,16 +2699,25 @@ EventStateManager::PostHandleEvent(nsPre
       if (mouseEvent->button == WidgetMouseEvent::eLeftButton &&
           !sNormalLMouseEventInProcess) {
         // We got a mouseup event while a mousedown event was being processed.
         // Make sure that the capturing content is cleared.
         nsIPresShell::SetCapturingContent(nullptr, 0);
         break;
       }
 
+      // For remote content, capture the event in the parent process at the
+      // <xul:browser remote> element. This will ensure that subsequent mousemove/mouseup
+      // events will continue to be dispatched to this element and therefore forwarded
+      // to the child.
+      if (dispatchedToContentProcess && !nsIPresShell::GetCapturingContent()) {
+        nsIContent* content = mCurrentTarget ? mCurrentTarget->GetContent() : nullptr;
+        nsIPresShell::SetCapturingContent(content, 0);
+      }
+
       nsCOMPtr<nsIContent> activeContent;
       if (nsEventStatus_eConsumeNoDefault != *aStatus) {
         nsCOMPtr<nsIContent> newFocus;      
         bool suppressBlur = false;
         if (mCurrentTarget) {
           mCurrentTarget->GetContentForEvent(aEvent, getter_AddRefs(newFocus));
           const nsStyleUserInterface* ui = mCurrentTarget->StyleUserInterface();
           activeContent = mCurrentTarget->GetContent();
--- a/mobile/android/base/MediaCastingBar.java
+++ b/mobile/android/base/MediaCastingBar.java
@@ -43,17 +43,17 @@ public class MediaCastingBar extends Rel
 
         mMediaPlay = (ImageButton) content.findViewById(R.id.media_play);
         mMediaPlay.setOnClickListener(this);
         mMediaPause = (ImageButton) content.findViewById(R.id.media_pause);
         mMediaPause.setOnClickListener(this);
         mMediaStop = (ImageButton) content.findViewById(R.id.media_stop);
         mMediaStop.setOnClickListener(this);
 
-        mCastingTo = (TextView) content.findViewById(R.id.media_casting_to);
+        mCastingTo = (TextView) content.findViewById(R.id.media_sending_to);
 
         // Capture clicks on the rest of the view to prevent them from
         // leaking into other views positioned below.
         content.setOnClickListener(this);
 
         mInflated = true;
     }
 
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -296,19 +296,19 @@ size. -->
 
 <!-- Localization note (find_text, find_prev, find_next, find_close) : These strings are used
      as alternate text for accessibility. They are not visible in the UI. -->
 <!ENTITY find_text "Find in Page">
 <!ENTITY find_prev "Previous">
 <!ENTITY find_next "Next">
 <!ENTITY find_close "Close">
 
-<!-- Localization note (media_casting_to, media_play, media_pause, media_stop) : These strings are used
+<!-- Localization note (media_sending_to, media_play, media_pause, media_stop) : These strings are used
      as alternate text for accessibility. They are not visible in the UI. -->
-<!ENTITY media_casting_to "Casting to Device">
+<!ENTITY media_sending_to "Sending to Device">
 <!ENTITY media_play "Play">
 <!ENTITY media_pause "Pause">
 <!ENTITY media_stop "Stop">
 
 <!ENTITY contextmenu_open_new_tab "Open in New Tab">
 <!ENTITY contextmenu_open_private_tab "Open in Private Tab">
 <!ENTITY contextmenu_remove "Remove">
 <!ENTITY contextmenu_add_to_launcher "Add to Home Screen">
--- a/mobile/android/base/resources/layout/media_casting.xml
+++ b/mobile/android/base/resources/layout/media_casting.xml
@@ -14,28 +14,28 @@
 
         <ImageButton android:id="@+id/media_pause"
                      style="@style/FindBar.ImageButton"
                      android:contentDescription="@string/media_pause"
                      android:src="@drawable/media_bar_pause"/>
 
     </RelativeLayout>
 
-    <TextView android:id="@+id/media_casting_to"
+    <TextView android:id="@+id/media_sending_to"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:layout_marginLeft="5dip"
               android:layout_marginRight="5dip"
               android:layout_alignParentLeft="true"
               android:layout_toLeftOf="@id/media_controls"
               android:layout_centerVertical="true"
               android:singleLine="true"
               android:ellipsize="end"
               android:textColor="#FFFFFFFF"
-              android:contentDescription="@string/media_casting_to"/>
+              android:contentDescription="@string/media_sending_to"/>
 
     <ImageButton android:id="@+id/media_stop"
                  style="@style/FindBar.ImageButton"
                  android:contentDescription="@string/media_stop"
                  android:layout_alignParentRight="true"
                  android:src="@drawable/media_bar_stop"/>
 
 </merge>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -100,17 +100,17 @@
   <string name="page">&page;</string>
   <string name="tools">&tools;</string>
 
   <string name="find_text">&find_text;</string>
   <string name="find_prev">&find_prev;</string>
   <string name="find_next">&find_next;</string>
   <string name="find_close">&find_close;</string>
 
-  <string name="media_casting_to">&media_casting_to;</string>
+  <string name="media_sending_to">&media_sending_to;</string>
   <string name="media_play">&media_play;</string>
   <string name="media_pause">&media_pause;</string>
   <string name="media_stop">&media_stop;</string>
 
   <string name="overlay_share_send_other">&overlay_share_send_other;</string>
   <string name="overlay_share_label">&overlay_share_label;</string>
   <string name="overlay_share_bookmark_btn_label">&overlay_share_bookmark_btn_label;</string>
   <string name="overlay_share_reading_list_btn_label">&overlay_share_reading_list_btn_label;</string>
--- a/mobile/android/base/tests/robocop_head.js
+++ b/mobile/android/base/tests/robocop_head.js
@@ -1162,17 +1162,17 @@ function JavaBridge(obj) {
 };
 
 JavaBridge.prototype = {
 
   _Services: Components.utils.import(
     "resource://gre/modules/Services.jsm", {}).Services,
 
   _sendMessageToJava: Components.utils.import(
-    "resource://gre/modules/Messaging.jsm", {}).sendMessageToJava,
+    "resource://gre/modules/Messaging.jsm", {}).Messaging.sendRequest,
 
   _sendMessage: function (innerType, args) {
     this._sendMessageToJava({
       type: this._EVENT_TYPE,
       innerType: innerType,
       method: args[0],
       args: Array.prototype.slice.call(args, 1),
     });
--- a/mobile/android/base/tests/roboextender/robocop_home_banner.html
+++ b/mobile/android/base/tests/roboextender/robocop_home_banner.html
@@ -16,30 +16,30 @@ function start() {
 }
 
 var messageId;
 
 function addMessage() {
   messageId = Home.banner.add({
     text: TEXT,
     onclick: function() {
-      sendMessageToJava({ type: "TestHomeBanner:MessageClicked" });
+      Messaging.sendRequest({ type: "TestHomeBanner:MessageClicked" });
     },
     onshown: function() {
-      sendMessageToJava({ type: "TestHomeBanner:MessageShown" });
+      Messaging.sendRequest({ type: "TestHomeBanner:MessageShown" });
     },
     ondismiss: function() {
-      sendMessageToJava({ type: "TestHomeBanner:MessageDismissed" });
+      Messaging.sendRequest({ type: "TestHomeBanner:MessageDismissed" });
     }
   });
-  sendMessageToJava({ type: "TestHomeBanner:MessageAdded" });
+  Messaging.sendRequest({ type: "TestHomeBanner:MessageAdded" });
 }
 
 function removeMessage() {
   Home.banner.remove(messageId);
-  sendMessageToJava({ type: "TestHomeBanner:MessageRemoved" });
+  Messaging.sendRequest({ type: "TestHomeBanner:MessageRemoved" });
 }
 
     </script>
   </head>
   <body onload="start();">
   </body>
 </html>
--- a/mobile/android/base/tests/roboextender/testSelectionHandler.html
+++ b/mobile/android/base/tests/roboextender/testSelectionHandler.html
@@ -208,17 +208,17 @@ function testCloseSelection() {
 }
 
 /* =================================================================================
  *
  * After finish of all selection tests, wrap up and go home.
  *
  */
 function finishTests() {
-  sendMessageToJava({
+  Messaging.sendRequest({
     type: "Robocop:testSelectionHandler",
     result: true,
     msg: "Done!",
     done: true
   });
 }
 
 /* ============================== Utility functions ======================
--- a/mobile/android/base/tests/testEventDispatcher.js
+++ b/mobile/android/base/tests/testEventDispatcher.js
@@ -23,17 +23,17 @@ function send_test_message(type) {
 
   // Make a copy
   let outerObject = JSON.parse(JSON.stringify(innerObject));
 
   outerObject.type = type;
   outerObject.object = innerObject;
   outerObject.objectArray = [null, innerObject];
 
-  sendMessageToJava(outerObject);
+  Messaging.sendRequest(outerObject);
 }
 
 function send_message_for_response(type, response) {
   sendMessageToJava({
     type: type,
     response: response,
   }, (success, error) => {
     if (response === "success") {
--- a/mobile/android/base/tests/testGeckoRequest.js
+++ b/mobile/android/base/tests/testGeckoRequest.js
@@ -3,38 +3,38 @@ Components.utils.import("resource://gre/
 let java = new JavaBridge(this);
 
 do_register_cleanup(() => {
   java.disconnect();
 });
 do_test_pending();
 
 function add_request_listener(message) {
-  RequestService.addListener(function (data) {
+  Messaging.addListener(function (data) {
     return { result: data + "bar" };
   }, message);
 }
 
 function add_exception_listener(message) {
-  RequestService.addListener(function (data) {
+  Messaging.addListener(function (data) {
     throw "error!";
   }, message);
 }
 
 function add_second_request_listener(message) {
   let exceptionCaught = false;
 
   try {
-    RequestService.addListener(() => {}, message);
+    Messaging.addListener(() => {}, message);
   } catch (e) {
     exceptionCaught = true;
   }
 
   do_check_true(exceptionCaught);
 }
 
 function remove_request_listener(message) {
-  RequestService.removeListener(message);
+  Messaging.removeListener(message);
 }
 
 function finish_test() {
   do_test_finished();
 }
--- a/mobile/android/base/util/NativeEventListener.java
+++ b/mobile/android/base/util/NativeEventListener.java
@@ -10,14 +10,14 @@ import org.mozilla.gecko.mozglue.Robocop
 @RobocopTarget
 public interface NativeEventListener {
     /**
      * Handles a message sent from Gecko.
      *
      * @param event    The name of the event being sent.
      * @param message  The message data.
      * @param callback The callback interface for this message. A callback is provided only if the
-     *                 originating sendMessageToJava call included a callback argument; otherwise,
+     *                 originating Messaging.sendRequest call included a callback argument; otherwise,
      *                 callback will be null. All listeners for a given event are given the same
      *                 callback object, and exactly one listener must handle the callback.
      */
     void handleMessage(String event, NativeJSObject message, EventCallback callback);
 }
--- a/mobile/android/chrome/content/CastingApps.js
+++ b/mobile/android/chrome/content/CastingApps.js
@@ -57,17 +57,17 @@ var CastingApps = {
     SimpleServiceDiscovery.registerTarget(rokuTarget);
     SimpleServiceDiscovery.registerTarget(fireflyTarget);
     SimpleServiceDiscovery.registerTarget(mediaPlayerTarget);
 
     // Search for devices continuously every 120 seconds
     SimpleServiceDiscovery.search(120 * 1000);
 
     this._castMenuId = NativeWindow.contextmenus.add(
-      Strings.browser.GetStringFromName("contextmenu.castToScreen"),
+      Strings.browser.GetStringFromName("contextmenu.sendToDevice"),
       this.filterCast,
       this.handleContextMenu.bind(this)
     );
 
     Services.obs.addObserver(this, "Casting:Play", false);
     Services.obs.addObserver(this, "Casting:Pause", false);
     Services.obs.addObserver(this, "Casting:Stop", false);
     Services.obs.addObserver(this, "Casting:Mirror", false);
@@ -424,24 +424,24 @@ var CastingApps = {
     }
 
     // We check for two state here:
     // 1. The video is actively being cast
     // 2. The video is allowed to be cast and is currently playing
     // Both states have the same action: Show the cast page action
     if (aVideo.mozIsCasting) {
       this.pageAction.id = PageActions.add({
-        title: Strings.browser.GetStringFromName("contextmenu.castToScreen"),
+        title: Strings.browser.GetStringFromName("contextmenu.sendToDevice"),
         icon: "drawable://casting_active",
         clickCallback: this.pageAction.click,
         important: true
       });
     } else if (aVideo.mozAllowCasting) {
       this.pageAction.id = PageActions.add({
-        title: Strings.browser.GetStringFromName("contextmenu.castToScreen"),
+        title: Strings.browser.GetStringFromName("contextmenu.sendToDevice"),
         icon: "drawable://casting",
         clickCallback: this.pageAction.click,
         important: true
       });
     }
   },
 
   prompt: function(aCallback, aFilterFunc) {
@@ -458,17 +458,17 @@ var CastingApps = {
       }
     });
 
     if (items.length == 0) {
       return;
     }
 
     let prompt = new Prompt({
-      title: Strings.browser.GetStringFromName("casting.prompt")
+      title: Strings.browser.GetStringFromName("casting.sendToDevice")
     }).setSingleChoiceItems(items).show(function(data) {
       let selected = data.button;
       let service = selected == -1 ? null : filteredServices[selected];
       if (aCallback)
         aCallback(service);
     });
   },
 
@@ -558,27 +558,27 @@ var CastingApps = {
 
   // RemoteMedia callback API methods
   onRemoteMediaStart: function(aRemoteMedia) {
     if (!this.session) {
       return;
     }
 
     aRemoteMedia.load(this.session.data);
-    sendMessageToJava({ type: "Casting:Started", device: this.session.service.friendlyName });
+    Messaging.sendRequest({ type: "Casting:Started", device: this.session.service.friendlyName });
 
     let video = this.session.videoRef.get();
     if (video) {
       this._sendEventToVideo(video, { active: true });
       this._updatePageAction(video);
     }
   },
 
   onRemoteMediaStop: function(aRemoteMedia) {
-    sendMessageToJava({ type: "Casting:Stopped" });
+    Messaging.sendRequest({ type: "Casting:Stopped" });
     this._shutdown();
   },
 
   onRemoteMediaStatus: function(aRemoteMedia) {
     if (!this.session) {
       return;
     }
 
--- a/mobile/android/chrome/content/PermissionsHelper.js
+++ b/mobile/android/chrome/content/PermissionsHelper.js
@@ -83,17 +83,17 @@ var PermissionsHelper = {
         this._currentPermissions = permissions;
 
         let host;
         try {
           host = uri.host;
         } catch(e) {
           host = uri.spec;
         }
-        sendMessageToJava({
+        Messaging.sendRequest({
           type: "Permissions:Data",
           host: host,
           permissions: permissions
         });
         break;
  
       case "Permissions:Clear":
         // An array of the indices of the permissions we want to clear
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ b/mobile/android/chrome/content/SelectionHandler.js
@@ -193,40 +193,40 @@ var SelectionHandler = {
         } else {
           Cu.reportError("Ignored \"TextSelection:Position\" message during invalid selection status");
         }
 
         break;
       }
 
       case "TextSelection:Get":
-        sendMessageToJava({
+        Messaging.sendRequest({
           type: "TextSelection:Data",
           requestId: aData,
           text: this._getSelectedText()
         });
         break;
     }
   },
 
   // Ignore selectionChange notifications during handle dragging, disable dynamic
   // IME text compositions (autoSuggest, autoCorrect, etc)
   _startDraggingHandles: function sh_startDraggingHandles() {
     if (!this._draggingHandles) {
       this._draggingHandles = true;
-      sendMessageToJava({ type: "TextSelection:DraggingHandle", dragging: true });
+      Messaging.sendRequest({ type: "TextSelection:DraggingHandle", dragging: true });
     }
   },
 
   // Act on selectionChange notifications when not dragging handles, allow dynamic
   // IME text compositions (autoSuggest, autoCorrect, etc)
   _stopDraggingHandles: function sh_stopDraggingHandles() {
     if (this._draggingHandles) {
       this._draggingHandles = false;
-      sendMessageToJava({ type: "TextSelection:DraggingHandle", dragging: false });
+      Messaging.sendRequest({ type: "TextSelection:DraggingHandle", dragging: false });
     }
   },
 
   handleEvent: function sh_handleEvent(aEvent) {
     switch (aEvent.type) {
       case "scroll":
         // Maintain position when top-level document is scrolled
         this._positionHandlesOnChange();
@@ -336,17 +336,17 @@ var SelectionHandler = {
                                                                       scroll.Y + aOptions.y,
                                                                       positions)) {
         this._closeSelection();
         return false;
     }
 
     // Determine position and show handles, open actionbar
     this._positionHandles(positions);
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "TextSelection:ShowHandles",
       handles: [this.HANDLE_TYPE_START, this.HANDLE_TYPE_END]
     });
     this._updateMenu();
     return true;
   },
 
   /*
@@ -541,17 +541,17 @@ var SelectionHandler = {
           order: this._getValue(action, "order", 0)
         };
         actions.push(a);
       }
     }
 
     actions.sort((a, b) => b.order - a.order);
 
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "TextSelection:Update",
       actions: actions
     });
   },
 
   /*
    * Actionbar methods.
    */
@@ -712,17 +712,17 @@ var SelectionHandler = {
     BrowserApp.deck.addEventListener("keyup", this, false);
     BrowserApp.deck.addEventListener("compositionupdate", this, false);
     BrowserApp.deck.addEventListener("compositionend", this, false);
 
     this._activeType = this.TYPE_CURSOR;
 
     // Determine position and show caret, open actionbar
     this._positionHandles();
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "TextSelection:ShowHandles",
       handles: [this.HANDLE_TYPE_MIDDLE]
     });
     this._updateMenu();
 
     return true;
   },
 
@@ -926,17 +926,17 @@ var SelectionHandler = {
       NativeWindow.toast.show(Strings.browser.GetStringFromName("selectionHelper.textCopied"), "short");
     }
     this._closeSelection();
   },
 
   shareSelection: function sh_shareSelection() {
     let selectedText = this._getSelectedText();
     if (selectedText.length) {
-      sendMessageToJava({
+      Messaging.sendRequest({
         type: "Share:Text",
         text: selectedText
       });
     }
     this._closeSelection();
   },
 
   searchSelection: function sh_searchSelection() {
@@ -995,17 +995,17 @@ var SelectionHandler = {
         selection.collapseToStart();
       }
     }
   },
 
   _deactivate: function sh_deactivate() {
     this._stopDraggingHandles();
     // Hide handle/caret, close actionbar
-    sendMessageToJava({ type: "TextSelection:HideHandles" });
+    Messaging.sendRequest({ type: "TextSelection:HideHandles" });
 
     this._removeObservers();
 
     // Only observed for caret positioning
     if (this._activeType == this.TYPE_CURSOR) {
       Services.obs.removeObserver(this, "TextSelection:UpdateCaretPos");
       BrowserApp.deck.removeEventListener("keyup", this);
       BrowserApp.deck.removeEventListener("compositionupdate", this);
@@ -1151,17 +1151,17 @@ var SelectionHandler = {
   // Position handles, allow for re-position, in case user drags handle
   // to invalid position, then releases, we can put it back where it started
   // positions is an array of objects with data about handle positions,
   // which we get from _getHandlePositions.
   _positionHandles: function sh_positionHandles(positions) {
     if (!positions) {
       positions = this._getHandlePositions(this._getScrollPos());
     }
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "TextSelection:PositionHandles",
       positions: positions,
       rtl: this._isRTL
     });
     this._prevHandlePositions = positions;
 
     // Text state transitions (text <--> no text) will affect selection context and actionbar display
     let currTargetElementHasText = (this._targetElement.textLength > 0);
--- a/mobile/android/chrome/content/ZoomHelper.js
+++ b/mobile/android/chrome/content/ZoomHelper.js
@@ -32,23 +32,23 @@ var ZoomHelper = {
 
     rect.type = "Browser:ZoomToRect";
     rect.x = Math.max(viewport.cssPageLeft, rect.x  - fudge + leftAdjustment);
     rect.y = Math.max(topPos, viewport.cssPageTop);
     rect.w = viewport.cssWidth;
     rect.h = viewport.cssHeight;
     rect.animate = false;
 
-    sendMessageToJava(rect);
+    Messaging.sendRequest(rect);
     BrowserApp.selectedTab._mReflozPoint = null;
   },
 
   zoomOut: function() {
     BrowserEventHandler.resetMaxLineBoxWidth();
-    sendMessageToJava({ type: "Browser:ZoomToPageWidth" });
+    Messaging.sendRequest({ type: "Browser:ZoomToPageWidth" });
   },
 
   isRectZoomedIn: function(aRect, aViewport) {
     // This function checks to see if the area of the rect visible in the
     // viewport (i.e. the "overlapArea" variable below) is approximately
     // the max area of the rect we can show. It also checks that the rect
     // is actually on-screen by testing the left and right edges of the rect.
     // In effect, this tells us whether or not zooming in to this rect
@@ -140,11 +140,11 @@ var ZoomHelper = {
         rect.y = cssTapY - (rect.h / 2);
       }
     }
 
     if (rect.w > viewport.cssWidth || rect.h > viewport.cssHeight) {
       BrowserEventHandler.resetMaxLineBoxWidth();
     }
 
-    sendMessageToJava(rect);
+    Messaging.sendRequest(rect);
   },
 };
--- a/mobile/android/chrome/content/aboutFeedback.js
+++ b/mobile/android/chrome/content/aboutFeedback.js
@@ -43,17 +43,17 @@ function init() {
 	updateActiveSection(aEvent.state ? aEvent.state.section : "intro")
   }, false);
 
   // Fill "Last visited site" input with most recent history entry URL.
   Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
 	document.getElementById("last-url").value = aData;
   }, "Feedback:LastUrl", false);
 
-  sendMessageToJava({ type: "Feedback:LastUrl" });
+  Messaging.sendRequest({ type: "Feedback:LastUrl" });
 }
 
 function uninit() {
   Services.obs.removeObserver(this, "Feedback:LastUrl");
 }
 
 function switchSection(aSection) {
   history.pushState({ section: aSection }, aSection);
@@ -61,25 +61,25 @@ function switchSection(aSection) {
 }
 
 function updateActiveSection(aSection) {
   document.querySelector("section[active]").removeAttribute("active");
   document.getElementById(aSection).setAttribute("active", true);
 }
 
 function openPlayStore() {
-  sendMessageToJava({ type: "Feedback:OpenPlayStore" });
+  Messaging.sendRequest({ type: "Feedback:OpenPlayStore" });
 
   window.close();
 }
 
 function maybeLater() {
   window.close();
 
-  sendMessageToJava({ type: "Feedback:MaybeLater" });
+  Messaging.sendRequest({ type: "Feedback:MaybeLater" });
 }
 
 function sendFeedback(aEvent) {
   // Prevent the page from reloading.
   aEvent.preventDefault();
 
   let section = history.state.section;
 
--- a/mobile/android/chrome/content/aboutHealthReport.js
+++ b/mobile/android/chrome/content/aboutHealthReport.js
@@ -83,17 +83,17 @@ let healthReportWrapper = {
       this.injectData("prefs", prefs);
     } catch (e) {
       this.reportFailure(this.ERROR_PREFS_FAILED);
     }
   },
 
   refreshPayload: function () {
     console.log("AboutHealthReport: page requested fresh payload.");
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: EVENT_HEALTH_REQUEST,
     });
   },
 
   updatePayload: function (data) {
     healthReportWrapper.injectData("payload", data);
     // Data is supposed to be a string, so the length should be
     // defined.  Just in case, we do this after injecting the data.
@@ -115,25 +115,25 @@ let healthReportWrapper = {
     };
 
     let iframe = document.getElementById("remote-report");
     iframe.contentWindow.postMessage(data, reportUrl);
   },
 
   showSettings: function () {
     console.log("AboutHealthReport: showing settings.");
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "Settings:Show",
       resource: "preferences_vendor",
     });
   },
 
   launchUpdater: function () {
     console.log("AboutHealthReport: launching updater.");
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "Updater:Launch",
     });
   },
 
   handleRemoteCommand: function (evt) {
     switch (evt.detail.command) {
       case "DisableDataSubmission":
         this.onOptOut();
--- a/mobile/android/chrome/content/aboutReader.js
+++ b/mobile/android/chrome/content/aboutReader.js
@@ -1,14 +1,15 @@
 /* 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/. */
 
 let Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
 
+Cu.import("resource://gre/modules/Messaging.jsm");
 Cu.import("resource://gre/modules/Services.jsm")
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
                                   "resource://gre/modules/UITelemetry.jsm");
 
 XPCOMUtils.defineLazyGetter(window, "gChromeWin", function ()
   window.QueryInterface(Ci.nsIInterfaceRequestor)
@@ -303,17 +304,17 @@ AboutReader.prototype = {
     if (this._isReadingListItem == 1) {
       classes.add("on");
     } else {
       classes.remove("on");
     }
   },
 
   _requestReadingListStatus: function Reader_requestReadingListStatus() {
-    gChromeWin.sendMessageToJava({
+    Messaging.sendRequest({
       type: "Reader:ListStatusRequest",
       url: this._article.url
     });
   },
 
   _onReaderToggle: function Reader_onToggle() {
     if (!this._article)
       return;
@@ -330,17 +331,17 @@ AboutReader.prototype = {
         if (success) {
           result = gChromeWin.Reader.READER_ADD_SUCCESS;
           UITelemetry.addEvent("save.1", "button", uptime, "reader");
         }
 
         let json = JSON.stringify({ fromAboutReader: true, url: this._article.url });
         Services.obs.notifyObservers(null, "Reader:Add", json);
 
-        gChromeWin.sendMessageToJava({
+        Messaging.sendRequest({
           type: "Reader:Added",
           result: result,
           title: this._article.title,
           url: this._article.url,
           length: this._article.length,
           excerpt: this._article.excerpt
         });
       }.bind(this));
@@ -354,17 +355,17 @@ AboutReader.prototype = {
       UITelemetry.addEvent("unsave.1", "button", null, "reader");
     }
   },
 
   _onShare: function Reader_onShare() {
     if (!this._article)
       return;
 
-    gChromeWin.sendMessageToJava({
+    Messaging.sendRequest({
       type: "Reader:Share",
       url: this._article.url,
       title: this._article.title
     });
 
     UITelemetry.addEvent("share.1", "list", null);
   },
 
@@ -519,24 +520,24 @@ AboutReader.prototype = {
     }
   },
 
   _toggleToolbarVisibility: function Reader_toggleToolbarVisibility() {
     this._setToolbarVisibility(!this._getToolbarVisibility());
   },
 
   _setBrowserToolbarVisiblity: function Reader_setBrowserToolbarVisiblity(visible) {
-    gChromeWin.sendMessageToJava({
+    Messaging.sendRequest({
       type: "BrowserToolbar:Visibility",
       visible: visible
     });
   },
 
   _setSystemUIVisibility: function Reader_setSystemUIVisibility(visible) {
-    gChromeWin.sendMessageToJava({
+    Messaging.sendRequest({
       type: "SystemUI:Visibility",
       visible: visible
     });
   },
 
   _loadFromURL: function Reader_loadFromURL(url) {
     this._showProgressDelayed();
 
@@ -555,17 +556,17 @@ AboutReader.prototype = {
       if (article)
         this._showContent(article);
       else
         this._showError(gStrings.GetStringFromName("aboutReader.loadError"));
     }.bind(this));
   },
 
   _requestFavicon: function Reader_requestFavicon() {
-    gChromeWin.sendMessageToJava({
+    Messaging.sendRequest({
       type: "Reader:FaviconRequest",
       url: this._article.url
     });
   },
 
   _loadFavicon: function Reader_loadFavicon(url, faviconUrl) {
     if (this._article.url !== url)
       return;
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -22,17 +22,17 @@ Cu.import("resource://gre/modules/UITele
 
 #ifdef ACCESSIBILITY
 Cu.import("resource://gre/modules/accessibility/AccessFu.jsm");
 #endif
 
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                   "resource://gre/modules/PluralForm.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava",
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
                                   "resource://gre/modules/Messaging.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
                                   "resource://gre/modules/devtools/dbg-server.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "UserAgentOverrides",
                                   "resource://gre/modules/UserAgentOverrides.jsm");
 
@@ -301,17 +301,17 @@ var BrowserApp = {
     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess();
     dump("zerdatime " + Date.now() + " - browser chrome startup finished.");
 
     this.deck = document.getElementById("browsers");
     this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() {
       try {
         BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false);
         Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
-        sendMessageToJava({ type: "Gecko:DelayedStartup" });
+        Messaging.sendRequest({ type: "Gecko:DelayedStartup" });
 
         // Queue up some other performance-impacting initializations
         Services.tm.mainThread.dispatch(function() {
           // Init LoginManager
           Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
         }, Ci.nsIThread.DISPATCH_NORMAL);
 
 #ifdef MOZ_SAFE_BROWSING
@@ -361,29 +361,29 @@ var BrowserApp = {
     Services.obs.addObserver(this, "Webapps:AutoUninstall", false);
     Services.obs.addObserver(this, "sessionstore-state-purge-complete", false);
 
     function showFullScreenWarning() {
       NativeWindow.toast.show(Strings.browser.GetStringFromName("alertFullScreenToast"), "short");
     }
 
     window.addEventListener("fullscreen", function() {
-      sendMessageToJava({
+      Messaging.sendRequest({
         type: window.fullScreen ? "ToggleChrome:Show" : "ToggleChrome:Hide"
       });
     }, false);
 
     window.addEventListener("mozfullscreenchange", function(e) {
       // This event gets fired on the document and its entire ancestor chain
       // of documents. When enabling fullscreen, it is fired on the top-level
       // document first and goes down; when disabling the order is reversed
       // (per spec). This means the last event on enabling will be for the innermost
       // document, which will have mozFullScreenElement set correctly.
       let doc = e.target;
-      sendMessageToJava({
+      Messaging.sendRequest({
         type: doc.mozFullScreen ? "DOMFullScreen:Start" : "DOMFullScreen:Stop",
         rootElement: (doc.mozFullScreen && doc.mozFullScreenElement == doc.documentElement)
       });
 
       if (doc.mozFullScreen)
         showFullScreenWarning();
     }, false);
 
@@ -456,17 +456,17 @@ var BrowserApp = {
     if (this.isGuest) {
       // Disable extension installs
       Services.prefs.setIntPref("extensions.enabledScopes", 1);
       Services.prefs.setIntPref("extensions.autoDisableScopes", 1);
       Services.prefs.setBoolPref("xpinstall.enabled", false);
     }
 
     // notify java that gecko has loaded
-    sendMessageToJava({ type: "Gecko:Ready" });
+    Messaging.sendRequest({ type: "Gecko:Ready" });
   },
 
   get _startupStatus() {
     delete this._startupStatus;
 
     let savedMilestone = null;
     try {
       savedMilestone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone");
@@ -482,17 +482,17 @@ var BrowserApp = {
     return this._startupStatus;
   },
 
   /**
    * Pass this a locale string, such as "fr" or "es_ES".
    */
   setLocale: function (locale) {
     console.log("browser.js: requesting locale set: " + locale);
-    sendMessageToJava({ type: "Locale:Set", locale: locale });
+    Messaging.sendRequest({ type: "Locale:Set", locale: locale });
   },
 
   _initRuntime: function(status, url, callback) {
     let sandbox = {};
     Services.scriptloader.loadSubScript("chrome://browser/content/WebappRT.js", sandbox);
     window.WebappRT = sandbox.WebappRT;
     WebappRT.init(status, url, callback);
   },
@@ -627,42 +627,42 @@ var BrowserApp = {
     });
 
     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"),
       NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext),
       function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_email");
 
         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
-        sendMessageToJava({
+        Messaging.sendRequest({
           type: "Contact:Add",
           email: url
         });
       });
 
     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"),
       NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext),
       function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_phone");
 
         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
-        sendMessageToJava({
+        Messaging.sendRequest({
           type: "Contact:Add",
           phone: url
         });
       });
 
     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.bookmarkLink"),
       NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkBookmarkableContext),
       function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_bookmark");
 
         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
         let title = aTarget.textContent || aTarget.title || url;
-        sendMessageToJava({
+        Messaging.sendRequest({
           type: "Bookmark:Insert",
           url: url,
           title: title
         });
       });
 
     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.playMedia"),
       NativeWindow.contextmenus.mediaContext("media-paused"),
@@ -768,17 +768,17 @@ var BrowserApp = {
       });
 
     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.setImageAs"),
       NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext),
       function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_background_image");
 
         let src = aTarget.src;
-        sendMessageToJava({
+        Messaging.sendRequest({
           type: "Image:SetAs",
           url: src
         });
       });
 
     NativeWindow.contextmenus.add(
       function(aTarget) {
         if (aTarget instanceof HTMLVideoElement) {
@@ -958,17 +958,17 @@ var BrowserApp = {
     try {
       aBrowser.loadURIWithFlags(aURI, flags, referrerURI, charset, postData);
     } catch(e) {
       if (tab) {
         let message = {
           type: "Content:LoadError",
           tabID: tab.id
         };
-        sendMessageToJava(message);
+        Messaging.sendRequest(message);
         dump("Handled load error: " + e)
       }
     }
   },
 
   addTab: function addTab(aURI, aParams) {
     aParams = aParams || {};
 
@@ -1005,17 +1005,17 @@ var BrowserApp = {
       Cu.reportError("Error trying to close tab (tab doesn't exist)");
       return;
     }
 
     let message = {
       type: "Tab:Close",
       tabID: aTab.id
     };
-    sendMessageToJava(message);
+    Messaging.sendRequest(message);
   },
 
   _loadWebapp: function(aMessage) {
 
     this._initRuntime(this._startupStatus, aMessage.url, aUrl => {
       this.manifestUrl = aMessage.url;
       this.addTab(aUrl, { title: aMessage.name });
     });
@@ -1075,17 +1075,17 @@ var BrowserApp = {
     // There's nothing to do if the tab is already selected
     if (aTab == this.selectedTab)
       return;
 
     let message = {
       type: "Tab:Select",
       tabID: aTab.id
     };
-    sendMessageToJava(message);
+    Messaging.sendRequest(message);
   },
 
   /**
    * Gets an open tab with the given URL.
    *
    * @param  aURL URL to look for
    * @return the tab with the given URL, or null if no such tab exists
    */
@@ -1318,17 +1318,17 @@ var BrowserApp = {
           pref.type = "string";
           pref.value = pref.value.toString();
           break;
       }
 
       prefs.push(pref);
     }
 
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "Preferences:Data",
       requestId: aRequestId,    // opaque request identifier, can be any string/int/whatever
       preferences: prefs
     });
   },
 
   setPreferences: function setPreferences(aPref) {
     let json = JSON.parse(aPref);
@@ -1424,26 +1424,26 @@ var BrowserApp = {
           promises.push(Sanitizer.clearItem("sessions"));
           break;
         default:
           promises.push(Sanitizer.clearItem(key));
       }
     }
 
     Promise.all(promises).then(function() {
-      sendMessageToJava({
+      Messaging.sendRequest({
         type: "Sanitize:Finished",
         success: true
       });
 
       if (callback) {
         callback();
       }
     }).catch(function(err) {
-      sendMessageToJava({
+      Messaging.sendRequest({
         type: "Sanitize:Finished",
         error: err,
         success: false
       });
 
       if (callback) {
         callback();
       }
@@ -1616,17 +1616,17 @@ var BrowserApp = {
         // perform a keyword search on the selected tab.
         this.selectedTab.userSearch = aData;
 
         // Don't store queries in private browsing mode.
         let isPrivate = PrivateBrowsingUtils.isWindowPrivate(this.selectedTab.browser.contentWindow);
         let query = isPrivate ? "" : aData;
 
         let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
-        sendMessageToJava({
+        Messaging.sendRequest({
           type: "Search:Keyword",
           identifier: engine.identifier,
           name: engine.name,
           query: query
         });
         break;
 
       case "Browser:Quit":
@@ -1677,21 +1677,21 @@ var BrowserApp = {
       case "FormHistory:Init": {
         // Force creation/upgrade of formhistory.sqlite
         FormHistory.count({});
         Services.obs.removeObserver(this, "FormHistory:Init");
         break;
       }
 
       case "sessionstore-state-purge-complete":
-        sendMessageToJava({ type: "Session:StatePurged" });
+        Messaging.sendRequest({ type: "Session:StatePurged" });
         break;
 
       case "gather-telemetry":
-        sendMessageToJava({ type: "Telemetry:Gather" });
+        Messaging.sendRequest({ type: "Telemetry:Gather" });
         break;
 
       case "Viewport:FixedMarginsChanged":
         gViewportMargins = JSON.parse(aData);
         this.selectedTab.updateViewportSize(gScreenWidth);
         break;
 
       case "nsPref:changed":
@@ -1866,25 +1866,25 @@ var NativeWindow = {
     Services.obs.removeObserver(this, "Menu:Clicked");
     Services.obs.removeObserver(this, "Doorhanger:Reply");
     Services.obs.removeObserver(this, "Toast:Click", false);
     Services.obs.removeObserver(this, "Toast:Hidden", false);
     this.contextmenus.uninit();
   },
 
   loadDex: function(zipFile, implClass) {
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "Dex:Load",
       zipfile: zipFile,
       impl: implClass || "Main"
     });
   },
 
   unloadDex: function(zipFile) {
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "Dex:Unload",
       zipfile: zipFile
     });
   },
 
   toast: {
     _callbacks: {},
     show: function(aMessage, aDuration, aOptions) {
@@ -1908,17 +1908,17 @@ var NativeWindow = {
           // If the caller specified a button, make sure we convert any chrome urls
           // to jar:jar urls so that the frontend can show them
           msg.button.icon = resolveGeckoURI(aOptions.button.icon);
         };
 
         this._callbacks[msg.button.id] = aOptions.button.callback;
       }
 
-      sendMessageToJava(msg);
+      Messaging.sendRequest(msg);
     }
   },
 
   menu: {
     _callbacks: [],
     _menuId: 1,
     toolsMenuID: -1,
     add: function() {
@@ -1933,31 +1933,31 @@ var NativeWindow = {
           };
       } else {
          throw "Incorrect number of parameters";
       }
 
       options.type = "Menu:Add";
       options.id = this._menuId;
 
-      sendMessageToJava(options);
+      Messaging.sendRequest(options);
       this._callbacks[this._menuId] = options.callback;
       this._menuId++;
       return this._menuId - 1;
     },
 
     remove: function(aId) {
-      sendMessageToJava({ type: "Menu:Remove", id: aId });
+      Messaging.sendRequest({ type: "Menu:Remove", id: aId });
     },
 
     update: function(aId, aOptions) {
       if (!aOptions)
         return;
 
-      sendMessageToJava({
+      Messaging.sendRequest({
         type: "Menu:Update", 
         id: aId,
         options: aOptions
       });
     }
   },
 
   doorhanger: {
@@ -1997,21 +1997,21 @@ var NativeWindow = {
         type: "Doorhanger:Add",
         message: aMessage,
         value: aValue,
         buttons: aButtons,
         // use the current tab if none is provided
         tabID: aTabID || BrowserApp.selectedTab.id,
         options: aOptions || {}
       };
-      sendMessageToJava(json);
+      Messaging.sendRequest(json);
     },
 
     hide: function(aValue, aTabID) {
-      sendMessageToJava({
+      Messaging.sendRequest({
         type: "Doorhanger:Remove",
         value: aValue,
         tabID: aTabID
       });
     }
   },
 
   observe: function(aSubject, aTopic, aData) {
@@ -3191,17 +3191,17 @@ Tab.prototype = {
         external: ("external" in aParams) ? aParams.external : false,
         selected: ("selected" in aParams) ? aParams.selected : true,
         title: truncate(title, MAX_TITLE_LENGTH),
         delayLoad: aParams.delayLoad || false,
         desktopMode: this.desktopMode,
         isPrivate: isPrivate,
         stub: stub
       };
-      sendMessageToJava(message);
+      Messaging.sendRequest(message);
 
       this.overscrollController = new OverscrollController(this);
     }
 
     this.browser.contentWindow.controllers.insertControllerAt(0, this.overscrollController);
 
     let flags = Ci.nsIWebProgress.NOTIFY_STATE_ALL |
                 Ci.nsIWebProgress.NOTIFY_LOCATION |
@@ -3255,17 +3255,17 @@ Tab.prototype = {
 
       try {
         this.browser.loadURIWithFlags(aURL, flags, referrerURI, charset, postData);
       } catch(e) {
         let message = {
           type: "Content:LoadError",
           tabID: this.id
         };
-        sendMessageToJava(message);
+        Messaging.sendRequest(message);
         dump("Handled load error: " + e);
       }
     }
   },
 
   /**
    * Retrieves the font size in twips for a given element.
    */
@@ -3346,17 +3346,17 @@ Tab.prototype = {
 
   /** 
    * Reloads the tab with the desktop mode setting.
    */
   reloadWithMode: function (aDesktopMode) {
     // Set desktop mode for tab and send change to Java
     if (this.desktopMode != aDesktopMode) {
       this.desktopMode = aDesktopMode;
-      sendMessageToJava({
+      Messaging.sendRequest({
         type: "DesktopMode:Changed",
         desktopMode: aDesktopMode,
         tabID: this.id
       });
     }
 
     // Only reload the page for http/https schemes
     let currentURI = this.browser.currentURI;
@@ -3775,17 +3775,17 @@ Tab.prototype = {
         let errorType = "";
         if (docURI.startsWith("about:certerror"))
           errorType = "certerror";
         else if (docURI.startsWith("about:blocked"))
           errorType = "blocked"
         else if (docURI.startsWith("about:neterror"))
           errorType = "neterror";
 
-        sendMessageToJava({
+        Messaging.sendRequest({
           type: "DOMContentLoaded",
           tabID: this.id,
           bgColor: backgroundColor,
           errorType: errorType,
           metadata: this.metatags
         });
 
         this.metatags = null;
@@ -3886,17 +3886,17 @@ Tab.prototype = {
           }
 
           let json = {
             type: "Link:Favicon",
             tabID: this.id,
             href: resolveGeckoURI(target.href),
             size: maxSize
           };
-          sendMessageToJava(json);
+          Messaging.sendRequest(json);
         } else if (list.indexOf("[alternate]") != -1 && aEvent.type == "DOMLinkAdded") {
           let type = target.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, "");
           let isFeed = (type == "application/rss+xml" || type == "application/atom+xml");
 
           if (!isFeed)
             return;
 
           try {
@@ -3906,17 +3906,17 @@ Tab.prototype = {
             if (!this.browser.feeds)
               this.browser.feeds = [];
             this.browser.feeds.push({ href: target.href, title: target.title, type: type });
 
             let json = {
               type: "Link:Feed",
               tabID: this.id
             };
-            sendMessageToJava(json);
+            Messaging.sendRequest(json);
           } catch (e) {}
         } else if (list.indexOf("[search]" != -1) && aEvent.type == "DOMLinkAdded") {
           let type = target.type && target.type.toLowerCase();
 
           // Replace all starting or trailing spaces or spaces before "*;" globally w/ "".
           type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
 
           // Check that type matches opensearch.
@@ -3960,47 +3960,47 @@ Tab.prototype = {
 
             // Broadcast message that this tab contains search engines that should be visible.
             let newEngineMessage = {
               type: "Link:OpenSearch",
               tabID: this.id,
               visible: true
             };
 
-            sendMessageToJava(newEngineMessage);
+            Messaging.sendRequest(newEngineMessage);
           }
         }
         break;
       }
 
       case "DOMTitleChanged": {
         if (!aEvent.isTrusted)
           return;
 
         // ignore on frames and other documents
         if (aEvent.originalTarget != this.browser.contentDocument)
           return;
 
-        sendMessageToJava({
+        Messaging.sendRequest({
           type: "DOMTitleChanged",
           tabID: this.id,
           title: truncate(aEvent.target.title, MAX_TITLE_LENGTH)
         });
         break;
       }
 
       case "DOMWindowClose": {
         if (!aEvent.isTrusted)
           return;
 
         // Find the relevant tab, and close it from Java
         if (this.browser.contentWindow == aEvent.target) {
           aEvent.preventDefault();
 
-          sendMessageToJava({
+          Messaging.sendRequest({
             type: "Tab:Close",
             tabID: this.id
           });
         }
         break;
       }
 
       case "DOMWillOpenModalDialog": {
@@ -4060,17 +4060,17 @@ Tab.prototype = {
         break;
       }
 
       case "pageshow": {
         // only send pageshow for the top-level document
         if (aEvent.originalTarget.defaultView != this.browser.contentWindow)
           return;
 
-        sendMessageToJava({
+        Messaging.sendRequest({
           type: "Content:PageShow",
           tabID: this.id
         });
 
         if (!aEvent.persisted && Services.prefs.getBoolPref("browser.ui.linkify.phone")) {
           if (!this._linkifier)
             this._linkifier = new Linkifier();
           this._linkifier.linkifyNumbers(this.browser.contentWindow.document);
@@ -4107,17 +4107,17 @@ Tab.prototype = {
             } else {
               this.readerActive = true;
             }
             return;
           }
 
           this.savedArticle = article;
 
-          sendMessageToJava({
+          Messaging.sendRequest({
             type: "Content:ReaderEnabled",
             tabID: this.id
           });
 
           if(this.readerActive)
             this.readerActive = false;
 
           if(!this.readerEnabled)
@@ -4171,17 +4171,17 @@ Tab.prototype = {
       let message = {
         type: "Content:StateChange",
         tabID: this.id,
         uri: truncate(uri, MAX_URI_LENGTH),
         state: aStateFlags,
         restoring: restoring,
         success: success
       };
-      sendMessageToJava(message);
+      Messaging.sendRequest(message);
     }
   },
 
   onLocationChange: function(aWebProgress, aRequest, aLocationURI, aFlags) {
     let contentWin = aWebProgress.DOMWindow;
 
     // Browser webapps may load content inside iframes that can not reach across the app/frame boundary
     // i.e. even though the page is loaded in an iframe window.top != webapp
@@ -4253,17 +4253,17 @@ Tab.prototype = {
       tabID: this.id,
       uri: truncate(fixedURI.spec, MAX_URI_LENGTH),
       userSearch: this.userSearch || "",
       baseDomain: baseDomain,
       contentType: (contentType ? contentType : ""),
       sameDocument: sameDocument
     };
 
-    sendMessageToJava(message);
+    Messaging.sendRequest(message);
 
     // The search term is only valid for this location change event, so reset it here.
     this.userSearch = "";
 
     if (!sameDocument) {
       // XXX This code assumes that this is the earliest hook we have at which
       // browser.contentDocument is changed to the new document we're loading
 
@@ -4295,17 +4295,17 @@ Tab.prototype = {
     let identity = IdentityHandler.checkIdentity(aState, this.browser);
 
     let message = {
       type: "Content:SecurityChange",
       tabID: this.id,
       identity: identity
     };
 
-    sendMessageToJava(message);
+    Messaging.sendRequest(message);
   },
 
   onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) {
   },
 
   onStatusChange: function(aBrowser, aWebProgress, aRequest, aStatus, aMessage) {
   },
 
@@ -4322,17 +4322,17 @@ Tab.prototype = {
       if ("url" in aParams)
         message.url = aParams.url;
       if ("index" in aParams)
         message.index = aParams.index;
       if ("numEntries" in aParams)
         message.numEntries = aParams.numEntries;
     }
 
-    sendMessageToJava(message);
+    Messaging.sendRequest(message);
   },
 
   _getGeckoZoom: function() {
     let res = {x: {}, y: {}};
     let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
     cwu.getResolution(res.x, res.y);
     let zoom = res.x.value * window.devicePixelRatio;
     return zoom;
@@ -4559,17 +4559,17 @@ Tab.prototype = {
     this.lastPageSizeAfterViewportRemeasure = {
       width: viewport.pageRight - viewport.pageLeft,
       height: viewport.pageBottom - viewport.pageTop
     };
   },
 
   sendViewportMetadata: function sendViewportMetadata() {
     let metadata = this.metadata;
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "Tab:ViewportMetadata",
       allowZoom: metadata.allowZoom,
       allowDoubleTapZoom: metadata.allowDoubleTapZoom,
       defaultZoom: metadata.defaultZoom || window.devicePixelRatio,
       minZoom: metadata.minZoom || 0,
       maxZoom: metadata.maxZoom || 0,
       isRTL: metadata.isRTL,
       tabID: this.id
@@ -4777,17 +4777,17 @@ var BrowserEventHandler = {
 
       if (this._scrollableElement != null) {
         // Discard if it's the top-level scrollable, we let Java handle this
         // The top-level scrollable is the body in quirks mode and the html element
         // in standards mode
         let doc = BrowserApp.selectedBrowser.contentDocument;
         let rootScrollable = (doc.compatMode === "BackCompat" ? doc.body : doc.documentElement);
         if (this._scrollableElement != rootScrollable) {
-          sendMessageToJava({ type: "Panning:Override" });
+          Messaging.sendRequest({ type: "Panning:Override" });
         }
       }
     }
 
     if (!ElementTouchHelper.isElementClickable(closest, null, false))
       closest = ElementTouchHelper.elementFromPoint(aEvent.changedTouches[0].screenX,
                                                     aEvent.changedTouches[0].screenY);
     if (!closest)
@@ -4817,17 +4817,17 @@ var BrowserEventHandler = {
 
   observe: function(aSubject, aTopic, aData) {
     if (aTopic == "dom-touch-listener-added") {
       let tab = BrowserApp.getTabForWindow(aSubject.top);
       if (!tab || tab.hasTouchListener)
         return;
 
       tab.hasTouchListener = true;
-      sendMessageToJava({
+      Messaging.sendRequest({
         type: "Tab:HasTouchListener",
         tabID: tab.id
       });
       return;
     } else if (aTopic == "nsPref:changed") {
       if (aData == "browser.zoom.reflowOnZoom") {
         this.updateReflozPref();
       }
@@ -4867,30 +4867,30 @@ var BrowserEventHandler = {
         if (this._firstScrollEvent) {
           while (this._scrollableElement != null &&
                  !this._elementCanScroll(this._scrollableElement, x, y))
             this._scrollableElement = this._findScrollableElement(this._scrollableElement, false);
 
           let doc = BrowserApp.selectedBrowser.contentDocument;
           if (this._scrollableElement == null ||
               this._scrollableElement == doc.documentElement) {
-            sendMessageToJava({ type: "Panning:CancelOverride" });
+            Messaging.sendRequest({ type: "Panning:CancelOverride" });
             return;
           }
 
           this._firstScrollEvent = false;
         }
 
         // Scroll the scrollable element
         if (this._elementCanScroll(this._scrollableElement, x, y)) {
           this._scrollElementBy(this._scrollableElement, x, y);
-          sendMessageToJava({ type: "Gesture:ScrollAck", scrolled: true });
+          Messaging.sendRequest({ type: "Gesture:ScrollAck", scrolled: true });
           SelectionHandler.subdocumentScrolled(this._scrollableElement);
         } else {
-          sendMessageToJava({ type: "Gesture:ScrollAck", scrolled: false });
+          Messaging.sendRequest({ type: "Gesture:ScrollAck", scrolled: false });
         }
 
         break;
       }
 
       case "Gesture:CancelTouch":
         this._cancelTapHighlight();
         break;
@@ -5796,17 +5796,17 @@ var FormAssistant = {
       let suggestions = autoCompleteSuggestions.concat(listSuggestions);
 
       // Return false if there are no suggestions to show
       if (!suggestions.length) {
         aCallback(false);
         return;
       }
 
-      sendMessageToJava({
+      Messaging.sendRequest({
         type:  "FormAssist:AutoComplete",
         suggestions: suggestions,
         rect: ElementTouchHelper.getBoundingContentRect(aElement)
       });
 
       // Keep track of input element so we can fill it in if the user
       // selects an autocomplete suggestion
       this._currentInputElement = aElement;
@@ -5831,27 +5831,27 @@ var FormAssistant = {
   },
 
   // Sends a validation message and position data for an element to the Java UI.
   // Returns true if there's a validation message to show, false otherwise.
   _showValidationMessage: function _sendValidationMessage(aElement) {
     if (!this._isValidateable(aElement))
       return false;
 
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "FormAssist:ValidationMessage",
       validationMessage: aElement.validationMessage,
       rect: ElementTouchHelper.getBoundingContentRect(aElement)
     });
 
     return true;
   },
 
   _hideFormAssistPopup: function _hideFormAssistPopup() {
-    sendMessageToJava({ type: "FormAssist:Hide" });
+    Messaging.sendRequest({ type: "FormAssist:Hide" });
   }
 };
 
 /**
  * An object to watch for Gecko status changes -- add-on installs, pref changes
  * -- and reflect them back to Java.
  */
 let HealthReportStatusListener = {
@@ -5912,17 +5912,17 @@ let HealthReportStatusListener = {
           case this.PREF_BLOCKLIST_ENABLED:
             response.value = Services.prefs.getBoolPref(aData);
             break;
           default:
             console.log("Unexpected pref in HealthReportStatusListener: " + aData);
             return;
         }
 
-        sendMessageToJava(response);
+        Messaging.sendRequest(response);
         break;
     }
   },
 
   MILLISECONDS_PER_DAY: 24 * 60 * 60 * 1000,
 
   COPY_FIELDS: [
     "blocklistState",
@@ -5971,17 +5971,17 @@ let HealthReportStatusListener = {
     return o;
   },
 
   notifyJava: function (aAddon, aNeedsRestart, aAction="Addons:Change") {
     let json = this.jsonForAddon(aAddon);
     if (this._shouldIgnore(aAddon)) {
       json.ignore = true;
     }
-    sendMessageToJava({ type: aAction, id: aAddon.id, json: json });
+    Messaging.sendRequest({ type: aAction, id: aAddon.id, json: json });
   },
 
   // Add-on listeners.
   onEnabling: function (aAddon, aNeedsRestart) {
     this.notifyJava(aAddon, aNeedsRestart);
   },
   onDisabling: function (aAddon, aNeedsRestart) {
     this.notifyJava(aAddon, aNeedsRestart);
@@ -6034,17 +6034,17 @@ let HealthReportStatusListener = {
           jsonP[pref] = {
             pref: pref,
             value: Services.prefs.getCharPref(pref),
             isUserSet: Services.prefs.prefHasUserValue(pref),
           };
         }
 
         console.log("Sending snapshot message.");
-        sendMessageToJava({
+        Messaging.sendRequest({
           type: "HealthReport:Snapshot",
           json: {
             addons: jsonA,
             prefs: jsonP,
           },
         });
       }.bind(this));
   },
@@ -6688,17 +6688,17 @@ var CharacterEncoding = {
   },
 
   sendState: function sendState() {
     let showCharEncoding = "false";
     try {
       showCharEncoding = Services.prefs.getComplexValue("browser.menu.showCharacterEncoding", Ci.nsIPrefLocalizedString).data;
     } catch (e) { /* Optional */ }
 
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "CharEncoding:State",
       visible: showCharEncoding
     });
   },
 
   getEncoding: function getEncoding() {
     function infoToCharset(info) {
       return { code: info.value, title: info.label };
@@ -6722,17 +6722,17 @@ var CharacterEncoding = {
 
     for (let i = 0; i < charsetCount; i++) {
       if (this._charsets[i].code === docCharset) {
         selected = i;
         break;
       }
     }
 
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "CharEncoding:Data",
       charsets: this._charsets,
       selected: selected
     });
   },
 
   setEncoding: function setEncoding(aEncoding) {
     let browser = BrowserApp.selectedBrowser;
@@ -6934,17 +6934,17 @@ OverscrollController.prototype = {
     return (this.tab.getViewport().y == 0);
   },
 
   isCommandEnabled : function isCommandEnabled(aCommand) {
     return this.supportsCommand(aCommand);
   },
 
   doCommand : function doCommand(aCommand){
-    sendMessageToJava({ type: "ToggleChrome:Focus" });
+    Messaging.sendRequest({ type: "ToggleChrome:Focus" });
   },
 
   onEvent : function onEvent(aEvent) { }
 };
 
 var SearchEngines = {
   _contextMenuId: null,
   PREF_SUGGEST_ENABLED: "browser.search.suggest.enabled",
@@ -7028,17 +7028,17 @@ var SearchEngines = {
     // the default engine because we only show suggestions for the default engine in the UI.
     let engine = Services.search.defaultEngine;
     if (engine.supportsResponseType("application/x-suggestions+json")) {
       suggestEngine = engine.name;
       suggestTemplate = engine.getSubmission("__searchTerms__", "application/x-suggestions+json").uri.spec;
     }
 
     // By convention, the currently configured default engine is at position zero in searchEngines.
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "SearchEngines:Data",
       searchEngines: searchEngines,
       suggest: {
         engine: suggestEngine,
         template: suggestTemplate,
         enabled: Services.prefs.getBoolPref(this.PREF_SUGGEST_ENABLED),
         prompted: Services.prefs.getBoolPref(this.PREF_SUGGEST_PROMPTED)
       }
@@ -7112,17 +7112,17 @@ var SearchEngines = {
       if (engines.length < 1) {
         // Broadcast message that there are no more add-able search engines.
         let newEngineMessage = {
           type: "Link:OpenSearch",
           tabID: tab.id,
           visible: false
         };
 
-        sendMessageToJava(newEngineMessage);
+        Messaging.sendRequest(newEngineMessage);
       }
     }).bind(this));
   },
 
   addOpenSearchEngine: function addOpenSearchEngine(engine) {
     Services.search.addEngine(engine.url, Ci.nsISearchEngine.DATA_XML, engine.iconURL, false, {
       onSuccess: function() {
         // Display a toast confirming addition of new search engine.
@@ -7407,23 +7407,23 @@ let Reader = {
     Services.obs.addObserver(this, "Reader:Add", false);
     Services.obs.addObserver(this, "Reader:Remove", false);
 
     Services.prefs.addObserver("reader.parse-on-load.", this, false);
   },
 
   pageAction: {
     readerModeCallback: function(){
-      sendMessageToJava({
+      Messaging.sendRequest({
         type: "Reader:Click",
       });
     },
 
     readerModeActiveCallback: function(){
-      sendMessageToJava({
+      Messaging.sendRequest({
         type: "Reader:LongClick",
       });
 
       UITelemetry.addEvent("save.1", "pageaction", null, "reader");
     },
   },
 
   updatePageAction: function(tab) {
@@ -7487,17 +7487,17 @@ let Reader = {
         } else {
           throw new Error("Reader:Add requires a tabID or an URL as argument");
         }
 
         let sendResult = function(result, article) {
           article = article || {};
           this.log("Reader:Add success=" + result + ", url=" + url + ", title=" + article.title + ", excerpt=" + article.excerpt);
 
-          sendMessageToJava({
+          Messaging.sendRequest({
             type: "Reader:Added",
             result: result,
             title: truncate(article.title, MAX_TITLE_LENGTH),
             url: truncate(url, MAX_URI_LENGTH),
             length: article.length,
             excerpt: article.excerpt
           });
         }.bind(this);
@@ -7535,17 +7535,17 @@ let Reader = {
 
         if (!("url" in args)) {
           throw new Error("Reader:Remove requires URL as an argument");
         }
 
         this.removeArticleFromCache(args.url, function(success) {
           this.log("Reader:Remove success=" + success + ", url=" + args.url);
           if (success && args.notify) {
-            sendMessageToJava({
+            Messaging.sendRequest({
               type: "Reader:Removed",
               url: args.url
             });
           }
         }.bind(this));
         break;
       }
 
@@ -8180,17 +8180,17 @@ var Distribution = {
     for (let key in localizeablePrefsOverrides) {
       try {
         let value = localizeablePrefsOverrides[key];
         localizedString.data = "data:text/plain," + key + "=" + value;
         defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
       } catch (e) { /* ignore bad prefs and move on */ }
     }
 
-    sendMessageToJava({ type: "Distribution:Set:OK" });
+    Messaging.sendRequest({ type: "Distribution:Set:OK" });
   },
 
   // aFile is an nsIFile
   // aCallback takes the parsed JSON object as a parameter
   readJSON: function dc_readJSON(aFile, aCallback) {
     Task.spawn(function() {
       let bytes = yield OS.File.read(aFile.path);
       let raw = new TextDecoder().decode(bytes) || "";
--- a/mobile/android/components/ContentDispatchChooser.js
+++ b/mobile/android/components/ContentDispatchChooser.js
@@ -59,17 +59,17 @@ ContentDispatchChooser.prototype =
           button: {
             label: searchText,
             callback: function() {
               let message = {
                 type: "Intent:Open",
                 url: "market://search?q=" + aURI.scheme,
               };
 
-              sendMessageToJava(message);
+              Messaging.sendRequest(message);
             }
           }
         });
       }
     }
   },
 };
 
--- a/mobile/android/components/SessionStore.js
+++ b/mobile/android/components/SessionStore.js
@@ -12,17 +12,17 @@ Cu.import("resource://gre/modules/Servic
 
 #ifdef MOZ_CRASHREPORTER
 XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
   "@mozilla.org/xre/app-info;1", "nsICrashReporter");
 #endif
 
 XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava", "resource://gre/modules/Messaging.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 function dump(a) {
   Services.console.logStringMessage(a);
 }
 
 // -----------------------------------------------------------------------
 // Session Store
@@ -139,17 +139,17 @@ SessionStore.prototype = {
 
               if (window.BrowserApp.tabs.length == 0) {
                 window.BrowserApp.addTab("about:home", {
                   selected: true
                 });
               }
 
               // Let Java know we're done restoring tabs so tabs added after this can be animated
-              sendMessageToJava({
+              Messaging.sendRequest({
                 type: "Session:RestoreEnd"
               });
             }.bind(this)
           };
           Services.obs.addObserver(restoreCleanup, "sessionstore-windows-restored", false);
 
           // Do a restore, triggered by Java
           let data = JSON.parse(aData);
@@ -449,17 +449,17 @@ SessionStore.prototype = {
       }
     }
 
     // Write only non-private data to disk
     this._writeFile(this._sessionFile, JSON.stringify(normalData), aAsync);
 
     // If we have private data, send it to Java; otherwise, send null to
     // indicate that there is no private data
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "PrivateBrowsing:Data",
       session: (privateData.windows.length > 0 && privateData.windows[0].tabs.length > 0) ? JSON.stringify(privateData) : null
     });
 
     this._lastSaveTime = Date.now();
   },
 
   _getCurrentState: function ss_getCurrentState() {
@@ -938,17 +938,17 @@ SessionStore.prototype = {
         // Get the url and title for the last entry in the session history.
         let lastEntry = tab.entries[tab.entries.length - 1];
         return {
           url: lastEntry.url,
           title: lastEntry.title || ""
         };
       });
 
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "ClosedTabs:Data",
       tabs: tabs
     });
   },
 
   getTabValue: function ss_getTabValue(aTab, aKey) {
     let browser = aTab.browser;
     let data = browser.__SS_extdata || {};
--- a/mobile/android/components/TabSource.js
+++ b/mobile/android/components/TabSource.js
@@ -7,17 +7,17 @@
 const { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
                                   "resource://gre/modules/Prompt.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava",
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
                                   "resource://gre/modules/Messaging.jsm");
 
 function TabSource() {
 }
 
 TabSource.prototype = {
   classID: Components.ID("{5850c76e-b916-4218-b99a-31f004e0a7e7}"),
   classDescription: "Fennec Tab Source",
@@ -67,25 +67,25 @@ TabSource.prototype = {
     return tabs[result].browser.contentWindow;
   },
 
   notifyStreamStart: function(window) {
     let app = Services.wm.getMostRecentWindow("navigator:browser").BrowserApp;
     let tabs = app.tabs;
     for (var i in tabs) {
       if (tabs[i].browser.contentWindow == window) {
-        sendMessageToJava({ type: "Tab:StreamStart", tabID: tabs[i].id });
+        Messaging.sendRequest({ type: "Tab:StreamStart", tabID: tabs[i].id });
       }
     }
   },
 
   notifyStreamStop: function(window) {
     let app = Services.wm.getMostRecentWindow("navigator:browser").BrowserApp;
     let tabs = app.tabs;
     for (let i in tabs) {
       if (tabs[i].browser.contentWindow == window) {
-        sendMessageToJava({ type: "Tab:StreamStop", tabID: tabs[i].id });
+        Messaging.sendRequest({ type: "Tab:StreamStop", tabID: tabs[i].id });
       }
     }
   }
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TabSource]);
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -206,17 +206,19 @@ password.dontSave=Don't save
 # This is not a string to translate. If users frequently use the "Character Encoding"
 # menu, set this to "true". Otherwise, you can leave it as "false".
 browser.menu.showCharacterEncoding=false
 
 # Text Selection
 selectionHelper.textCopied=Text copied to clipboard
 
 # Casting
-casting.prompt=Cast to Device
+# LOCALIZATION NOTE (casting.sendToDevice): Label that will be used in the
+# dialog/prompt.
+casting.sendToDevice=Send to Device
 casting.mirrorTab=Mirror Tab
 casting.mirrorTabStop=Stop Mirror
 
 # Context menu
 contextmenu.openInNewTab=Open Link in New Tab
 contextmenu.openInPrivateTab=Open Link in Private Tab
 contextmenu.addToReadingList=Add to Reading List
 contextmenu.share=Share
@@ -242,17 +244,19 @@ contextmenu.playMedia=Play
 contextmenu.pauseMedia=Pause
 contextmenu.shareMedia=Share Video
 contextmenu.showControls2=Show Controls
 contextmenu.mute=Mute
 contextmenu.unmute=Unmute
 contextmenu.saveVideo=Save Video
 contextmenu.saveAudio=Save Audio
 contextmenu.addToContacts=Add to Contacts
-contextmenu.castToScreen=Cast to Screen
+# LOCALIZATION NOTE (contextmenu.sendToDevice):
+# The label that will be used in the contextmenu and the pageaction
+contextmenu.sendToDevice=Send to Device
 
 contextmenu.copy=Copy
 contextmenu.cut=Cut
 contextmenu.selectAll=Select All
 contextmenu.paste=Paste
 
 contextmenu.call=Call
 
@@ -379,9 +383,9 @@ browser.menu.context.mailto = Mail
 # "Subscribe to page" prompts created in FeedHandler.js
 feedHandler.chooseFeed=Choose feed
 feedHandler.subscribeWith=Subscribe with
 
 # LOCALIZATION NOTE (nativeWindow.deprecated):
 # This string is shown in the console when someone uses deprecated NativeWindow apis.
 # %1$S=name of the api that's deprecated, %2$S=New API to use. This may be a url to
 # a file they should import or the name of an api.
-nativeWindow.deprecated=%S is deprecated. Please use %S instead
\ No newline at end of file
+nativeWindow.deprecated=%S is deprecated. Please use %S instead
--- a/mobile/android/modules/Accounts.jsm
+++ b/mobile/android/modules/Accounts.jsm
@@ -67,14 +67,14 @@ let Accounts = Object.freeze({
    *
    * Optional extras are passed, as a JSON string, to the Firefox
    * Account Getting Started activity in the extras bundle of the
    * activity launch intent, under the key "extras".
    *
    * There is no return value from this method.
    */
   launchSetup: function (extras) {
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "Accounts:Create",
       extras: extras,
     });
   },
 });
--- a/mobile/android/modules/HelperApps.jsm
+++ b/mobile/android/modules/HelperApps.jsm
@@ -6,16 +6,19 @@
 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
                                   "resource://gre/modules/Prompt.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
+                                  "resource://gre/modules/Messaging.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava",
                                   "resource://gre/modules/Messaging.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
   let ContentAreaUtils = {};
   Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
   return ContentAreaUtils;
 });
@@ -147,17 +150,17 @@ var HelperApps =  {
       sendMessageToJava(msg, function(data) {
         callback(parseData(data));
       });
     }
   },
 
   launchUri: function launchUri(uri) {
     let msg = this._getMessage("Intent:Open", uri);
-    sendMessageToJava(msg);
+    Messaging.sendRequest(msg);
   },
 
   _parseApps: function _parseApps(appInfo) {
     // appInfo -> {apps: [app1Label, app1Default, app1PackageName, app1ActivityName, app2Label, app2Defaut, ...]}
     // see GeckoAppShell.java getHandlersForIntent function for details
     const numAttr = 4; // 4 elements per ResolveInfo: label, default, package name, activity name.
 
     let apps = [];
@@ -197,17 +200,17 @@ var HelperApps =  {
             callback(data);
         });
     } else {
         let msg = this._getMessage("Intent:Open", uri, {
             packageName: app.packageName,
             className: app.activityName
         });
 
-        sendMessageToJava(msg);
+        Messaging.sendRequest(msg);
     }
   },
 
   _sendMessageSync: function(msg) {
     let res = null;
     sendMessageToJava(msg, function(data) {
       res = data;
     });
--- a/mobile/android/modules/Home.jsm
+++ b/mobile/android/modules/Home.jsm
@@ -89,17 +89,17 @@ let HomeBanner = (function () {
       totalWeight += message.weight;
       message.totalWeight = totalWeight;
     }
 
     let threshold = Math.random() * totalWeight;
     for (let key in _messages) {
       let message = _messages[key];
       if (threshold < message.totalWeight) {
-        sendMessageToJava({
+        Messaging.sendRequest({
           type: "HomeBanner:Data",
           id: message.id,
           text: message.text,
           iconURI: message.iconURI
         });
         return;
       }
     }
@@ -209,17 +209,17 @@ let HomePanels = (function () {
           try {
             panels.push(_generatePanel(id));
           } catch(e) {
             Cu.reportError("Home.panels: Invalid options, panel.id = " + id + ": " + e);
           }
         }
       }
 
-      sendMessageToJava({
+      Messaging.sendRequest({
         type: "HomePanels:Data",
         panels: panels,
         requestId: requestId
       });
     },
 
     "HomePanels:Authenticate": function handlePanelsAuthenticate(id) {
       // Generate panel options to get auth handler.
@@ -424,35 +424,35 @@ let HomePanels = (function () {
       _assertPanelExists(id);
 
       delete _registeredPanels[id];
     },
 
     install: function(id) {
       _assertPanelExists(id);
 
-      sendMessageToJava({
+      Messaging.sendRequest({
         type: "HomePanels:Install",
         panel: _generatePanel(id)
       });
     },
 
     uninstall: function(id) {
       _assertPanelExists(id);
 
-      sendMessageToJava({
+      Messaging.sendRequest({
         type: "HomePanels:Uninstall",
         id: id
       });
     },
 
     update: function(id) {
       _assertPanelExists(id);
 
-      sendMessageToJava({
+      Messaging.sendRequest({
         type: "HomePanels:Update",
         panel: _generatePanel(id)
       });
     },
 
     setAuthenticated: function(id, isAuthenticated) {
       _assertPanelExists(id);
 
--- a/mobile/android/modules/HomeProvider.jsm
+++ b/mobile/android/modules/HomeProvider.jsm
@@ -295,17 +295,17 @@ function refreshDataset(datasetId) {
   if (gRefreshTimers[datasetId]) {
     return;
   }
 
   let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   timer.initWithCallback(function(timer) {
     delete gRefreshTimers[datasetId];
 
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "HomePanels:RefreshDataset",
       datasetId: datasetId
     });
   }, 100, Ci.nsITimer.TYPE_ONE_SHOT);
 
   gRefreshTimers[datasetId] = timer;
 }
 
--- a/mobile/android/modules/Messaging.jsm
+++ b/mobile/android/modules/Messaging.jsm
@@ -4,74 +4,51 @@
 "use strict"
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
-this.EXPORTED_SYMBOLS = ["sendMessageToJava", "RequestService"];
+this.EXPORTED_SYMBOLS = ["sendMessageToJava", "Messaging"];
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
 function sendMessageToJava(aMessage, aCallback) {
-  if (aCallback) {
-    let id = uuidgen.generateUUID().toString();
-    let obs = {
-      observe: function(aSubject, aTopic, aData) {
-        let data = JSON.parse(aData);
-        if (data.__guid__ != id) {
-          return;
-        }
-
-        Services.obs.removeObserver(obs, aMessage.type + ":Response", false);
+  Cu.reportError("sendMessageToJava is deprecated. Use Messaging API instead.");
 
-        if (data.status === "cancel") {
-          // No Java-side listeners handled our callback.
-          return;
-        }
-
-        aCallback(data.status === "success" ? data.response : null,
-                  data.status === "error"   ? data.response : null);
-      }
-    }
-
-    aMessage.__guid__ = id;
-    Services.obs.addObserver(obs, aMessage.type + ":Response", false);
-  }
-
-  return Services.androidBridge.handleGeckoMessage(aMessage);
+  Messaging.sendRequest(aMessage, aCallback);
 }
 
-let RequestService = {
+let Messaging = {
   /**
    * Add a listener for the given message.
    *
    * Only one request listener can be registered for a given message.
    *
    * Example usage:
-   *   RequestService.addListener({
+   *   Messaging.addListener({
    *     // aMessage is the message name.
    *     // aData is data sent from Java with the request.
    *     // The return value is used to respond to the request. The return
    *     //   type *must* be an instance of Object.
    *     onRequest: function (aMessage, aData) {
    *       if (aData == "foo") {
    *         return { response: "bar" };
    *       }
    *       return {};
    *     }
    *   }, "Demo:Request");
    *
    * The listener may also be a generator function, useful for performing a
    * task asynchronously. For example:
-   *   RequestService.addListener({
+   *   Messaging.addListener({
    *     onRequest: function* (aMessage, aData) {
    *       yield new Promise(resolve => setTimeout(resolve, 2000));
    *       return { response: "bar" };
    *     }
    *   }, "Demo:Request");
    *
    * @param aListener Listener object with an onRequest function (see example
    *                  usage above).
@@ -84,16 +61,51 @@ let RequestService = {
   /**
    * Removes a listener for a given message.
    *
    * @param aMessage The event to stop listening for.
    */
   removeListener: function (aMessage) {
     requestHandler.removeListener(aMessage);
   },
+
+  /**
+   * Sends a request to Java.
+   *
+   * @param aMessage  Message to send; must be an object with a "type" property
+   * @param aCallback Callback function, required if this request expects a response.
+   */
+  sendRequest: function (aMessage, aCallback) {
+    if (aCallback) {
+      let id = uuidgen.generateUUID().toString();
+      let obs = {
+        observe: function(aSubject, aTopic, aData) {
+          let data = JSON.parse(aData);
+          if (data.__guid__ != id) {
+            return;
+          }
+
+          Services.obs.removeObserver(obs, aMessage.type + ":Response", false);
+
+          if (data.status === "cancel") {
+            // No Java-side listeners handled our callback.
+            return;
+          }
+
+          aCallback(data.status === "success" ? data.response : null,
+                    data.status === "error"   ? data.response : null);
+        }
+      }
+
+      aMessage.__guid__ = id;
+      Services.obs.addObserver(obs, aMessage.type + ":Response", false);
+    }
+
+    return Services.androidBridge.handleGeckoMessage(aMessage);
+  },
 };
 
 let requestHandler = {
   _listeners: {},
 
   addListener: function (aListener, aMessage) {
     if (aMessage in this._listeners) {
       throw new Error("Error in addListener: A listener already exists for message " + aMessage);
@@ -130,14 +142,14 @@ let requestHandler = {
       if (typeof result !== "object" || result === null) {
         throw new Error("Gecko request listener did not return an object");
       }
       response = result;
     } catch (e) {
       Cu.reportError(e);
     }
 
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "Gecko:Request" + wrapper.id,
       response: response
     });
   })
 };
--- a/mobile/android/modules/NetErrorHelper.jsm
+++ b/mobile/android/modules/NetErrorHelper.jsm
@@ -96,17 +96,17 @@ handlers.wifi = {
     UITelemetry.addEvent("neterror.1", "button", "wifitoggle");
     // Show indeterminate progress while we wait for the network.
     node.disabled = true;
     node.classList.add("inProgress");
 
     this.node = Cu.getWeakReference(node);
     Services.obs.addObserver(this, "network:link-status-changed", true);
 
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "Wifi:Enable"
     });
   },
 
   observe: function(subject, topic, data) {
     let node = this.node.get();
     if (!node) {
       return;
--- a/mobile/android/modules/OrderedBroadcast.jsm
+++ b/mobile/android/modules/OrderedBroadcast.jsm
@@ -65,16 +65,16 @@ function sendOrderedBroadcast(action, to
       // This is called from within a notified observer, so we don't
       // need to take special pains to catch exceptions.
       theCallback(theData, theToken, theAction);
     },
   };
 
   Services.obs.addObserver(observer, responseEvent, false);
 
-  sendMessageToJava({
+  Messaging.sendRequest({
     type: "OrderedBroadcast:Send",
     action: action,
     responseEvent: responseEvent,
     token: { callbackId: callbackId, data: token || null },
     permission: permission,
   });
 };
--- a/mobile/android/modules/PageActions.jsm
+++ b/mobile/android/modules/PageActions.jsm
@@ -62,17 +62,17 @@ var PageActions = {
       if (this._items[aData].longClickCallback) {
         this._items[aData].longClickCallback();
       }
     }
   },
 
   add: function(aOptions) {
     let id = uuidgen.generateUUID().toString();
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "PageActions:Add",
       id: id,
       title: aOptions.title,
       icon: resolveGeckoURI(aOptions.icon),
       important: "important" in aOptions ? aOptions.important : false
     });
 
     this._items[id] = {
@@ -80,17 +80,17 @@ var PageActions = {
       longClickCallback: aOptions.longClickCallback
     };
 
     this._maybeInit();
     return id;
   },
 
   remove: function(id) {
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "PageActions:Remove",
       id: id
     });
 
     delete this._items[id];
     this._maybeUninit();
   }
 }
--- a/mobile/android/modules/SharedPreferences.jsm
+++ b/mobile/android/modules/SharedPreferences.jsm
@@ -71,17 +71,17 @@ function SharedPreferencesImpl(options =
   this._scope = options.scope;
   this._profileName = options.profileName;
   this._branch = options.branch;
   this._observers = {};
 }
 
 SharedPreferencesImpl.prototype = Object.freeze({
   _set: function _set(prefs) {
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "SharedPreferences:Set",
       preferences: prefs,
       scope: this._scope,
       profileName: this._profileName,
       branch: this._branch,
     });
   },
 
@@ -201,17 +201,17 @@ SharedPreferencesImpl.prototype = Object
   },
 
   _installAndroidListener: function _installAndroidListener() {
     if (this._listening)
       return;
     this._listening = true;
 
     Services.obs.addObserver(this, "SharedPreferences:Changed", false);
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "SharedPreferences:Observe",
       enable: true,
       scope: this._scope,
       profileName: this._profileName,
       branch: this._branch,
     });
   },
 
@@ -238,17 +238,17 @@ SharedPreferencesImpl.prototype = Object
   },
 
   _uninstallAndroidListener: function _uninstallAndroidListener() {
     if (!this._listening)
       return;
     this._listening = false;
 
     Services.obs.removeObserver(this, "SharedPreferences:Changed");
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "SharedPreferences:Observe",
       enable: false,
       scope: this._scope,
       profileName: this._profileName,
       branch: this._branch,
     });
   },
 });
--- a/mobile/android/modules/WebappManager.jsm
+++ b/mobile/android/modules/WebappManager.jsm
@@ -17,16 +17,17 @@ Cu.import("resource://gre/modules/NetUti
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
 Cu.import("resource://gre/modules/Webapps.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava", "resource://gre/modules/Messaging.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "Strings", function() {
   return Services.strings.createBundle("chrome://browser/locale/webapp.properties");
 });
 
 /**
@@ -188,33 +189,33 @@ this.WebappManager = {
         this._postInstall(aData.profilePath, aManifest, aData.app.origin,
                           aData.app.apkPackageName, aData.app.manifestURL);
       }).bind(this));
     });
   },
 
   _postInstall: function(aProfilePath, aNewManifest, aOrigin, aApkPackageName, aManifestURL) {
     // aOrigin may now point to the app: url that hosts this app.
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "Webapps:Postinstall",
       apkPackageName: aApkPackageName,
       origin: aOrigin,
     });
   },
 
   askUninstall: function(aData) {
     // Android does not currently support automatic uninstalling of apps.
     // See bug 1019054.
     DOMApplicationRegistry.denyUninstall(aData, "NOT_SUPPORTED");
   },
 
   launch: function({ apkPackageName }) {
     debug("launch: " + apkPackageName);
 
-    sendMessageToJava({
+    Messaging.sendRequest({
       type: "Webapps:Launch",
       packageName: apkPackageName,
     });
   },
 
   uninstall: Task.async(function*(aData, aMessageManager) {
     debug("uninstall: " + aData.manifestURL);
 
@@ -231,17 +232,17 @@ this.WebappManager = {
       throw new Error("app not found in registry");
     }
 
     // If the APK is installed, then _getAPKVersions will return a version
     // for it, so we can use that function to determine its install status.
     let apkVersions = yield this._getAPKVersions([ app.apkPackageName ]);
     if (app.apkPackageName in apkVersions) {
       debug("APK is installed; requesting uninstallation");
-      sendMessageToJava({
+      Messaging.sendRequest({
         type: "Webapps:UninstallApk",
         apkPackageName: app.apkPackageName,
       });
 
       // We don't need to call DOMApplicationRegistry.doUninstall at this point,
       // because the APK uninstall listener will call autoUninstall once the APK
       // is uninstalled; and if the user cancels the APK uninstallation, then we
       // shouldn't remove the app from the registry anyway.
--- a/mobile/android/search/java/org/mozilla/search/providers/SearchEngineManager.java
+++ b/mobile/android/search/java/org/mozilla/search/providers/SearchEngineManager.java
@@ -81,19 +81,25 @@ public class SearchEngineManager impleme
      * @param callback a SearchEngineCallback to be called after successfully looking
      *                 up the search engine. This will run on the UI thread.
      */
     private void getEngineFromPrefs(final SearchEngineCallback callback) {
         final AsyncTask<Void, Void, SearchEngine> task = new AsyncTask<Void, Void, SearchEngine>() {
             @Override
             protected SearchEngine doInBackground(Void... params) {
                 String identifier = GeckoSharedPrefs.forApp(context).getString(SearchPreferenceActivity.PREF_SEARCH_ENGINE_KEY, null);
-                if (TextUtils.isEmpty(identifier)) {
-                    identifier = context.getResources().getString(R.string.default_engine_identifier);
+                if (!TextUtils.isEmpty(identifier)) {
+                    try {
+                        return createEngine(identifier);
+                    } catch (IllegalArgumentException e) {
+                        Log.e(LOG_TAG, "Exception creating search engine from pref. Falling back to default engine.", e);
+                    }
                 }
+
+                identifier = context.getResources().getString(R.string.default_engine_identifier);
                 return createEngine(identifier);
             }
 
             @Override
             protected void onPostExecute(SearchEngine engine) {
                 // Only touch engine on the main thread.
                 SearchEngineManager.this.engine = engine;
                 if (callback != null) {
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -237,12 +237,13 @@ user_pref("browser.aboutHomeSnippets.upd
 user_pref("dom.mozApps.debug", true);
 
 // Don't fetch or send directory tiles data from real servers
 user_pref("browser.newtabpage.directory.source", 'data:application/json,{"testing":1}');
 user_pref("browser.newtabpage.directory.ping", "");
 
 // Enable Loop
 user_pref("loop.enabled", true);
+user_pref("loop.throttled", false);
 
 // Ensure UITour won't hit the network
 user_pref("browser.uitour.pinnedTabUrl", "http://%(server)s/uitour-dummy/pinnedTab");
 user_pref("browser.uitour.url", "http://%(server)s/uitour-dummy/tour");
--- a/toolkit/components/thumbnails/PageThumbsProtocol.js
+++ b/toolkit/components/thumbnails/PageThumbsProtocol.js
@@ -47,16 +47,17 @@ Protocol.prototype = {
    */
   get defaultPort() -1,
 
   /**
    * The flags specific to this protocol implementation.
    */
   get protocolFlags() {
     return Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD |
+           Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE |
            Ci.nsIProtocolHandler.URI_NORELATIVE |
            Ci.nsIProtocolHandler.URI_NOAUTH;
   },
 
   /**
    * Creates a new URI object that is suitable for loading by this protocol.
    * @param aSpec The URI string in UTF8 encoding.
    * @param aOriginCharset The charset of the document from which the URI originated.