merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Thu, 20 Nov 2014 11:36:19 +0100
changeset 240874 cc4e62b85d77b09c619097fdb3554bce111d1c13
parent 240866 38c5837fda9e7ff47930a6c5f614233ca75a0d75 (current diff)
parent 240873 94e9fc62355f9698506678269d25a83fdf5f757e (diff)
child 240901 6ce1b906c690fd60abfbf4c7dea60d9e405f0ad8
push id4311
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 19:37:41 +0000
treeherdermozilla-beta@150c9fed433b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone36.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -116,17 +116,17 @@ skip-if = os == "linux" || e10s # Bug 10
 skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
 [browser_action_searchengine_alias.js]
 skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
 [browser_addKeywordSearch.js]
 [browser_search_favicon.js]
 skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
 [browser_alltabslistener.js]
 [browser_autocomplete_a11y_label.js]
-skip-if = e10s # Bug ????? - no e10s switch-to-tab support yet
+skip-if = e10s # Bug 1101993 - times out for unknown reasons when run in the dir (works on its own)
 [browser_backButtonFitts.js]
 skip-if = os != "win" || e10s # The Fitts Law back button is only supported on Windows (bug 571454) / e10s - Bug 1099154: test touches content (attempts to add an event listener directly to the contentWindow)
 [browser_blob-channelname.js]
 [browser_bookmark_titles.js]
 skip-if = buildapp == 'mulet' || toolkit == "windows" || e10s # Disabled on Windows due to frequent failures (bugs 825739, 841341) / e10s - Bug 1094205 - places doesn't return the right thing in e10s mode, for some reason
 [browser_bug304198.js]
 skip-if = e10s
 [browser_bug321000.js]
@@ -153,55 +153,55 @@ skip-if = e10s # Bug 1093155 - tries to 
 skip-if = e10s # Bug 1056146 - zoom tests use FullZoomHelper and break in e10s
 [browser_bug422590.js]
 [browser_bug846489.js]
 [browser_bug423833.js]
 skip-if = true # bug 428712
 [browser_bug424101.js]
 skip-if = e10s # Bug 1093155 - tries to use context menu from browser-chrome and gets in a mess when in e10s mode
 [browser_bug427559.js]
-skip-if = e10s # Bug ?????? - "content window is focused - Got [object ChromeWindow], expected [object XrayWrapper [object Window]]"
+skip-if = e10s # Bug 1102015 - "content window is focused - Got [object ChromeWindow], expected [object CPOW [object Window]]"
 [browser_bug431826.js]
 [browser_bug432599.js]
 [browser_bug435035.js]
 [browser_bug435325.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 1099156 - test directly manipulates content
 [browser_bug441778.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 1056146 - zoom tests use FullZoomHelper and break in e10s
 [browser_bug455852.js]
 skip-if = e10s
 [browser_bug460146.js]
 skip-if = e10s # Bug 866413 - PageInfo doesn't work in e10s
 [browser_bug462289.js]
-skip-if = toolkit == "cocoa" || e10s # Bug ?????? - not sure why this is timing out and crashing!!
+skip-if = toolkit == "cocoa" || e10s # Bug 1102017 - middle-button mousedown on selected tab2 does not activate tab - Didn't expect [object XULElement], but got it
 [browser_bug462673.js]
 skip-if = e10s # Bug 1093404 - test expects sync window opening from content and is disappointed in that expectation
 [browser_bug477014.js]
 skip-if = e10s # Bug 1093206 - need to re-enable tests relying on swapFrameLoaders et al for e10s
 [browser_bug479408.js]
 skip-if = buildapp == 'mulet'
 [browser_bug481560.js]
-skip-if = e10s # Bug ????? - This bug attached an event listener directly to the content
+skip-if = e10s # Bug 1102018 - This bug attaches an event listener directly to the content, which then never gets called.
 [browser_bug484315.js]
 skip-if = e10s
 [browser_bug491431.js]
 skip-if = buildapp == 'mulet'
 [browser_bug495058.js]
 skip-if = e10s # Bug 1093206 - need to re-enable tests relying on swapFrameLoaders et al (and thus replaceTabWithWindow) for e10s
 [browser_bug517902.js]
 skip-if = e10s # Bug 866413 - PageInfo doesn't work in e10s
 [browser_bug519216.js]
 skip-if = e10s # Bug ?????? - some weird timing issue with progress listeners that fails intermittently
 [browser_bug520538.js]
 [browser_bug521216.js]
 [browser_bug533232.js]
 [browser_bug537013.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 1093206 - need to re-enable tests relying on swapFrameLoaders et al for e10s (test calls replaceTabWithWindow)
 [browser_bug537474.js]
-skip-if = e10s # Bug ?????? - test doesn't wait for document to be created before it checks it
+skip-if = e10s # Bug 1102020 - test tries to use browserDOMWindow.openURI to open a link, and gets a null rv where it expects a window
 [browser_bug550565.js]
 [browser_bug553455.js]
 skip-if = true # Bug 1094312
 #skip-if = buildapp == 'mulet' || e10s # Bug 1066070 - I don't think either popup notifications nor addon install stuff works on mulet? ; for e10s, indefinite waiting halfway through the test, tracked in bug 1093586
 [browser_bug555224.js]
 skip-if = e10s # Bug 1056146 - zoom tests use FullZoomHelper and break in e10s
 [browser_bug555767.js]
 skip-if = e10s # Bug 1093373 - relies on browser.sessionHistory
@@ -326,17 +326,17 @@ skip-if = buildapp == 'mulet' || e10s ||
 skip-if = e10s # Bug 863514 - no gesture support.
 [browser_getshortcutoruri.js]
 [browser_hide_removing.js]
 [browser_homeDrop.js]
 skip-if = buildapp == 'mulet'
 [browser_identity_UI.js]
 skip-if = e10s # Bug ?????? - this test fails for obscure reasons on non-windows builds only.
 [browser_keywordBookmarklets.js]
-skip-if = e10s # Bug ?????? - this test fails for obscure reasons on non-windows builds only.
+skip-if = e10s # Bug 1102025 - different principals for the bookmarklet only in e10s mode (unclear if test or 'real' issue)
 [browser_keywordSearch.js]
 skip-if = e10s # Bug 921957 - remote webprogress doesn't supply cancel method on the request object
 [browser_keywordSearch_postData.js]
 [browser_lastAccessedTab.js]
 skip-if = toolkit == "windows" # Disabled on Windows due to frequent failures (bug 969405)
 [browser_locationBarCommand.js]
 skip-if = os == "linux" || e10s # Linux: Intermittent failures, bug 917535; e10s: Bug 1094252 - Focus issues (There should be no focused element - Got [object XULElement], expected null)
 [browser_locationBarExternalLoad.js]
@@ -364,17 +364,17 @@ skip-if = asan # Disabled because it tak
 
 [browser_pinnedTabs.js]
 [browser_plainTextLinks.js]
 skip-if = e10s # Bug 1093155 - tries to use context menu from browser-chrome and gets in a mess when in e10s mode
 [browser_popupUI.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 1100707 - test fails in e10s because it can't get accel-w to close the popup (?)
 [browser_popup_blocker.js]
 [browser_printpreview.js]
-skip-if = buildapp == 'mulet' || e10s # Bug ?????? - timeout after logging "Error: Channel closing: too late to send/recv, messages will be lost"
+skip-if = buildapp == 'mulet' || e10s # Bug 1101973 - breaks the next test in e10s, and may be responsible for later timeout after logging "Error: Channel closing: too late to send/recv, messages will be lost"
 [browser_private_browsing_window.js]
 skip-if = buildapp == 'mulet'
 [browser_private_no_prompt.js]
 skip-if = buildapp == 'mulet'
 [browser_relatedTabs.js]
 [browser_remoteTroubleshoot.js]
 support-files =
   test_remoteTroubleshoot.html
--- a/browser/components/loop/LoopRooms.jsm
+++ b/browser/components/loop/LoopRooms.jsm
@@ -155,20 +155,16 @@ let LoopRoomsInternal = {
    */
   getAll: function(version = null, callback) {
     if (!callback) {
       callback = version;
       version = null;
     }
 
     Task.spawn(function* () {
-      let deferredInitialization = Promise.defer();
-      MozLoopService.delayedInitialize(deferredInitialization);
-      yield deferredInitialization.promise;
-
       if (!gDirty) {
         callback(null, [...this.rooms.values()]);
         return;
       }
 
       // Fetch the rooms from the server.
       let url = "/rooms" + (version ? "?version=" + encodeURIComponent(version) : "");
       let response = yield MozLoopService.hawkRequest(this.sessionType, url, "GET");
@@ -195,16 +191,22 @@ let LoopRoomsInternal = {
           eventEmitter.emit("update" + ":" + room.roomToken, room);
         } else {
           // Next, request the detailed information for each room. If the request
           // fails the room data will not be added to the map.
           yield LoopRooms.promise("get", room.roomToken);
         }
       }
 
+      // If there's no rooms in the list, remove the guest created room flag, so that
+      // we don't keep registering for guest when we don't need to.
+      if (this.sessionType == LOOP_SESSION_TYPE.GUEST && !this.rooms.size) {
+        this.setGuestCreatedRoom(false);
+      }
+
       // Set the 'dirty' flag back to FALSE, since the list is as fresh as can be now.
       gDirty = false;
       callback(null, [...this.rooms.values()]);
     }.bind(this)).catch(error => {
       callback(error);
     });
   },
 
@@ -264,21 +266,49 @@ let LoopRoomsInternal = {
     MozLoopService.hawkRequest(this.sessionType, "/rooms", "POST", room)
       .then(response => {
         let data = JSON.parse(response.body);
         extend(room, data);
         // Do not keep this value - it is a request to the server.
         delete room.expiresIn;
         this.rooms.set(room.roomToken, room);
 
+        if (this.sessionType == LOOP_SESSION_TYPE.GUEST) {
+          this.setGuestCreatedRoom(true);
+        }
+
         eventEmitter.emit("add", room);
         callback(null, room);
       }, error => callback(error)).catch(error => callback(error));
   },
 
+  /**
+   * Sets whether or not the user has created a room in guest mode.
+   *
+   * @param {Boolean} created If the user has created the room.
+   */
+  setGuestCreatedRoom: function(created) {
+    if (created) {
+      Services.prefs.setBoolPref("loop.createdRoom", created);
+    } else {
+      Services.prefs.clearUserPref("loop.createdRoom");
+    }
+  },
+
+  /**
+   * Returns true if the user has a created room in guest mode.
+   */
+  getGuestCreatedRoom: function() {
+    try {
+      return Services.prefs.getBoolPref("loop.createdRoom");
+    } catch (x) {
+      return false;
+    }
+  },
+
   open: function(roomToken) {
     let windowData = {
       roomToken: roomToken,
       type: "room"
     };
 
     MozLoopService.openChatWindow(windowData);
   },
@@ -481,16 +511,20 @@ this.LoopRooms = {
   leave: function(roomToken, sessionToken, callback) {
     return LoopRoomsInternal.leave(roomToken, sessionToken, callback);
   },
 
   rename: function(roomToken, newRoomName, callback) {
     return LoopRoomsInternal.rename(roomToken, newRoomName, callback);
   },
 
+  getGuestCreatedRoom: function() {
+    return LoopRoomsInternal.getGuestCreatedRoom();
+  },
+
   promise: function(method, ...params) {
     return new Promise((resolve, reject) => {
       this[method](...params, (error, result) => {
         if (error) {
           reject(error);
         } else {
           resolve(result);
         }
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -363,42 +363,16 @@ function injectLoopAPI(targetWindow) {
           callback(null, chosenButton == 0);
         } catch (ex) {
           callback(cloneValueInto(ex, targetWindow));
         }
       }
     },
 
     /**
-     * Call to ensure that any necessary registrations for the Loop Service
-     * have taken place.
-     *
-     * Callback parameters:
-     * - err null on successful registration, non-null otherwise.
-     *
-     * @param {LOOP_SESSION_TYPE} sessionType
-     * @param {Function} callback Will be called once registration is complete,
-     *                            or straight away if registration has already
-     *                            happened.
-     */
-    ensureRegistered: {
-      enumerable: true,
-      writable: true,
-      value: function(sessionType, callback) {
-        // We translate from a promise to a callback, as we can't pass promises from
-        // Promise.jsm across the priv versus unpriv boundary.
-        MozLoopService.promiseRegisteredWithServers(sessionType).then(() => {
-          callback(null);
-        }, err => {
-          callback(cloneValueInto(err, targetWindow));
-        }).catch(Cu.reportError);
-      }
-    },
-
-    /**
      * 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.
      *
      * This is used to determine whether or not we should be registering with the
      * push server on start.
      *
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -414,32 +414,32 @@ let MozLoopServiceInternal = {
       this.deferredRegistrations.delete(sessionType);
       log.debug("Cleared deferredRegistration for sessionType:", sessionType);
     });
 
     return result;
   },
 
   /**
-   * Performs a hawk based request to the loop server.
+   * Performs a hawk based request to the loop server - there is no pre-registration
+   * for this request, if this is required, use hawkRequest.
    *
    * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
    *                                        This is one of the LOOP_SESSION_TYPE members.
    * @param {String} path The path to make the request to.
    * @param {String} method The request method, e.g. 'POST', 'GET'.
    * @param {Object} payloadObj An object which is converted to JSON and
    *                            transmitted with the request.
    * @returns {Promise}
    *        Returns a promise that resolves to the response of the API call,
    *        or is rejected with an error.  If the server response can be parsed
    *        as JSON and contains an 'error' property, the promise will be
    *        rejected with this JSON-parsed response.
    */
-  hawkRequest: function(sessionType, path, method, payloadObj) {
-    log.debug("hawkRequest: " + path, sessionType);
+  hawkRequestInternal: function(sessionType, path, method, payloadObj) {
     if (!gHawkClient) {
       gHawkClient = new HawkClient(this.loopServerUri);
     }
 
     let sessionToken;
     try {
       sessionToken = Services.prefs.getCharPref(this.getSessionTokenPrefName(sessionType));
     } catch (x) {
@@ -475,16 +475,42 @@ let MozLoopServiceInternal = {
           this.setError("registration", error);
         }
       }
       throw error;
     });
   },
 
   /**
+   * Performs a hawk based request to the loop server, registering if necessary.
+   *
+   * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
+   *                                        This is one of the LOOP_SESSION_TYPE members.
+   * @param {String} path The path to make the request to.
+   * @param {String} method The request method, e.g. 'POST', 'GET'.
+   * @param {Object} payloadObj An object which is converted to JSON and
+   *                            transmitted with the request.
+   * @returns {Promise}
+   *        Returns a promise that resolves to the response of the API call,
+   *        or is rejected with an error.  If the server response can be parsed
+   *        as JSON and contains an 'error' property, the promise will be
+   *        rejected with this JSON-parsed response.
+   */
+  hawkRequest: function(sessionType, path, method, payloadObj) {
+    log.debug("hawkRequest: " + path, sessionType);
+    return new Promise((resolve, reject) => {
+      MozLoopService.promiseRegisteredWithServers(sessionType).then(() => {
+        this.hawkRequestInternal(sessionType, path, method, payloadObj).then(resolve, reject);
+      }, err => {
+        reject(err);
+      }).catch(reject);
+    });
+  },
+
+  /**
    * Generic hawkRequest onError handler for the hawkRequest promise.
    *
    * @param {Object} error - error reporting object
    *
    */
 
   _hawkRequestError: function(error) {
     log.error("Loop hawkRequest error:", error);
@@ -576,17 +602,17 @@ let MozLoopServiceInternal = {
     // that will register only the calls notification.
     let msg = {
         simplePushURL: callsPushURL,
         simplePushURLs: {
           calls: callsPushURL,
           rooms: roomsPushURL,
         },
     };
-    return this.hawkRequest(sessionType, "/registration", "POST", msg)
+    return this.hawkRequestInternal(sessionType, "/registration", "POST", msg)
       .then((response) => {
         // If this failed we got an invalid token.
         if (!this.storeSessionToken(sessionType, response.headers)) {
           return Promise.reject("session-token-wrong-size");
         }
 
         log.debug("Successfully registered with server for sessionType", sessionType);
         this.clearError("registration");
@@ -632,17 +658,17 @@ let MozLoopServiceInternal = {
    */
   unregisterFromLoopServer: function(sessionType, pushURL) {
     let prefType = Services.prefs.getPrefType(this.getSessionTokenPrefName(sessionType));
     if (prefType == Services.prefs.PREF_INVALID) {
       return Promise.resolve("already unregistered");
     }
 
     let unregisterURL = "/registration?simplePushURL=" + encodeURIComponent(pushURL);
-    return this.hawkRequest(sessionType, unregisterURL, "DELETE")
+    return this.hawkRequestInternal(sessionType, unregisterURL, "DELETE")
       .then(() => {
         log.debug("Successfully unregistered from server for sessionType", sessionType);
       },
       error => {
         if (error.code === 401) {
           // Authorization failed, invalid token. This is fine since it may mean we already logged out.
           return;
         }
@@ -821,17 +847,17 @@ let MozLoopServiceInternal = {
 
   /**
    * Fetch Firefox Accounts (FxA) OAuth parameters from the Loop Server.
    *
    * @return {Promise} resolved with the body of the hawk request for OAuth parameters.
    */
   promiseFxAOAuthParameters: function() {
     const SESSION_TYPE = LOOP_SESSION_TYPE.FXA;
-    return this.hawkRequest(SESSION_TYPE, "/fxa-oauth/params", "POST").then(response => {
+    return this.hawkRequestInternal(SESSION_TYPE, "/fxa-oauth/params", "POST").then(response => {
       if (!this.storeSessionToken(SESSION_TYPE, response.headers)) {
         throw new Error("Invalid FxA hawk token returned");
       }
       let prefType = Services.prefs.getPrefType(this.getSessionTokenPrefName(SESSION_TYPE));
       if (prefType == Services.prefs.PREF_INVALID) {
         throw new Error("No FxA hawk token returned and we don't have one saved");
       }
 
@@ -1021,16 +1047,17 @@ this.MozLoopService = {
       if (window) {
         window.LoopUI.playSound("room-joined");
       }
     });
 
     // If expiresTime is not in the future and the user hasn't
     // previously authenticated then skip registration.
     if (!MozLoopServiceInternal.urlExpiryTimeIsInFuture() &&
+        !LoopRooms.getGuestCreatedRoom() &&
         !MozLoopServiceInternal.fxAOAuthTokenData) {
       return Promise.resolve("registration not needed");
     }
 
     let deferredInitialization = Promise.defer();
     gInitializeTimerFunc(deferredInitialization);
 
     return deferredInitialization.promise;
@@ -1054,17 +1081,18 @@ this.MozLoopService = {
     error => {
       // If we get a non-object then setError was already called for a different error type.
       if (typeof(error) == "object") {
         MozLoopServiceInternal.setError("initialization", error, () => MozLoopService.delayedInitialize(Promise.defer()));
       }
     });
 
     try {
-      if (MozLoopServiceInternal.urlExpiryTimeIsInFuture()) {
+      if (MozLoopServiceInternal.urlExpiryTimeIsInFuture() ||
+          LoopRooms.getGuestCreatedRoom()) {
         yield this.promiseRegisteredWithServers(LOOP_SESSION_TYPE.GUEST);
       } else {
         log.debug("delayedInitialize: URL expiry time isn't in the future so not registering as a guest");
       }
     } catch (ex) {
       log.debug("MozLoopService: Failure of guest registration", ex);
       deferredInitialization.reject(ex);
       yield completedPromise;
--- a/browser/components/loop/content/js/client.js
+++ b/browser/components/loop/content/js/client.js
@@ -77,50 +77,39 @@ loop.Client = (function($) {
      */
     _failureHandler: function(cb, error) {
       var message = "HTTP " + error.code + " " + error.error + "; " + error.message;
       console.error(message);
       cb(error);
     },
 
     /**
-     * Ensures the client is registered with the push server.
+     * Requests a call URL from the Loop server. It will note the
+     * expiry time for the url with the mozLoop api.  It will select the
+     * appropriate hawk session to use based on whether or not the user
+     * is currently logged into a Firefox account profile.
      *
      * Callback parameters:
-     * - err null on successful registration, non-null otherwise.
-     *
-     * @param {LOOP_SESSION_TYPE} sessionType Guest or FxA
-     * @param {Function} cb Callback(err)
-     */
-    _ensureRegistered: function(sessionType, cb) {
-      this.mozLoop.ensureRegistered(sessionType, function(error) {
-        if (error) {
-          console.log("Error registering with Loop server, code: " + error);
-          cb(error);
-          return;
-        } else {
-          cb(null);
-        }
-      });
-    },
-
-    /**
-     * Internal handler for requesting a call url from the server.
-     *
-     * Callback parameters:
-     * - err null on successful registration, non-null otherwise.
+     * - err null on successful request, non-null otherwise.
      * - callUrlData an object of the obtained call url data if successful:
      * -- callUrl: The url of the call
      * -- expiresAt: The amount of hours until expiry of the url
      *
-     * @param {LOOP_SESSION_TYPE} sessionType
+     * @param  {String} simplepushUrl a registered Simple Push URL
      * @param  {string} nickname the nickname of the future caller
      * @param  {Function} cb Callback(err, callUrlData)
      */
-    _requestCallUrlInternal: function(sessionType, nickname, cb) {
+    requestCallUrl: function(nickname, cb) {
+      var sessionType;
+      if (this.mozLoop.userProfile) {
+        sessionType = this.mozLoop.LOOP_SESSION_TYPE.FXA;
+      } else {
+        sessionType = this.mozLoop.LOOP_SESSION_TYPE.GUEST;
+      }
+
       this.mozLoop.hawkRequest(sessionType, "/call-url/", "POST",
                                {callerId: nickname},
         function (error, responseText) {
           if (error) {
             this._telemetryAdd("LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS", false);
             this._failureHandler(cb, error);
             return;
           }
@@ -149,27 +138,16 @@ loop.Client = (function($) {
      * @param {mozLoop.LOOP_SESSION_TYPE} sessionType The type of session which
      *                                                the url belongs to.
      * @param {function} cb Callback function used for handling an error
      *                      response. XXX The incoming call panel does not
      *                      exist after the block button is clicked therefore
      *                      it does not make sense to display an error.
      **/
     deleteCallUrl: function(token, sessionType, cb) {
-      this._ensureRegistered(sessionType, function(err) {
-        if (err) {
-          cb(err);
-          return;
-        }
-
-        this._deleteCallUrlInternal(token, sessionType, cb);
-      }.bind(this));
-    },
-
-    _deleteCallUrlInternal: function(token, sessionType, cb) {
       function deleteRequestCallback(error, responseText) {
         if (error) {
           this._failureHandler(cb, error);
           return;
         }
 
         try {
           cb(null);
@@ -180,50 +158,16 @@ loop.Client = (function($) {
       }
 
       this.mozLoop.hawkRequest(sessionType,
                                "/call-url/" + token, "DELETE", null,
                                deleteRequestCallback.bind(this));
     },
 
     /**
-     * Requests a call URL from the Loop server. It will note the
-     * expiry time for the url with the mozLoop api.  It will select the
-     * appropriate hawk session to use based on whether or not the user
-     * is currently logged into a Firefox account profile.
-     *
-     * Callback parameters:
-     * - err null on successful registration, non-null otherwise.
-     * - callUrlData an object of the obtained call url data if successful:
-     * -- callUrl: The url of the call
-     * -- expiresAt: The amount of hours until expiry of the url
-     *
-     * @param  {String} simplepushUrl a registered Simple Push URL
-     * @param  {string} nickname the nickname of the future caller
-     * @param  {Function} cb Callback(err, callUrlData)
-     */
-    requestCallUrl: function(nickname, cb) {
-      var sessionType;
-      if (this.mozLoop.userProfile) {
-        sessionType = this.mozLoop.LOOP_SESSION_TYPE.FXA;
-      } else {
-        sessionType = this.mozLoop.LOOP_SESSION_TYPE.GUEST;
-      }
-
-      this._ensureRegistered(sessionType, function(err) {
-        if (err) {
-          cb(err);
-          return;
-        }
-
-        this._requestCallUrlInternal(sessionType, nickname, cb);
-      }.bind(this));
-    },
-
-    /**
      * Sets up an outgoing call, getting the relevant data from the server.
      *
      * Callback parameters:
      * - err null on successful registration, non-null otherwise.
      * - result an object of the obtained data for starting the call, if successful
      *
      * @param {Array} calleeIds an array of emails and phone numbers.
      * @param {String} callType the type of call.
--- a/browser/components/loop/test/desktop-local/client_test.js
+++ b/browser/components/loop/test/desktop-local/client_test.js
@@ -27,17 +27,16 @@ describe("loop.Client", function() {
     sandbox = sinon.sandbox.create();
     callback = sinon.spy();
     fakeToken = "fakeTokenText";
     mozLoop = {
       getLoopPref: sandbox.stub()
         .returns(null)
         .withArgs("hawk-session-token")
         .returns(fakeToken),
-      ensureRegistered: sinon.stub().callsArgWith(1, null),
       noteCallUrlExpiry: sinon.spy(),
       hawkRequest: sinon.stub(),
       LOOP_SESSION_TYPE: {
         GUEST: 1,
         FXA: 2
       },
       userProfile: null,
       telemetryAdd: sinon.spy()
@@ -50,31 +49,16 @@ describe("loop.Client", function() {
   });
 
   afterEach(function() {
     sandbox.restore();
   });
 
   describe("loop.Client", function() {
     describe("#deleteCallUrl", function() {
-      it("should ensure loop is registered", function() {
-        client.deleteCallUrl("fakeToken", mozLoop.LOOP_SESSION_TYPE.FXA, callback);
-
-        sinon.assert.calledOnce(mozLoop.ensureRegistered);
-      });
-
-      it("should send an error when registration fails", function() {
-        mozLoop.ensureRegistered.callsArgWith(1, "offline");
-
-        client.deleteCallUrl("fakeToken", mozLoop.LOOP_SESSION_TYPE.FXA, callback);
-
-        sinon.assert.calledOnce(callback);
-        sinon.assert.calledWithExactly(callback, "offline");
-      });
-
       it("should make a delete call to /call-url/{fakeToken}", function() {
         client.deleteCallUrl(fakeToken, mozLoop.LOOP_SESSION_TYPE.GUEST, callback);
 
         sinon.assert.calledOnce(hawkRequestStub);
         sinon.assert.calledWith(hawkRequestStub,
                                 mozLoop.LOOP_SESSION_TYPE.GUEST,
                                 "/call-url/" + fakeToken, "DELETE");
       });
@@ -101,31 +85,16 @@ describe("loop.Client", function() {
         sinon.assert.calledOnce(callback);
         sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
           return err.code == 400 && "invalid token" == err.message;
         }));
       });
     });
 
     describe("#requestCallUrl", function() {
-      it("should ensure loop is registered", function() {
-        client.requestCallUrl("foo", callback);
-
-        sinon.assert.calledOnce(mozLoop.ensureRegistered);
-      });
-
-      it("should send an error when registration fails", function() {
-        mozLoop.ensureRegistered.callsArgWith(1, "offline");
-
-        client.requestCallUrl("foo", callback);
-
-        sinon.assert.calledOnce(callback);
-        sinon.assert.calledWithExactly(callback, "offline");
-      });
-
       it("should post to /call-url/", function() {
         client.requestCallUrl("foo", callback);
 
         sinon.assert.calledOnce(hawkRequestStub);
         sinon.assert.calledWithExactly(hawkRequestStub, sinon.match.number,
           "/call-url/", "POST", {callerId: "foo"}, sinon.match.func);
       });
 
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -546,53 +546,54 @@ describe("loop.conversation", function()
 
           sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
           sinon.assert.calledWithExactly(
             navigator.mozLoop.calls.clearCallInProgress, "8699");
         });
       });
 
       describe("#blocked", function() {
-        var mozLoop;
+        var mozLoop, deleteCallUrlStub;
 
         beforeEach(function() {
           icView = mountTestComponent();
 
           icView._websocket = {
             decline: sinon.spy(),
             close: sinon.stub()
           };
           sandbox.stub(window, "close");
 
           mozLoop = {
             LOOP_SESSION_TYPE: {
               GUEST: 1,
               FXA: 2
             }
           };
+
+          deleteCallUrlStub = sandbox.stub(loop.Client.prototype,
+                                           "deleteCallUrl");
         });
 
         it("should call mozLoop.stopAlerting", function() {
           icView.declineAndBlock();
 
           sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
         });
 
         it("should call delete call", function() {
           sandbox.stub(conversation, "get").withArgs("callToken")
                                            .returns("fakeToken")
                                            .withArgs("sessionType")
                                            .returns(mozLoop.LOOP_SESSION_TYPE.FXA);
 
-          var deleteCallUrl = sandbox.stub(loop.Client.prototype,
-                                           "deleteCallUrl");
           icView.declineAndBlock();
 
-          sinon.assert.calledOnce(deleteCallUrl);
-          sinon.assert.calledWithExactly(deleteCallUrl,
+          sinon.assert.calledOnce(deleteCallUrlStub);
+          sinon.assert.calledWithExactly(deleteCallUrlStub,
             "fakeToken", mozLoop.LOOP_SESSION_TYPE.FXA, sinon.match.func);
         });
 
         it("should get callToken from conversation model", function() {
           sandbox.stub(conversation, "get");
           icView.declineAndBlock();
 
           sinon.assert.called(conversation.get);
@@ -601,19 +602,17 @@ describe("loop.conversation", function()
         });
 
         it("should trigger error handling in case of error", function() {
           // XXX just logging to console for now
           var log = sandbox.stub(console, "log");
           var fakeError = {
             error: true
           };
-          sandbox.stub(loop.Client.prototype, "deleteCallUrl", function(_, __, cb) {
-            cb(fakeError);
-          });
+          deleteCallUrlStub.callsArgWith(2, fakeError);
           icView.declineAndBlock();
 
           sinon.assert.calledOnce(log);
           sinon.assert.calledWithExactly(log, fakeError);
         });
 
         it("should close the window", function() {
           icView.declineAndBlock();
--- a/browser/components/loop/test/xpcshell/test_loopservice_hawk_errors.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_hawk_errors.js
@@ -32,17 +32,17 @@ add_task(function* setup_server() {
   loopServer.registerPathHandler("/401", errorRequestHandler);
   loopServer.registerPathHandler("/404", errorRequestHandler);
   loopServer.registerPathHandler("/500", errorRequestHandler);
   loopServer.registerPathHandler("/503", errorRequestHandler);
 });
 
 add_task(function* error_offline() {
   Services.io.offline = true;
-  yield MozLoopService.hawkRequest(LOOP_SESSION_TYPE.GUEST, "/offline", "GET").then(
+  yield MozLoopServiceInternal.hawkRequestInternal(LOOP_SESSION_TYPE.GUEST, "/offline", "GET").then(
     () => Assert.ok(false, "Should have rejected"),
     (error) => {
       MozLoopServiceInternal.setError("testing", error);
       Assert.strictEqual(MozLoopService.errors.size, 1, "Should be one error");
 
       // Network errors are converted to the "network" errorType.
       let err = MozLoopService.errors.get("network");
       Assert.strictEqual(err.code, null);
@@ -53,17 +53,17 @@ add_task(function* error_offline() {
   Services.io.offline = false;
 });
 
 add_task(cleanup_between_tests);
 
 add_task(function* guest_401() {
   Services.prefs.setCharPref("loop.hawk-session-token", "guest");
   Services.prefs.setCharPref("loop.hawk-session-token.fxa", "fxa");
-  yield MozLoopService.hawkRequest(LOOP_SESSION_TYPE.GUEST, "/401", "POST").then(
+  yield MozLoopServiceInternal.hawkRequestInternal(LOOP_SESSION_TYPE.GUEST, "/401", "POST").then(
     () => Assert.ok(false, "Should have rejected"),
     (error) => {
       Assert.strictEqual(Services.prefs.getPrefType("loop.hawk-session-token"),
                          Services.prefs.PREF_INVALID,
                          "Guest session token should have been cleared");
       Assert.strictEqual(Services.prefs.getCharPref("loop.hawk-session-token.fxa"),
                          "fxa",
                          "FxA session token should NOT have been cleared");
@@ -78,17 +78,17 @@ add_task(function* guest_401() {
   });
 });
 
 add_task(cleanup_between_tests);
 
 add_task(function* fxa_401() {
   Services.prefs.setCharPref("loop.hawk-session-token", "guest");
   Services.prefs.setCharPref("loop.hawk-session-token.fxa", "fxa");
-  yield MozLoopService.hawkRequest(LOOP_SESSION_TYPE.FXA, "/401", "POST").then(
+  yield MozLoopServiceInternal.hawkRequestInternal(LOOP_SESSION_TYPE.FXA, "/401", "POST").then(
     () => Assert.ok(false, "Should have rejected"),
     (error) => {
       Assert.strictEqual(Services.prefs.getCharPref("loop.hawk-session-token"),
                          "guest",
                          "Guest session token should NOT have been cleared");
       Assert.strictEqual(Services.prefs.getPrefType("loop.hawk-session-token.fxa"),
                          Services.prefs.PREF_INVALID,
                          "Fxa session token should have been cleared");
@@ -100,68 +100,68 @@ add_task(function* fxa_401() {
       Assert.strictEqual(err.friendlyDetails, getLoopString("password_changed_question"));
       Assert.strictEqual(err.friendlyDetailsButtonLabel, getLoopString("retry_button"));
   });
 });
 
 add_task(cleanup_between_tests);
 
 add_task(function* error_404() {
-  yield MozLoopService.hawkRequest(LOOP_SESSION_TYPE.GUEST, "/404", "GET").then(
+  yield MozLoopServiceInternal.hawkRequestInternal(LOOP_SESSION_TYPE.GUEST, "/404", "GET").then(
     () => Assert.ok(false, "Should have rejected"),
     (error) => {
       MozLoopServiceInternal.setError("testing", error);
       Assert.strictEqual(MozLoopService.errors.size, 1, "Should be one error");
 
       let err = MozLoopService.errors.get("testing");
       Assert.strictEqual(err.code, 404);
       Assert.strictEqual(err.friendlyMessage, getLoopString("generic_failure_title"));
       Assert.equal(err.friendlyDetails, null);
       Assert.equal(err.friendlyDetailsButtonLabel, null);
   });
 });
 
 add_task(cleanup_between_tests);
 
 add_task(function* error_500() {
-  yield MozLoopService.hawkRequest(LOOP_SESSION_TYPE.GUEST, "/500", "GET").then(
+  yield MozLoopServiceInternal.hawkRequestInternal(LOOP_SESSION_TYPE.GUEST, "/500", "GET").then(
     () => Assert.ok(false, "Should have rejected"),
     (error) => {
       MozLoopServiceInternal.setError("testing", error);
       Assert.strictEqual(MozLoopService.errors.size, 1, "Should be one error");
 
       let err = MozLoopService.errors.get("testing");
       Assert.strictEqual(err.code, 500);
       Assert.strictEqual(err.friendlyMessage, getLoopString("service_not_available"));
       Assert.strictEqual(err.friendlyDetails, getLoopString("try_again_later"));
       Assert.strictEqual(err.friendlyDetailsButtonLabel, getLoopString("retry_button"));
   });
 });
 
 add_task(cleanup_between_tests);
 
 add_task(function* profile_500() {
-  yield MozLoopService.hawkRequest(LOOP_SESSION_TYPE.GUEST, "/500", "GET").then(
+  yield MozLoopServiceInternal.hawkRequestInternal(LOOP_SESSION_TYPE.GUEST, "/500", "GET").then(
     () => Assert.ok(false, "Should have rejected"),
     (error) => {
       MozLoopServiceInternal.setError("profile", error);
       Assert.strictEqual(MozLoopService.errors.size, 1, "Should be one error");
 
       let err = MozLoopService.errors.get("profile");
       Assert.strictEqual(err.code, 500);
       Assert.strictEqual(err.friendlyMessage, getLoopString("problem_accessing_account"));
       Assert.equal(err.friendlyDetails, null);
       Assert.equal(err.friendlyDetailsButtonLabel, null);
   });
 });
 
 add_task(cleanup_between_tests);
 
 add_task(function* error_503() {
-  yield MozLoopService.hawkRequest(LOOP_SESSION_TYPE.GUEST, "/503", "GET").then(
+  yield MozLoopServiceInternal.hawkRequestInternal(LOOP_SESSION_TYPE.GUEST, "/503", "GET").then(
     () => Assert.ok(false, "Should have rejected"),
     (error) => {
       MozLoopServiceInternal.setError("testing", error);
       Assert.strictEqual(MozLoopService.errors.size, 1, "Should be one error");
 
       let err = MozLoopService.errors.get("testing");
       Assert.strictEqual(err.code, 503);
       Assert.strictEqual(err.friendlyMessage, getLoopString("service_not_available"));
--- a/browser/installer/windows/nsis/defines.nsi.in
+++ b/browser/installer/windows/nsis/defines.nsi.in
@@ -87,18 +87,20 @@ VIAddVersionKey "LegalTrademarks" "${Bra
 #endif
 VIAddVersionKey "LegalCopyright"  "${CompanyName}"
 VIAddVersionKey "FileVersion"     "${AppVersion}"
 VIAddVersionKey "ProductVersion"  "${AppVersion}"
 # Comments is not used but left below commented out for future reference
 # VIAddVersionKey "Comments"        "Comments"
 
 # It isn't possible to get the size of the installation prior to downloading
-# so the stub installer uses an estimate.
-!define APPROXIMATE_REQUIRED_SPACE_MB "42.2"
+# so the stub installer uses an estimate. The size is derived from the size of
+# the complete installer, the size of the extracted complete installer, and at
+# least 15 MB additional for working room.
+!define APPROXIMATE_REQUIRED_SPACE_MB "145"
 
 # Control positions in Dialog Units so they are placed correctly with
 # non-default DPI settings
 !define OPTIONS_ITEM_EDGE_DU 90u
 !define OPTIONS_ITEM_WIDTH_DU 356u
 !define OPTIONS_SUBITEM_EDGE_DU 119u
 !define OPTIONS_SUBITEM_WIDTH_DU 327u
 !define INSTALL_BLURB_TOP_DU 78u
--- a/browser/installer/windows/nsis/stub.nsi
+++ b/browser/installer/windows/nsis/stub.nsi
@@ -1742,54 +1742,34 @@ Function UpdateFreeSpaceLabel
   Call CheckSpace
 
   StrCpy $0 "$SpaceAvailableBytes"
 
   StrCpy $1 "$(BYTE)"
 
   ${If} $0 > 1024
   ${OrIf} $0 < 0
-    ; Multiply by 10 so it is possible to display a decimal in the size
-    System::Int64Op $0 * 10
-    Pop $0
     System::Int64Op $0 / 1024
     Pop $0
     StrCpy $1 "$(KILO)$(BYTE)"
-    ${If} $0 > 10240
+    ${If} $0 > 1024
     ${OrIf} $0 < 0
       System::Int64Op $0 / 1024
       Pop $0
       StrCpy $1 "$(MEGA)$(BYTE)"
-      ${If} $0 > 10240
+      ${If} $0 > 1024
       ${OrIf} $0 < 0
         System::Int64Op $0 / 1024
         Pop $0
         StrCpy $1 "$(GIGA)$(BYTE)"
       ${EndIf}
     ${EndIf}
-    StrLen $3 "$0"
-    ${If} $3 > 1
-      StrCpy $2 "$0" -1 ; All characters except the last one
-      StrCpy $0 "$0" "" -1 ; The last character
-      ${If} "$0" == "0"
-        StrCpy $0 "$2" ; Don't display the decimal if it is 0
-      ${Else}
-        StrCpy $0 "$2.$0"
-      ${EndIf}
-    ${ElseIf} $3 == 1
-      StrCpy $0 "0.$0"
-    ${Else}
-      ; This should never happen
-      System::Int64Op $0 / 10
-      Pop $0
-    ${EndIf}
   ${EndIf}
 
   SendMessage $LabelFreeSpace ${WM_SETTEXT} 0 "STR:$0 $1"
-
 FunctionEnd
 
 Function OnChange_DirRequest
   Pop $0
   System::Call 'user32::GetWindowTextW(i $DirRequest, w .r0, i ${NSIS_MAX_STRLEN})'
   StrCpy $1 "$0" 1 ; the first character
   ${If} "$1" == "$\""
     StrCpy $1 "$0" "" -1 ; the last character
new file mode 100644
--- /dev/null
+++ b/js/ipc/CPOWTimer.cpp
@@ -0,0 +1,18 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=4 sw=4 et tw=80:
+ *
+ * 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/. */
+
+#include "jsfriendapi.h"
+#include "xpcprivate.h"
+#include "CPOWTimer.h"
+
+CPOWTimer::~CPOWTimer() {
+    /* This is a best effort to find the compartment responsible for this CPOW call */
+    xpc::CompartmentPrivate* compartment = xpc::CompartmentPrivate::Get(js::GetObjectCompartment(mozilla::dom::GetIncumbentGlobal()
+                                                                                                 ->GetGlobalJSObject()));
+    PRIntervalTime time = PR_IntervalNow() - startInterval;
+    compartment->CPOWTime += time;
+}
new file mode 100644
--- /dev/null
+++ b/js/ipc/CPOWTimer.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=4 sw=4 et tw=80:
+ *
+ * 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/. */
+
+#ifndef CPOWTIMER_H
+#define CPOWTIMER_H
+
+#include "prinrval.h"
+
+class JSObject;
+
+class MOZ_STACK_CLASS CPOWTimer {
+  public:
+    CPOWTimer(): startInterval(PR_IntervalNow()) {}
+    ~CPOWTimer();
+
+  private:
+    PRIntervalTime startInterval;
+};
+
+#endif
--- a/js/ipc/WrapperOwner.cpp
+++ b/js/ipc/WrapperOwner.cpp
@@ -6,16 +6,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "WrapperOwner.h"
 #include "JavaScriptLogging.h"
 #include "mozilla/unused.h"
 #include "mozilla/dom/BindingUtils.h"
 #include "jsfriendapi.h"
 #include "xpcprivate.h"
+#include "CPOWTimer.h"
 #include "WrapperFactory.h"
 
 #include "nsIRemoteTagService.h"
 
 using namespace js;
 using namespace JS;
 using namespace mozilla;
 using namespace mozilla::jsipc;
@@ -129,17 +130,20 @@ const char CPOWProxyHandler::family = 0;
 const CPOWProxyHandler CPOWProxyHandler::singleton;
 
 #define FORWARD(call, args)                                             \
     WrapperOwner *owner = OwnerOf(proxy);                               \
     if (!owner->active()) {                                             \
         JS_ReportError(cx, "cannot use a CPOW whose process is gone");  \
         return false;                                                   \
     }                                                                   \
-    return owner->call args;
+    {                                                                   \
+        CPOWTimer timer;                                                \
+        return owner->call args;                                        \
+    }
 
 bool
 CPOWProxyHandler::getPropertyDescriptor(JSContext *cx, HandleObject proxy, HandleId id,
                                         MutableHandle<JSPropertyDescriptor> desc) const
 {
     FORWARD(getPropertyDescriptor, (cx, proxy, id, desc));
 }
 
--- a/js/ipc/moz.build
+++ b/js/ipc/moz.build
@@ -1,15 +1,16 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 UNIFIED_SOURCES += [
+    'CPOWTimer.cpp',
     'JavaScriptChild.cpp',
     'JavaScriptParent.cpp',
     'JavaScriptShared.cpp',
     'WrapperAnswer.cpp',
     'WrapperOwner.cpp',
 ]
 
 IPDL_SOURCES += [
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -3624,16 +3624,17 @@ public:
     };
 
     explicit CompartmentPrivate(JSCompartment *c)
         : wantXrays(false)
         , writeToGlobalPrototype(false)
         , skipWriteToGlobalPrototype(false)
         , universalXPConnectEnabled(false)
         , forcePermissiveCOWs(false)
+        , CPOWTime(0)
         , skipCOWCallableChecks(false)
         , scriptability(c)
         , scope(nullptr)
     {
         MOZ_COUNT_CTOR(xpc::CompartmentPrivate);
         mozilla::PodArrayZero(wrapperDenialWarnings);
     }
 
@@ -3677,16 +3678,19 @@ public:
     // This is only ever set during mochitest runs when enablePrivilege is called.
     // It allows the SpecialPowers scope to waive the normal chrome security
     // wrappers and expose properties directly to content. This lets us avoid a
     // bunch of overhead and complexity in our SpecialPowers automation glue.
     //
     // Using it in production is inherently unsafe.
     bool forcePermissiveCOWs;
 
+    // A running count of how much time we've spent processing CPOWs.
+    PRIntervalTime               CPOWTime;
+
     // Disables the XPConnect security checks that deny access to callables and
     // accessor descriptors on COWs. Do not use this unless you are bholley.
     bool skipCOWCallableChecks;
 
     // Whether we've emitted a warning about a property that was filtered out
     // by a security wrapper. See XrayWrapper.cpp.
     bool wrapperDenialWarnings[WrapperDenialTypeCount];
 
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -2732,17 +2732,17 @@ public class BrowserApp extends GeckoApp
 
             return true;
         }
 
         bookmark.setEnabled(!AboutPages.isAboutReader(tab.getURL()));
         bookmark.setVisible(!GeckoProfile.get(this).inGuestMode());
         bookmark.setCheckable(true);
         bookmark.setChecked(tab.isBookmark());
-        bookmark.setIcon(tab.isBookmark() ? R.drawable.ic_menu_bookmark_remove : R.drawable.ic_menu_bookmark_add);
+        bookmark.setIcon(resolveBookmarkIconID(tab.isBookmark()));
 
         back.setEnabled(tab.canDoBack());
         forward.setEnabled(tab.canDoForward());
         desktopMode.setChecked(tab.getDesktopMode());
         desktopMode.setIcon(tab.getDesktopMode() ? R.drawable.ic_menu_desktop_mode_on : R.drawable.ic_menu_desktop_mode_off);
 
         String url = tab.getURL();
         if (AboutPages.isAboutReader(url)) {
@@ -2831,16 +2831,32 @@ public class BrowserApp extends GeckoApp
             exitGuestMode.setVisible(true);
         } else {
             enterGuestMode.setVisible(true);
         }
 
         return true;
     }
 
+    private int resolveBookmarkIconID(final boolean isBookmark) {
+        if (NewTabletUI.isEnabled(this) && HardwareUtils.isLargeTablet()) {
+            if (isBookmark) {
+                return R.drawable.new_tablet_ic_menu_bookmark_remove;
+            } else {
+                return R.drawable.new_tablet_ic_menu_bookmark_add;
+            }
+        }
+
+        if (isBookmark) {
+            return R.drawable.ic_menu_bookmark_remove;
+        } else {
+            return R.drawable.ic_menu_bookmark_add;
+        }
+    }
+
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         Tab tab = null;
         Intent intent = null;
 
         final int itemId = item.getItemId();
 
         // Track the menu action. We don't know much about the context, but we can use this to determine
@@ -2852,21 +2868,21 @@ public class BrowserApp extends GeckoApp
         }
 
         if (itemId == R.id.bookmark) {
             tab = Tabs.getInstance().getSelectedTab();
             if (tab != null) {
                 if (item.isChecked()) {
                     Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.MENU, "bookmark");
                     tab.removeBookmark();
-                    item.setIcon(R.drawable.ic_menu_bookmark_add);
+                    item.setIcon(resolveBookmarkIconID(false));
                 } else {
                     Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, "bookmark");
                     tab.addBookmark();
-                    item.setIcon(R.drawable.ic_menu_bookmark_remove);
+                    item.setIcon(resolveBookmarkIconID(true));
                 }
             }
             return true;
         }
 
         if (itemId == R.id.share) {
             shareCurrentUrl();
             return true;
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -223,16 +223,17 @@ public class GeckoAppShell
     private static Sensor gAccelerometerSensor;
     private static Sensor gLinearAccelerometerSensor;
     private static Sensor gGyroscopeSensor;
     private static Sensor gOrientationSensor;
     private static Sensor gProximitySensor;
     private static Sensor gLightSensor;
 
     private static final String GECKOREQUEST_RESPONSE_KEY = "response";
+    private static final String GECKOREQUEST_ERROR_KEY = "error";
 
     /*
      * Keep in sync with constants found here:
      * http://mxr.mozilla.org/mozilla-central/source/uriloader/base/nsIWebProgressListener.idl
     */
     static public final int WPL_STATE_START = 0x00000001;
     static public final int WPL_STATE_STOP = 0x00000010;
     static public final int WPL_STATE_IS_DOCUMENT = 0x00020000;
@@ -429,17 +430,17 @@ public class GeckoAppShell
     public static void sendRequestToGecko(final GeckoRequest request) {
         final String responseMessage = "Gecko:Request" + request.getId();
 
         EventDispatcher.getInstance().registerGeckoThreadListener(new NativeEventListener() {
             @Override
             public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
                 EventDispatcher.getInstance().unregisterGeckoThreadListener(this, event);
                 if (!message.has(GECKOREQUEST_RESPONSE_KEY)) {
-                    request.onError();
+                    request.onError(message.getObject(GECKOREQUEST_ERROR_KEY));
                     return;
                 }
                 request.onResponse(message.getObject(GECKOREQUEST_RESPONSE_KEY));
             }
         }, responseMessage);
 
         sendEventToGecko(GeckoEvent.createBroadcastEvent(request.getName(), request.getData()));
     }
--- a/mobile/android/base/menu/GeckoMenuInflater.java
+++ b/mobile/android/base/menu/GeckoMenuInflater.java
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.menu;
 
 import java.io.IOException;
 
 import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.NewTabletUI;
 import org.mozilla.gecko.R;
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
@@ -127,22 +128,30 @@ public class GeckoMenuInflater extends M
         item.title = a.getText(R.styleable.MenuItem_android_title);
         item.checkable = a.getBoolean(R.styleable.MenuItem_android_checkable, false);
         item.checked = a.getBoolean(R.styleable.MenuItem_android_checked, false);
         item.visible = a.getBoolean(R.styleable.MenuItem_android_visible, true);
         item.enabled = a.getBoolean(R.styleable.MenuItem_android_enabled, true);
         item.hasSubMenu = false;
 
         // TODO: (bug 1058909) Remove this branch when we remove old tablet. We do this to
-        // avoid using a new menu resource for new tablet (which only has a new reload button).
-        if (item.id == R.id.reload && NewTabletUI.isEnabled(mContext)) {
-            item.iconRes = R.drawable.new_tablet_ic_menu_reload;
+        // avoid using a new menu resource for new tablet.
+        final int iconResID;
+        if (!NewTabletUI.isEnabled(mContext)) {
+            iconResID = a.getResourceId(R.styleable.MenuItem_android_icon, 0);
         } else {
-            item.iconRes = a.getResourceId(R.styleable.MenuItem_android_icon, 0);
+            if (item.id == R.id.reload) {
+                iconResID = R.drawable.new_tablet_ic_menu_reload;
+            } else if (HardwareUtils.isLargeTablet() && item.id == R.id.bookmark) {
+                iconResID = R.drawable.new_tablet_ic_menu_bookmark_add;
+            } else {
+                iconResID = a.getResourceId(R.styleable.MenuItem_android_icon, 0);
+            }
         }
+        item.iconRes = iconResID;
 
         if (Versions.feature11Plus) {
             item.showAsAction = a.getInt(R.styleable.MenuItem_android_showAsAction, 0);
         }
 
         a.recycle();
     }
 
index 802af44f1596d5162feab910ad868a03fa145c30..5bd9a95a161377502d0e64865b5e3e74a414a55f
GIT binary patch
literal 775
zc%17D@N?(olHy`uVBq!ia0vp^(m*WB!3-pGf}dA0FfdvN_=LCuxp7Gu(TVBt$?0*4
z8F4@|3B-;~0;06&gfx(X#B^ihG>9xv+#w+WB$%8Lo0t-xlmXNZRPW&602GN!N&{;L
zGSVU9AO?s8DT_@`1?fsk4^2n_GUDS?ViQ0LVq;@N6BB`ofhZy=38W@DEh;$~s3j&T
zDJmfyXiGfU90UnNKz2+_8pvUZ88I=*V2jd2!5}^!XkA)-a$0O+ny+tKXeh)BAeR7T
zA$p;PKphPBa&!vVmW;T#xEP4<z-EHTWUxyT)BTc?fLcB=>D>ngL`F%FUob<fR>|D0
zC$=vv)@7W)ntmsPgK4(Y-3N9oOH2R!z0mOD@P-2~V;7i6d^QgHCnkFSm(;aB`OWI8
zkFMU^bLIWMFFS8ZmY1pQILOASJ)<R$i$CYxBIcFfCr$maY+ixP%-_P%=brz3dUAOr
z?+yDIUBIXl_H=O!sbE}t;iZ{VqR6q2x~(!^9U2QgUd(7;a%Sdp$JBYY-|JbwNuK+b
zwO9V{zJJ%_&PUDu7thQYzu5l5l{-Q?s<SV$nQv)7^r6LC$z^VAL3Fvus(JS!<nLUQ
zKOW!7xI>ro_=5`tTD*x{1tT6UmTZntTd-y08e2oZd7Yk_bsIALCRwVM?__DnoWEpd
zqz8B7S<hMg&KH<&ESf%T+QKb14Wc)-)@ZFbvDT$3`_47b{O0I+U%2iF&HlBGb=%r}
z>jc+e`O^uztG{I1yR2)rh&VYVWm!SSgvT7yzI^12^gDl!F(zix*&R)9chyYZqZ^*U
zJxMvpb#GLUO}XfbO^!i#>}|YOUU?P&@k>l?Wo`BUAAf$SUFYZVsYx@|mbC#!34^Dr
KpUXO@geCxrZ$pm&
index cc01566a8da04aeeb3b6488b713beac498feb69e..20678853b8465345d5c76ab85bd3bd38d4934b5c
GIT binary patch
literal 555
zc%17D@N?(olHy`uVBq!ia0vp^LO?9S!3-p~d(N!@QuhLULR^8ggM&kKbV@{GVrXb+
zTvD2+r)O+ZT6}UwTvB>`a=Jr80+0kDAU7!^CLsl=CN2>ylAIBpkP1?nm=PD3;p>|g
z2LwP_uwJ0aKs9k7Eg-!>Eip-{@j#VH86YlD3Zx8bOKdFASfFWOy@?>H*u=D`1h7<6
zS`0*4B3M~sT6}y;ELbEaF*P<RJt`>)WM+I?Y$C`IpkAQ)F$oz!hyAp!DhIm5vn0qb
zn1Mw`maU#W`_P{w95r_LAK6xg|N3kCW3CyOu;YS%jOX8Y%=8Z~T(my_c3|#T&5yd*
zVy5M(?kb4>S*aLroUqTzL}{<G(~Zwp9VQ;RG9PH@W=|K#kP61qQ`du990c4Rh6%D9
z<w=tXJd+e~=HS`td;eRvy-3u%vC3Bb^4EG(quo1yvifgvt^J#z*<!#o%{6R=YTVMy
z+}h?@X4?;J_4W1Sdh_PpTX`8dP46<EXD(arT+%E^TYBJ<WlY4*B@4B8gzY?dQTL8!
zW!>Z{1^RF09u#)XQ#f@?g!z2lo*TiAzq0=C-CO_SuZhE#uXF$WVQRfK|6uD+@wJyr
lgT5ZQdY=7o(UksDABN1kTIIZZCxMa!gQu&X%Q~loCIHb%<d*;d
index 92e88fd92c9eb9c8e6c189c1ac5c77c5a5e61163..1f22612cef7a301a43a33433a2e3a132b084654c
GIT binary patch
literal 995
zc%17D@N?(olHy`uVBq!ia0vp^DnP8k!3-o9%B;A^z`)oa;1l8s<i;hX1tcUyC#J_F
zq{k<v#l@w>#>N80<CD{)qM~AxQ{$3>;^}co8SzOOu}SHn2?;<g(a|X&!Q_m1uxvy^
z22c>F5vUA|(nCW-V-qtX5)(lRk~3mrl4F6o5>miiplRumk!fHPK<wDq6o-U_*u>O$
zuwJMk$>|Ob4spp~!MHf68<I1Ojnm>1!O9?d!GdvdX|c&^@rmg`DG)a?4Qvj`t{AAx
zGk{Ktj*bSIlL&G%&@b^I1hgn4CMh*84&)cGXH(;nQo$ONfDQvWH31kb5VxhpfJ_EC
zFD5Y+7)sG#?*P39atzo3U^l?L07mi2AjibVr^Y5gf)PZ<g53tzl>re1I{Wy1lhr^&
zTS|ibf*I-sv|Qcv#F%OXW7mHAxF%VmN@o7=Uo#adxZA(=vX#l6KciA?@#5<jk^7G)
zDRQ{?JUQ?(izVrm@bw3)SkwRIpHBTD*O!>{H-6=@Bhhygc=+D?U(MJvadxoTuC0D9
zAJjE-HPoJ$8og<<VszeKuwh1IW%#+h8#i|z)b`!IK5*gwo1b^gId*Pw2r#MCd%8G=
zR50$ne6yZAQR3*w%a_VHuC7|<%Bw1-tGP9}IU>r(SG9>{qkdAn$D8iY?Y}GRChs}F
zNAi8@e)H$g%Bwf5*p=`yZqt_K+s`*|>Is;Z9=2x5i+EFmjvHC4GBVB@Onq}g<6oq=
z@Y-m5H}A4${+uP9X1&HopXpl8%6M(I?3SPK*LMrLU98n5X5YEG+4?=d@7tT%?)OYI
zzrD3@m{I>B!06P3q+bS8_9<I^>JO5c-B@OG;UZ_(<jRcYA<hXg+YGpJ9n6npb{Std
z$9AQ_FRe`A)Eyn+g1Bc)Q+lonh^OvKXeo0(b2_F;Q1^y`+C`B$dfp)|!Jdn>3$(mT
zME$O-%xbt?+Qk*KsL3VN;_iYSoYSL4w%*=wXD9b!FT;9%&hPRc6mQ9~ifmPy^1<+=
z%&mC_VRcdMALiLEuV|e1%X^#8zEgYo?)S`a-|OoArSJboyQY<P23L>Uh^!afaJV3u
z&2y@n?pgmAUw(RD6KP$4f5JNJKmQ&-U;ppYd;4PUfPYL4f6QuQPw)Q@Od1THu6{1-
HoD!M<@M5Rs
index 107c77e8c3feb7d213a4f5c39fff08b76f16a475..2eb454333e88ae0ba338647ccb0bfe6b86fdfc54
GIT binary patch
literal 1323
zc%17D@N?(olHy`uVBq!ia0vp^W<YGo!3-q5&T3dOFfe`%@Ck7Ra^sRRKxATiR8&-4
zQhHosT3k{Zh>@HTpOg`sm;n@tO-PSTO!G@h0;x>SaBy&lOG*W+Nr$KelCg>Dp`oEb
zc0^JVP&__4JvKHLB$AXKnwSVA<AExH2F9iU1%a|a5~u~LH#yzd7-(rm3`j4~-t@?X
zw19*Jpj1pUNIV{FAjC#6BO?wdo}3z=m=1O!NF+WfH6CbMaz<=2*b8wPF^Op)jbM92
zL(`(8qhpiOKx&dQqN7uQ;;}#m%&9=n#wDc1K=dYp?Mlpmr~xTRg4+vZfWjvxCOIDL
zDF^}wMFzw!FoHW0Xbw;TC@^DSApn$(N=S{2Oo>fS2C0n8h>J@FsRa8SgyKPd0ed9F
z)6)}d8pP!wi$W6;A`%nh!M22kCPyTIT;-6E0CZstFs72gmIBo$Wkg1%#U!PI-3(G3
z5A;!T#trqo8o-ELRubeF%+Sg>LC`Y3wrkqN>J(pdUZ&YBo>Q;<`+c~Md8yRvw_<GT
zM0>w~Q{>#v)BO2Q8|wk}YiBhCP6T``IX@}?!^X);iDliJ{r<k|b2Kyh`l*SZulUS1
znJo#<zqp@9c~^vdnH!h-BI8J4d1r^pluPrkuXvf(@ZjL?8Cl_L9_jDZ+-V@L^F&(h
z@q+2Mv{!|`VV`C5W8Xaq*|5_>3O6}wJhGpaz7h#ud+2B3otL|}F)%RMd%8G=R50E>
z|1-QNQ0Dl@^F3`LSy8jHf}*^Pl0^)+M6A;6_G#IqBBWLA6e)Bn$m!dgiRbUXlsR5o
zHShVo^jH60eZBmBPW8R#HG2Qz^<Ferb1oEl9=S`WuF@&#Qr8oa8<oK;Ph7fWY*cHK
zlKQ;j&$RC`r*58VvGMv8yIMg+B<NIEm-mX0SsSJ5r><RSs~s=BVDW=p0u7(OZb=uN
z+LL+MEhKl5?QNN7OkUe_mg;`qRdYpQQ`ReiNZz$sueYh@-QT!*pQ4O*cF%<+D=Wh}
z{{JX9Z=bu#w%Rg3{&Zc0OLX?snZFsH%jz^Oc{@SN+NXK_QSMgdw?)?qOZHi@O?|97
zWwV%+pTpA4bBn}mJr{CoEpG}*S*}^K+~HwQ|Ee|ZPC_D4E1R^EotBAjlF@po(^@=X
z?sBOr%`XSf-SuZ*A$*<nMy38GPQ6ZT@v^zoWgm8(c(ibFNQ&u&iK?4cU%05UDf@z<
z!lsZHJM=a=zSyC-X;M+f4$p^TCLsY5O?`<IUGtosZCkA`6ijdP%h=(rXu#zD{q6=S
z-k^#P0(^44y}uK`&S+EjUubg6*N*$(A`Kts1ST%w$tpKL$cUMmoP68tJO9Vu1a_Tc
znUhRY<6hNG{5Z+UUnFf|_?8*#@?<LC`c^-#jJ9#xT>d^WEY#D}z{xXBTF0*DiBih(
zgKAO7Sw+t|Jo+b+nDaMoMU7kn&*7H^0Uw@f%d*617KDAvoy2v&x?W-1%$o}tbhGNu
wwQb<YJh?V@KijA0&ps{Nyu7aZ&xF5>>%5IlnD(VS115e3Pgg&ebxsLQ0FSdbYXATM
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..79aea57bc797602fbb01191f315abfa28ff388a8
GIT binary patch
literal 897
zc%17D@N?(olHy`uVBq!ia0vp^av;pX3?zBp#Z6#fV2lj#32|ir0|y5OW8*Z3goM!0
zP+#9PAQ>GU9RT7c$Hm1(My7=(CdS6bMn<NDCL}~8B}FACM<is##->Cjqyfq3#Ps<1
z)VR2msD#wG<mA}Iw78`7n8eiR_>7o@jJTw<*u<2Wg!I^i^qAzdIH2OBjF_a1*rbg3
z#Pqo2)Yzo-xWu&h#I)EXpe#r*HW8#NE-3>E0o9~KNTBw(<g|DoI~gpVoB=WcBnVaj
zK_GTUd@>RP*{Gy+un3TZm;hqLLl|HK<Dn)%>;fr9Vt@^V+YV+!%mG`M5f4Vo1VrM1
zAzM@u<QL4q$im9O$;HDbAS5gzDkd%=C9j~Up{b><XJBY<X=Ux;=;Yz$9TFCukeHO5
zl9`=TSzX`I)Y{(B-8*68<SEnUEm*i{@ygZfHf-Fqb=&scdk-8ueB{{i6DQAHx^nf}
z{fAGUK6~-%_1pI!KYjl4?fZ|PzkdJCEz?{8jN(*J7sn6@$<l*=t%DO8jz3&K{ifn!
zk!81>G^b2oGQBrRb;*Vk(LIa&_-;M>{Qv)j%j=$7J%4!XUG(*e``c`<eiys9a@{)b
zAG1SjBYPsR7HS{Z@ggKwKe1SN)2fr76DuUn-3~ond?P>OMtk@w#X2tKOB{bPC$C=l
z{$qFl{nG(CQ>3~BEMsO>PRg2eaeqO;u~}0LJ3}oaUW5y3--zxx*`fQ6HNrJF;fXri
z<RX=JwNDb#yZ$oY_*l}so%_!o<*)OYq6=e%?=#hXJ-OpGbE}wT6jwvNf9QASguRgq
zpOq?joR-?bV5j|sb%#vIi$u;o5v!(8%9fipEW04}<jE;#zlAb26SAe}oqX|fq9Nbz
zqQ7lDR}AA<o+<Z`{O~k`Ju3Nfx<vZrjbRE4J=px*`ZxBu`#)$@N{(=B_!=_tddSpg
zLY+H>Li0r;kL{?sue;c~zIEdo55^x;CQXfE+^1#qbp5iY9_xPv|9UTfT&M8~r+(;0
PP)_o6^>bP0l+XkKjlzI7
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..67654c5d76800a5bcd2844bd2aa17224679a12cf
GIT binary patch
literal 641
zc%17D@N?(olHy`uVBq!ia0vp^av;pX3?zBp#Z3TGp96eCTp7S1G&I!LH_b08DLOhj
zG&C(LDk?TMHZ&n2CMGQ|E+Z;AIU*q=Dk&)@DJecaH98?JK0YNjDLpzNH6}4NIzA&l
zIXyNpB`z@|COIuWF+DChJuV3-k^y2TrNx5DxTK6&kaz}ADjrHg89<G(U~XJ8R2D1_
zLh(>7U}a!7R2SF^h%AUq28lytp@zT}Be)<o5ItD7#tP`?fRZ4;U<O7`E?z!<K`{wQ
zX?X=Db!}Z^6H_Z|J13uj$gJG5%G$d6rndHup5Fe6ljbg5vUJ(<HJi8X+I#fa$;-Fy
zK7R7z&D(eHKYsfB<?FZaKYsrD^VfgTaYdlnZ#`WcLnI_?4|aw%IWVwZm>w1pm=PHn
zb*oEvi*}@1(h|?^_y6}V>oj(Elh`9}{At~6_ie$SWM=m8f7;xjb6g}RB|;`ZV!gxr
z2@|<9p4zw_3Vk6Hu&c@KUGJsoos!Su{=R$K9z6Ma`{KX7tQ=vNc~_X$<VC$G>eIV5
zXEE>Q<36^+8*I`(=y0gckE-LGxVAy+pxTnTg28ULXRK=R;m{7a;j>}kuGB-kA+l;E
z3qH+!;GAf1)M3q;DMm@5UVrVDY9;Zy9ZNs$t?uk5)nO@8xBj|<h~9opi;k@xc4CTa
ot%WzWM!fx-Dc<$y?}E1a`m$5~mNi|Q4}(&Yr>mdKI;Vst00M{w+W-In
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0098b76bd26ab501d846fee8e477e60691979e4b
GIT binary patch
literal 629
zc%17D@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQae*%0$Tp3`%!NDOkG&DLoIv^n-
zDk>^8G&wRdEi^GPHa0dQDJd>4BQ7p2A|WF-HYGkj4M;{Oq{YRh#3d)kB&NnEr^hCy
zM1#oGnB=tBg!H(?^q8a!AQ_vK9-o*I2PBiz<C4;1LF_azJ0liM#sj5*DuD>XMIyln
zq7j7RLE>Qb$q;UO9GD9xq1xk;;VR+W41_X>Y%;{yjPLbHF+iUOmjw9*GcYo-aPkQV
z3W<n{NlD8oC@O2}>KR(uIXSy~`-VivWE2*al$KRBHnp|)Ph7NN<E9<EcJDoW<k*Q*
zXU?9#aOv9h+js9hc=+V$i<ht8ynFxo>(9UclAk431C77x>Eak7A<24>)yavGfptUT
zb|$4);Tv~v;I`ga`+a}6G{f%Qz?eD7!_aW~i3~%6(edLm*%)M&Y7{Xs_&nJavg@44
zg2mTv+}Qp5tW5FGz4!0uT(OE#J6WE+eDb-u*UBfwWT>rHR&BU+BJ0wX;;H>o3|srR
zR7vhQSuMKY*Sy<|61&bvv#!|lG*LpL_MDBu{ON`b%~u+$_!w=L%`cky&xWz&@7Axm
zfB)os&HP`=xXLp6m(Qg0OE>p0r%hh6cgF?4Ns$MNK5f0{`~6n<CMJ!z?d**Q-(F|>
YulGTn@4_zYbWqaqboFyt=akR{0H)6buK)l5
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..112c5af885ac1c6e9fd445f97dbd6453b01a771c
GIT binary patch
literal 436
zc%17D@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa^8<WBT!FNwr>C!PT4-o!Kte)f
zWLjioN@zktL}FrGTxx7=N_>1uOk!$ua(ZlHYFtukY(hE^#V4i4C8ozFr^P0v$AP5M
zfMi@!Mm&fOW+VaCWW*(b*bs3D8J`Rh1d9W?Anh4o5{f`7fogz)V6-(LX%o<iCM7|B
z!3^wNJOU!(60&NV##YYmo?gCwA)y&rdF2g_&FxdxtlPM4``-P>Pn<q?<<{-{4<A2!
z@$U2Af82jw0QFDsba4!kkW4+;D%9k_z;eOW@sNYg8-_q0r<?!(o8?>VQM}h&WV0yQ
z$L~U!YJP@f;EY8togVhJDS8`+y!rX<i<ZXT^SfJlUEXvEY*22H+pKzx@$td7cE*D>
zo1SGZN?95b#65L_SI7PqucofB08JAQzHAu}xz?JkXEZ7%)O_0gl6mjf_AiP%e<n6e
d+IK#N!G7;@&83%Le^zDyg}<wx%Q~loCII;Wt+fCE
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..9cf55fb750949e3209eca683c740a9a5efb82d71
GIT binary patch
literal 1108
zc%17D@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*FxK#^NA%Cx&(BWL^T<Z2>+Zt_&~`
z8XD^0;9zW=24W;607+k85IZ0tAu=*8HZ~<bJ}o9DIVvF?$c|1-kB?7{OHPhSOp8rQ
zk4{L5Nl1xLPLEDbk4s980jfy^ksx+*T3ljAY(jc`QfgdcI*=QioED#y0c6JmxgZA6
zgp9c4RG>LPMm&g2k4pxUV2z1saUe6HBv1>8Oiqsn8Vl3~R0%W(!T=eY4rYK10g@nd
zKqjX{xIn3RkQ7J+Y#LZu5=0!%g|Z=v;~)k?HG<VZOhYmji3GU>f*=ae%#4Sb3F0Or
ztN@ZOHH$w1<Dju5$S;_Ik%^gwm5qawi$_32QCUS*Q%l>>$i%|d&e_G)-NVz{$Jft4
zAT%sIGCDpXDLFMQBP%;6FTb?BqOz*7rM;`WXVT;;Q>RT|uyE1hB}<pBTC;Y;#x2`+
z?%KU)-~Iy!4;?;n`pnsL=PzBpa`oD++xH(ndiwn3>yMwleEs(Q$IoBC|NQ;;|LV<t
zVDfnH>Eak7A=!Jd+djCE;n>6PZ*tD43QqFU-Fj)4L@bBsE``<@rRgDU9kWh%bk+X<
zT$i3Rf3ESidG0&4#Kp9}%D<5~k|SCst-PpdO=?f*d@jz(Mc%D7im4}q>Ua<Qu@MNX
zo8J(?5fKn4-k^FqiplBJb7p~wN(a)P7Rp6@_x@M<V3S{JFXLJxIfl>MihgQI-M(LQ
zm({G<sA=|rdyjUXlTNR-3cGZ}P~x|1)bb!J&)d;8w=YI0xeBFtPtjbe<G9VmrSKBp
zwPV$@vP;%1+nHYTwqeVTv_hWk?W>ti39jJU&i{Bu=vTgDJ^{-)&LzAF^A`VZpw?Zw
z=#gj7UvHyze-F;kxWw6|X7DdA{M1?Io{bGfEXNG^W|zG26G}d!&;Osp?bAG?s6w8b
zMH|}-Y(l1)$uJj59{A{@7b{V?^N8WRQ;%5<RHth`n7Zd3)83+&tnW_0mb{=eUF^Zm
zJ@=Tu7OiA=d-|1EWBTGPt5=8g6(xkq?W?^pDbd|LK{@O6ji&qC?wn&Np5&yxWKE08
zSw_*KY=&PU*1pF|*8XZ(T~y6d!moC6lFS=6xzle|CENf0ZBN-V?Y2nf;;WZ-241o|
z^_JZ|?e&x(^TQM87kNMGhz)de=5fCiWOko#!?hI6?yFy9Z+twtG`HIF)%14zU3=~>
bQ~uBJO8v`%CefG*P+{Qd>gTe~DWM4f=D69O
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..5b9299437465db3a539517136cc476c003c476ac
GIT binary patch
literal 744
zc%17D@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*HX>F!BZXgt#)mKxk;FLqbAmLPB(O
zN<?B}L_$VvY)VX0QgmW^Oj3GWa&l}^dUQf+Y+_nWLPlItT5MuUOhP&k#U-W2B&WrL
z$+)D9SfF51YJ5^gJV;|kTykn$Vp=Rv9B4>l22gukQhF>%J4iu1NMi;_1i}E3=`fTL
zpA3?UO9r8MFp2{!hLG`4@r-zwNE}Fg21pPr5)alJ4-<iCkB2D(tAVi7<6-2@!_u>W
zVG&sp<QL4q$i&7WAtS4xsIH}NXku#a;OOM);py!c5Ev9378#wClA4i|S5#b9+1b<E
zH*xZu`Ae3sTD^9|rd_-D9XNR8<e9VQE?m5P<?4<5kDtGO|Ka1O&)>fP{Qc+eKjy>C
zz+h)I@N{tuk&x^?$gO-RfPw8o{%4PSODA|}d41+_?V7NtLvGdk|NjLejql#PTgLbA
zbGTMrMWEX04+(RXXBU(opXkn-$8^Q@!7`7#lbO<2o8%n6Y07(^YmUyPEzIiEO|FKX
zILrS-olVp9IH&&Q4Ti#27VAqiUR>AbGq3rvblsb|clHIo+FLU#BCs)IvYg4{j0=<5
zyuK`HX*ILW&}V4mi!Ei6;Qan#k$9WlCs+4)L5U=0D~TfsUHqD+J;_d8L7E&!Q%)Rk
zTCB?(=6TEOkQa!-dQ0oTDz|9U2#wfVue1byYCLS#@aR-<;q+2bICbfYwcV>rQ(86r
z4;z@XpPBbH^MrHA(;H`nN<N3ntakcxP`+Pr#<cD~9~m#IpS&K+I{Df=&pM|mc~`$L
gPuTkPf>-hz>36ea{QsTQE&-)hPgg&ebxsLQ0C0^k#{d8T
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..fdf1ae32a3586ecb9b45bb8c690272868607b24b
GIT binary patch
literal 1579
zc%17D@N?(olHy`uVBq!ia0vp^HXzKw3=&b&bO2I}#X;^)4C~IxykuZtVhQjGab<vk
z(9lo^2M1&0G$6y%)6*d#0mz7rjRlf^NlBrhX#oidadB~xk!hici9qql$du58gqWDL
zh@_;5#KgF`w1|X^sHCL$__Uaq<hZ!hn53lW#Ps<1)aZn?n56Wmgw&YCwAiHdxTN&x
z_>8!uw3vjH_~i81#8e<VHZdhSIXxx;!cC1$NRLTQi%ZOiON21eW0TY3lQLqHQ{zCU
zWyAx`Nlc3aazSQ7xEb*v5fB4N#wMl314V#bkl{cnC?gi=B#<D80V2WdIIyeY62T<M
zA_xO)EIKy>q&_(v#s*2nLl_W2kZBN=AU4D<m^jpGxE6FSj2#b^g4hDJ3(k&5H~`9y
zg9}0ogj<~qbqv%Qa7&Ys-H@@*{`g&B3Rqnd<QL4q$i&RT%Er#Y$<4#dFCZ);Dkd%=
zDJ3hXprov#rmm%}t8ZXrVrgw-YiIB1<m~G1>E-R?9}p5677-O47oU`pnwFlKm6Kmk
zSW;S6Syf%v*wozG*51+8-8*^8)M?Xa%$zlQ{(?n|mn>bja@Fd!>o;!RvUS_`-Fx=#
z+kfETp~FXx9y@;G)af&4&tJH7<?6NTH*ej&cmLs|$4{O<d-3x1o44;ie){_D$IoBC
z|NQ;;_4m?h1_ow1PZ!4!3CXz!yPbnmW!N5k&oPeGGCi_{!O+L`bhJh$<J7{F4uYq=
z`2+&Jq+C^JO<Fngk-_Ilr<?>5wNBit-*mrx-{Uf~eTU8Fv2(BNKKg2d$5l?Fz+TtL
zUmcGw2I(9V-(s2|$n^ixw*t;j;oirepP2ogPxd?WiR*G|R~6#}yG~EneD`a@d@Zfe
z&=5Pvx^;nfHm!*k*{=H}S$nF~g0@rcCyujru`?=sXh=HZ^v*0Ovms-_Ch48OrzY8O
z2+Azq(Ku)A@*;t4%^_t<;r2m3;s?SEd_SreZK>;fBq4KrMTp{T^X*sK^=+1Ysxg&W
zJwJHnpT{nbuZ0?L?!AA((B#|{IhC&lPfsot?v)o&TDfQ6>+hUtGqhcUUdkBT?7R{h
z&$r-%jjvKc=Y=OVisuUVZS20A{czK^-;X4oTen-pK2o0eZ5FR~!iFSg-|7?<zv&ij
zD|uU`w~I%gIHYy&o`l?@GU44TUH_bzXz#RE%EISu55MKdxeLog7EQjD$!)p8FZgv^
z($^sCOhxV2DqG*{-uUzP%ApflK5y0SWxVSamdQlQEI0chHEG&kx3?T(B`voC1oYO&
zFTK{rx+QS;Hkr&YzToWE<zZPDnNlhq=VzV$aKY#8e1T`0yWH1uy7^vtx1Y87QTrC(
zLm88{UEH1+V&(Rft7lz6WegktQ+<u9ge%U5zxDR82#fqq+%o@EQN_vyo0#3KSL|We
zuU@gH@uHNL{9$kHqb4>!!7@$L-)pQpe7LmBuHt9+B5}>x=E_T_KVsWsvozzNb}I7|
z2g$h1g)^Gk1*XPcaFX3DvRPI!WA_S?gKH*vaZY*AsA;u~k7ePMfSXL4{Wa(PWcZQg
z{?aJwHS>dNw~vx%`hr{-bGnV3OQah#j&3)QUhv}ZE=MVzb<Xlf&l~Lay;%85b#fk`
z@yn+x{kp6TKJ8D~Rb8!{{XTZt?2m_^$S(VtAJ3q)LI0S^QiGeG)0gWyS~Q)VT=G*u
z&DW<(Ua#Twsr*Mizl7A@mYgXORdfz~&LbOtcuDWJBmAlc8ZW&sTw|N;8*(n}fb-Ev
qt*HjTw9GEXce3WZn%`ln{Ip&{dXq-3-*Gum8Sm-p=d#Wzp$P!v$f834
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d2baeed443d989754b3312c85f2b3b792dcf472d
GIT binary patch
literal 1123
zc%17D@N?(olHy`uVBq!ia0vp^HXzKw3=&b&bYNg$To~XJ;>rL6p`oD;4i3h~X&^>I
z0+5W2jRlf^NlDSs(V?Mf0SO6FQBk3x$#HRUk&$UY*~rM0(1e7zxQw{Cw1|X^sHCK*
zg!H($)Y#aR*kquxl(^*N*rfE>#I(4i^thz7_+%hEB|14hCLujODJ?cJH7+SNJ}EUW
zF+DapEj}p&%t(z(NQ;57W0Nv~Cd4JC#ezgK;*!&WC>CfGkPS977Q_IV3?$=#YJln!
z)4>d&uJm{aBN@U4ks#BOK<325#gm}Qz#<U68S!9cU=pk#8D?fOgbPyu=ElLea7H{-
zFe4tW1!8G3)OM)e<P3-#;=%3#8JnCA6M>TVb$&Gfqo=DR$S;_Ik%^gwm5qawn}?T=
zUqDbuSVT-*LRwBiNmWf#N6*m2+|tI*$=TJ--P125G%P$KDmo@MAvq;AJuAPUu&A`G
zqNcW?skyal!o;c5X3d?qaM9u=OP8%)vu^!{jhnaZ*t758k)tP0o<4K--1!R^FI~QN
z{l=}^cke%Z^7Pq@m#^QvefRmx*Y7`m{r>a!pZ!OvAHXE^+|$J|L_%`z!QOc8L<aT`
z_seGcn5s_NvxT=)bn_nVu2z+$u2Z~BJ)MlWB_~Dx{g<`*UGclT^QGsD@9URMii!0-
zwD_8D(Pq(kKg1$G9b?yjq?6q)t{?gB9DC{V71r#f##i=!cfMLEb<X*!2(P@!ip~p9
zn6B6?KH0@4>b^arOi)YivXXSeo@YvH^&Iax->Ttqx^|H1?K+2R4BAi19M3)E4coQw
zpsPmJ&wt(dBDX3`uap+ayKGTTf8Ue7{l&A>SN5Juc9i(G?QYGMAG5i*@-N%wCNgYY
z>{=Bhv-T*9t@Lp#+vwv=w$ToA<{fRctGd!Ysjli$|0c&OgO1#Pf?CoaotO6pUYx<^
zTrkN?t=VAZtYu9vwIW*5G;_^ZX3whhnzi1sb(gAF<f>+e!$zt)<_;?iy~A(1S!8Hk
za^1-3_v+;;Ilo0EqOFttc6lB0>{=I8FlpIplQ}6bw3yW^cbz)yI78@=KzD!>OA@D-
zhr=5))jLf$3{-iXbNb58t7n+AZ#0cc^<&s58MW4N#Vj%Hw;I-;XNg@E$Xs~xdN$*W
zn$CWc%tjA(Ui;wc1fd<jX14Kllmvc%ez5Mwn|D7Q@0NCaDY`v5jicrLzobZgm!v=G
yYFzB>Y+O2>jw`Ic|9zSH=D~#kyX(FG|1s$%JfE;z-TN-6pzw6{b6Mw<&;$Vcved%>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/new_tablet_ic_menu_bookmark_add.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@null"/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/new_tablet_ic_menu_bookmark_remove.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@null"/>
--- a/mobile/android/base/resources/values-large-v11/styles.xml
+++ b/mobile/android/base/resources/values-large-v11/styles.xml
@@ -102,25 +102,20 @@
         <item name="android:layout_width">wrap_content</item>
         <item name="android:layout_height">wrap_content</item>
         <item name="android:scaleType">center</item>
         <item name="android:background">@drawable/new_tablet_action_bar_button</item>
 
         <!-- layout_width/height doesn't work here, likely because it's
              an ImageButton, so we use padding instead.
 
-             Because the curve of the reload button is lower than the end of the arrow and thus
-             the top of the view, it is perceived to be slighly lower than the other buttons.
-             Thus we use padding to fill a slightly smaller height than the toolbar and let
-             android:gravity center the image with a slight offset (.5dp higher) instead. Note that
-             this offset also affects the pressed state, but it's hardly noticeable and thus negligible.
-
-             Note: the above change is done on a generic style and thus any other items added
-             to the menu bar may appear to be offset. -->
-        <item name="android:paddingTop">20dp</item>
+             Notes:
+                 * The bookmarks star is larger than the reload button
+                 * The reload button contains whitespace at the top of the image to lower it -->
+        <item name="android:paddingTop">19dp</item>
         <item name="android:paddingBottom">21dp</item>
         <item name="android:paddingLeft">@dimen/new_tablet_browser_toolbar_menu_item_padding_horizontal</item>
         <item name="android:paddingRight">@dimen/new_tablet_browser_toolbar_menu_item_padding_horizontal</item>
     </style>
 
     <style name="Widget.BookmarksListView" parent="Widget.HomeListView">
         <item name="android:scrollbarStyle">outsideOverlay</item>
     </style>
--- a/mobile/android/base/tests/testGeckoRequest.java
+++ b/mobile/android/base/tests/testGeckoRequest.java
@@ -91,17 +91,17 @@ public class testGeckoRequest extends UI
 
         GeckoAppShell.sendRequestToGecko(new GeckoRequest(REQUEST_EXCEPTION_EVENT, null) {
             @Override
             public void onResponse(NativeJSObject nativeJSObject) {
                 responseReceived.set(true);
             }
 
             @Override
-            public void onError() {
+            public void onError(NativeJSObject error) {
                 errorReceived.set(true);
             }
         });
 
         WaitHelper.waitFor("Received error for listener with exception", new Condition() {
             @Override
             public boolean isSatisfied() {
                 return errorReceived.get();
--- a/mobile/android/base/util/GeckoRequest.java
+++ b/mobile/android/base/util/GeckoRequest.java
@@ -1,8 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 package org.mozilla.gecko.util;
 
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import org.mozilla.gecko.mozglue.RobocopTarget;
@@ -72,20 +75,20 @@ public abstract class GeckoRequest {
      * @param nativeJSObject The response data from Gecko
      */
     @RobocopTarget
     public abstract void onResponse(NativeJSObject nativeJSObject);
 
     /**
      * Callback executed when the request fails.
      *
-     * In general, this should not be overridden since there's no way to differentiate between
-     * expected errors and logic errors in JS. If the Gecko-side request handler wants to send a
-     * recoverable error to Java, it should include any error data in the response object that the
-     * {@link #onResponse(NativeJSObject)} callback can handle as necessary.
+     * By default, an exception is thrown. This should be overridden if the
+     * GeckoRequest is able to recover from the error.
      *
      * @throws RuntimeException
      */
     @RobocopTarget
-    public void onError() {
-        throw new RuntimeException("Unhandled error for GeckoRequest: " + name);
+    public void onError(NativeJSObject error) {
+        final String message = error.optString("message", "<no message>");
+        final String stack = error.optString("stack", "<no stack>");
+        throw new RuntimeException("Unhandled error for GeckoRequest " + name + ": " + message + "\nJS stack:\n" + stack);
     }
-}
\ No newline at end of file
+}
--- a/mobile/android/modules/Messaging.jsm
+++ b/mobile/android/modules/Messaging.jsm
@@ -138,29 +138,31 @@ let requestHandler = {
     delete this._listeners[aMessage];
     Services.obs.removeObserver(this, aMessage);
   },
 
   observe: Task.async(function* (aSubject, aTopic, aData) {
     let wrapper = JSON.parse(aData);
     let listener = this._listeners[aTopic];
 
-    // A null response indicates an error. If an error occurs in the callback
-    // below, the response will remain null, and Java will fire onError for
-    // this request.
-    let response = null;
-
     try {
-      let result = yield listener(wrapper.data);
-      if (typeof result !== "object" || result === null) {
+      let response = yield listener(wrapper.data);
+      if (typeof response !== "object" || response === null) {
         throw new Error("Gecko request listener did not return an object");
       }
-      response = result;
+
+      Messaging.sendRequest({
+        type: "Gecko:Request" + wrapper.id,
+        response: response
+      });
     } catch (e) {
-      Cu.reportError(e);
-    }
+      Cu.reportError("Error in Messaging handler for " + aTopic + ": " + e);
 
-    Messaging.sendRequest({
-      type: "Gecko:Request" + wrapper.id,
-      response: response
-    });
+      Messaging.sendRequest({
+        type: "Gecko:Request" + wrapper.id,
+        error: {
+          message: e.message || (e && e.toString()),
+          stack: e.stack || Components.stack.formattedStack,
+        }
+      });
+    }
   })
 };