Merge fx-team to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Fri, 24 Oct 2014 14:50:33 -0700
changeset 212216 be08d039123b9a62241fe114072a6d52847a8c8a
parent 212215 7fc54253717b36ac7f9f1251274a983d571d2f1c (current diff)
parent 212189 8496d7a26512c7ac07ac0ab37f1e15cb513eef0b (diff)
child 212260 c70f62375f7d496edee7ebd2535fce746500e96c
push id9560
push userkwierso@gmail.com
push dateFri, 24 Oct 2014 22:09:23 +0000
treeherderfx-team@c70f62375f7d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone36.0a1
Merge fx-team to m-c a=merge
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1608,17 +1608,17 @@ pref("loop.enabled", true);
 pref("loop.throttled", true);
 pref("loop.soft_start_ticket_number", -1);
 pref("loop.soft_start_hostname", "soft-start.loop.services.mozilla.com");
 #endif
 
 pref("loop.server", "https://loop.services.mozilla.com");
 pref("loop.seenToS", "unseen");
 pref("loop.learnMoreUrl", "https://www.firefox.com/hello/");
-pref("loop.legal.ToS_url", "https://call.mozilla.com/legal/terms/");
+pref("loop.legal.ToS_url", "https://hello.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");
 pref("loop.retry_delay.start", 60000);
 pref("loop.retry_delay.limit", 300000);
 pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
 pref("loop.feedback.product", "Loop");
 pref("loop.debug.loglevel", "Error");
--- a/browser/components/loop/LoopRooms.jsm
+++ b/browser/components/loop/LoopRooms.jsm
@@ -11,30 +11,21 @@ Cu.import("resource://gre/modules/Servic
 
 XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService",
                                   "resource:///modules/loop/MozLoopService.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LOOP_SESSION_TYPE",
                                   "resource:///modules/loop/MozLoopService.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
                                   "resource:///modules/loop/MozLoopPushHandler.jsm");
 
-// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
-XPCOMUtils.defineLazyGetter(this, "log", () => {
-  let ConsoleAPI = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).ConsoleAPI;
-  let consoleOptions = {
-    maxLogLevel: Services.prefs.getCharPref(PREF_LOG_LEVEL).toLowerCase(),
-    prefix: "Loop",
-  };
-  return new ConsoleAPI(consoleOptions);
-});
-
 this.EXPORTED_SYMBOLS = ["LoopRooms", "roomsPushNotification"];
 
 let gRoomsListFetched = false;
 let gRooms = new Map();
+let gCallbacks = new Map();
 
   /**
    * Callback used to indicate changes to rooms data on the LoopServer.
    *
    * @param {Object} version Version number assigned to this change set.
    * @param {Object} channelID Notification channel identifier.
    *
    */
@@ -54,40 +45,42 @@ let LoopRoomsInternal = {
       // Fetch the rooms from the server.
       let sessionType = MozLoopService.userProfile ? LOOP_SESSION_TYPE.FXA :
                         LOOP_SESSION_TYPE.GUEST;
       let rooms = yield this.requestRoomList(sessionType);
       // Add each room to our in-memory Map using a locally unique
       // identifier.
       for (let room of rooms) {
         let id = MozLoopService.generateLocalID();
-        room.localRoomID = id;
+        room.localRoomId = id;
         // Next, request the detailed information for each room.
         // If the request fails the room data will not be added to the map.
         try {
           let details = yield this.requestRoomDetails(room.roomToken, sessionType);
           for (let attr in details) {
             room[attr] = details[attr]
           }
+          delete room.currSize; //This attribute will be eliminated in the next revision.
           gRooms.set(id, room);
         }
-        catch (error) {log.warn("failed GETing room details for roomToken = " + room.roomToken + ": ", error)}
+        catch (error) {MozLoopService.log.warn(
+          "failed GETing room details for roomToken = " + room.roomToken + ": ", error)}
       }
       callback(null, [...gRooms.values()]);
       return;
-      }.bind(this)).catch((error) => {log.error("getAll error:", error);
+      }.bind(this)).catch((error) => {MozLoopService.log.error("getAll error:", error);
                                       callback(error)});
     return;
   },
 
-  getRoomData: function(roomID, callback) {
-    if (gRooms.has(roomID)) {
-      callback(null, gRooms.get(roomID));
+  getRoomData: function(localRoomId, callback) {
+    if (gRooms.has(localRoomId)) {
+      callback(null, gRooms.get(localRoomId));
     } else {
-      callback(new Error("Room data not found or not fetched yet for room with ID " + roomID));
+      callback(new Error("Room data not found or not fetched yet for room with ID " + localRoomId));
     }
     return;
   },
 
   /**
    * Request list of all rooms associated with this account.
    *
    * @param {String} sessionType Indicates which hawkRequest endpoint to use.
@@ -125,16 +118,160 @@ let LoopRoomsInternal = {
    *
    * @param {Object} version Version number assigned to this change set.
    * @param {Object} channelID Notification channel identifier.
    *
    */
   onNotification: function(version, channelID) {
     return;
   },
+
+  createRoom: function(props, callback) {
+    // Always create a basic room record and launch the window, attaching
+    // the localRoomId. Later errors will be returned via the registered callback.
+    let localRoomId = MozLoopService.generateLocalID((id) => {gRooms.has(id)})
+    let room = {localRoomId : localRoomId};
+    for (let prop in props) {
+      room[prop] = props[prop]
+    }
+
+    gRooms.set(localRoomId, room);
+    this.addCallback(localRoomId, "RoomCreated", callback);
+    MozLoopService.openChatWindow(null, "", "about:loopconversation#room/" + localRoomId);
+
+    if (!"roomName" in props ||
+        !"expiresIn" in props ||
+        !"roomOwner" in props ||
+        !"maxSize" in props) {
+      this.postCallback(localRoomId, "RoomCreated",
+                        new Error("missing required room create property"));
+      return localRoomId;
+    }
+
+    let sessionType = MozLoopService.userProfile ? LOOP_SESSION_TYPE.FXA :
+                                                   LOOP_SESSION_TYPE.GUEST;
+
+    MozLoopService.hawkRequest(sessionType, "/rooms", "POST", props).then(
+      (response) => {
+        let data = JSON.parse(response.body);
+        for (let attr in data) {
+          room[attr] = data[attr]
+        }
+        delete room.expiresIn; //Do not keep this value - it is a request to the server
+        this.postCallback(localRoomId, "RoomCreated", null, room);
+      },
+      (error) => {
+        this.postCallback(localRoomId, "RoomCreated", error);
+      });
+
+    return localRoomId;
+  },
+
+  /**
+   * Send an update to the callbacks registered for a specific localRoomId
+   * for a callback type.
+   *
+   * The result set is always saved. Then each
+   * callback function that has been registered when this function is
+   * called will be called with the result set. Any new callback that
+   * is regsitered via addCallback will receive a copy of the last
+   * saved result set when registered. This allows the posting operation
+   * to complete before the callback is registered in an asynchronous
+   * operation.
+   *
+   * Callbacsk must be of the form:
+   *    function (error, success) {...}
+   *
+   * @param {String} localRoomId Local room identifier.
+   * @param {String} callbackName callback type
+   * @param {?Error} error result or null.
+   * @param {?Object} success result if error argument is null.
+   */
+  postCallback: function(localRoomId, callbackName, error, success) {
+    let roomCallbacks = gCallbacks.get(localRoomId);
+    if (!roomCallbacks) {
+      // No callbacks have been registered or results posted for this room.
+      // Initialize a record for this room and callbackName, saving the
+      // result set.
+      gCallbacks.set(localRoomId, new Map([[
+        callbackName,
+        { callbackList: [], result: { error: error, success: success } }]]));
+      return;
+    }
+
+    let namedCallback = roomCallbacks.get(callbackName);
+    // A callback of this name has not been registered.
+    if (!namedCallback) {
+      roomCallbacks.set(
+        callbackName,
+        {callbackList: [], result: {error: error, success: success}});
+      return;
+    }
+
+    // Record the latest result set.
+    namedCallback.result = {error: error, success: success};
+
+    // Call each registerd callback passing the new result posted.
+    namedCallback.callbackList.forEach((callback) => {
+      callback(error, success);
+    });
+  },
+
+  addCallback: function(localRoomId, callbackName, callback) {
+    let roomCallbacks = gCallbacks.get(localRoomId);
+    if (!roomCallbacks) {
+      // No callbacks have been registered or results posted for this room.
+      // Initialize a record for this room and callbackName.
+      gCallbacks.set(localRoomId, new Map([[
+        callbackName,
+        {callbackList: [callback]}]]));
+      return;
+    }
+
+    let namedCallback = roomCallbacks.get(callbackName);
+    // A callback of this name has not been registered.
+    if (!namedCallback) {
+      roomCallbacks.set(
+        callbackName,
+        {callbackList: [callback]});
+      return;
+    }
+
+    // Add this callback if not already in the array
+    if (namedCallback.callbackList.indexOf(callback) >= 0) {
+      return;
+    }
+    namedCallback.callbackList.push(callback);
+
+    // If a result has been posted for this callback
+    // send it using this new callback function.
+    let result = namedCallback.result;
+    if (result) {
+      callback(result.error, result.success);
+    }
+  },
+
+  deleteCallback: function(localRoomId, callbackName, callback) {
+    let roomCallbacks = gCallbacks.get(localRoomId);
+    if (!roomCallbacks) {
+      return;
+    }
+
+    let namedCallback = roomCallbacks.get(callbackName);
+    if (!namedCallback) {
+      return;
+    }
+
+    let i = namedCallback.callbackList.indexOf(callback);
+    if (i >= 0) {
+      namedCallback.callbackList.splice(i, 1);
+    }
+
+    return;
+  },
 };
 Object.freeze(LoopRoomsInternal);
 
 /**
  * The LoopRooms class.
  *
  * Each method that is a member of this class requires the last argument to be a
  * callback Function. MozLoopAPI will cause things to break if this invariant is
@@ -151,20 +288,56 @@ this.LoopRooms = {
    */
   getAll: function(callback) {
     return LoopRoomsInternal.getAll(callback);
   },
 
   /**
    * Return the current stored version of the data for the indicated room.
    *
-   * @param {String} roomID Local room identifier
+   * @param {String} localRoomId Local room identifier
    * @param {Function} callback Function that will be invoked once the operation
    *                            finished. The first argument passed will be an
    *                            `Error` object or `null`. The second argument will
    *                            be the list of rooms, if it was fetched successfully.
    */
-  getRoomData: function(roomID, callback) {
-    return LoopRoomsInternal.getRoomData(roomID, callback);
+  getRoomData: function(localRoomId, callback) {
+    return LoopRoomsInternal.getRoomData(localRoomId, callback);
+  },
+
+  /**
+   * Create a room. Will both open a chat window for the new room
+   * and perform an exchange with the LoopServer to create the room.
+   * for a callback type. Callback must be of the form:
+   *    function (error, success) {...}
+   *
+   * @param {Object} room properties to be sent to the LoopServer
+   * @param {Function} callback Must be of the form: function (error, success) {...}
+   *
+   * @returns {String} localRoomId assigned to this new room.
+   */
+  createRoom: function(roomProps, callback) {
+    return LoopRoomsInternal.createRoom(roomProps, callback);
+  },
+
+  /**
+   * Register a callback of a specified type with a localRoomId.
+   *
+   * @param {String} localRoomId Local room identifier.
+   * @param {String} callbackName callback type
+   * @param {Function} callback Must be of the form: function (error, success) {...}
+   */
+  addCallback: function(localRoomId, callbackName, callback) {
+    return LoopRoomsInternal.addCallback(localRoomId, callbackName, callback);
+  },
+
+  /**
+   * Un-register and delete a callback of a specified type for a localRoomId.
+   *
+   * @param {String} localRoomId Local room identifier.
+   * @param {String} callbackName callback type
+   * @param {Function} callback Previously passed to addCallback().
+   */
+  deleteCallback: function(localRoomId, callbackName, callback) {
+    return LoopRoomsInternal.deleteCallback(localRoomId, callbackName, callback);
   },
 };
 Object.freeze(LoopRooms);
-
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -174,16 +174,45 @@ loop.conversation = (function(mozL10n) {
           )
         )
         /* jshint ignore:end */
       );
     }
   });
 
   /**
+   * Incoming Call failed view. Displayed when a call fails.
+   *
+   * XXX Based on CallFailedView, but built specially until we flux-ify the
+   * incoming call views (bug 1088672).
+   */
+  var IncomingCallFailedView = React.createClass({displayName: 'IncomingCallFailedView',
+    propTypes: {
+      cancelCall: React.PropTypes.func.isRequired
+    },
+
+    render: function() {
+      document.title = mozL10n.get("generic_failure_title");
+
+      return (
+        React.DOM.div({className: "call-window"}, 
+          React.DOM.h2(null, mozL10n.get("generic_failure_title")), 
+
+          React.DOM.div({className: "btn-group call-action-group"}, 
+            React.DOM.button({className: "btn btn-cancel", 
+                    onClick: this.props.cancelCall}, 
+              mozL10n.get("cancel_button")
+            )
+          )
+        )
+      );
+    }
+  });
+
+  /**
    * This view manages the incoming conversation views - from
    * call initiation through to the actual conversation and call end.
    *
    * At the moment, it does more than that, these parts need refactoring out.
    */
   var IncomingConversationView = React.createClass({displayName: 'IncomingConversationView',
     propTypes: {
       client: React.PropTypes.instanceOf(loop.Client).isRequired,
@@ -249,21 +278,23 @@ loop.conversation = (function(mozL10n) {
               model: this.props.conversation, 
               video: {enabled: callType !== "audio"}}
             )
           );
         }
         case "end": {
           // XXX To be handled with the "failed" view state when bug 1047410 lands
           if (this.state.callFailed) {
-            document.title = mozL10n.get("generic_failure_title");
-          } else {
-            document.title = mozL10n.get("conversation_has_ended");
+            return IncomingCallFailedView({
+              cancelCall: this.closeWindow.bind(this)}
+            )
           }
 
+          document.title = mozL10n.get("conversation_has_ended");
+
           var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
             "feedback.baseUrl");
 
           var appVersionInfo = navigator.mozLoop.appVersionInfo;
 
           var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, {
             product: navigator.mozLoop.getLoopCharPref("feedback.product"),
             platform: appVersionInfo.OS,
@@ -389,37 +420,42 @@ loop.conversation = (function(mozL10n) {
      * @param {Object} progressData The progress data from the websocket.
      * @param {String} previousState The previous state from the websocket.
      */
     _handleWebSocketProgress: function(progressData, previousState) {
       // We only care about the terminated state at the moment.
       if (progressData.state !== "terminated")
         return;
 
-      if (progressData.reason === "cancel" ||
-          progressData.reason === "closed") {
+      // XXX This would be nicer in the _abortIncomingCall function, but we need to stop
+      // it here for now due to server-side issues that are being fixed in bug 1088351.
+      // This is before the abort call to ensure that it happens before the window is
+      // closed.
+      navigator.mozLoop.stopAlerting();
+
+      // If we hit any of the termination reasons, and the user hasn't accepted
+      // then it seems reasonable to close the window/abort the incoming call.
+      //
+      // If the user has accepted the call, and something's happened, display
+      // the call failed view.
+      //
+      // https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons
+      if (previousState === "init" || previousState === "alerting") {
         this._abortIncomingCall();
-        return;
+      } else {
+        this.setState({callFailed: true, callStatus: "end"});
       }
 
-      if (progressData.reason === "timeout") {
-        if (previousState === "init" || previousState === "alerting") {
-          this._abortIncomingCall();
-        } else {
-          this.setState({callFailed: true, callStatus: "end"});
-        }
-      }
     },
 
     /**
      * Silently aborts an incoming call - stops the alerting, and
      * closes the websocket.
      */
     _abortIncomingCall: function() {
-      navigator.mozLoop.stopAlerting();
       this._websocket.close();
       // Having a timeout here lets the logging for the websocket complete and be
       // displayed on the console if both are on.
       setTimeout(this.closeWindow, 0);
     },
 
     closeWindow: function() {
       window.close();
@@ -639,13 +675,14 @@ loop.conversation = (function(mozL10n) {
       outgoing: outgoing
     }));
   }
 
   return {
     AppControllerView: AppControllerView,
     IncomingConversationView: IncomingConversationView,
     IncomingCallView: IncomingCallView,
+    IncomingCallFailedView: IncomingCallFailedView,
     init: init
   };
 })(document.mozL10n);
 
 document.addEventListener('DOMContentLoaded', loop.conversation.init);
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -174,16 +174,45 @@ loop.conversation = (function(mozL10n) {
           </div>
         </div>
         /* jshint ignore:end */
       );
     }
   });
 
   /**
+   * Incoming Call failed view. Displayed when a call fails.
+   *
+   * XXX Based on CallFailedView, but built specially until we flux-ify the
+   * incoming call views (bug 1088672).
+   */
+  var IncomingCallFailedView = React.createClass({
+    propTypes: {
+      cancelCall: React.PropTypes.func.isRequired
+    },
+
+    render: function() {
+      document.title = mozL10n.get("generic_failure_title");
+
+      return (
+        <div className="call-window">
+          <h2>{mozL10n.get("generic_failure_title")}</h2>
+
+          <div className="btn-group call-action-group">
+            <button className="btn btn-cancel"
+                    onClick={this.props.cancelCall}>
+              {mozL10n.get("cancel_button")}
+            </button>
+          </div>
+        </div>
+      );
+    }
+  });
+
+  /**
    * This view manages the incoming conversation views - from
    * call initiation through to the actual conversation and call end.
    *
    * At the moment, it does more than that, these parts need refactoring out.
    */
   var IncomingConversationView = React.createClass({
     propTypes: {
       client: React.PropTypes.instanceOf(loop.Client).isRequired,
@@ -249,21 +278,23 @@ loop.conversation = (function(mozL10n) {
               model={this.props.conversation}
               video={{enabled: callType !== "audio"}}
             />
           );
         }
         case "end": {
           // XXX To be handled with the "failed" view state when bug 1047410 lands
           if (this.state.callFailed) {
-            document.title = mozL10n.get("generic_failure_title");
-          } else {
-            document.title = mozL10n.get("conversation_has_ended");
+            return <IncomingCallFailedView
+              cancelCall={this.closeWindow.bind(this)}
+            />
           }
 
+          document.title = mozL10n.get("conversation_has_ended");
+
           var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
             "feedback.baseUrl");
 
           var appVersionInfo = navigator.mozLoop.appVersionInfo;
 
           var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, {
             product: navigator.mozLoop.getLoopCharPref("feedback.product"),
             platform: appVersionInfo.OS,
@@ -389,37 +420,42 @@ loop.conversation = (function(mozL10n) {
      * @param {Object} progressData The progress data from the websocket.
      * @param {String} previousState The previous state from the websocket.
      */
     _handleWebSocketProgress: function(progressData, previousState) {
       // We only care about the terminated state at the moment.
       if (progressData.state !== "terminated")
         return;
 
-      if (progressData.reason === "cancel" ||
-          progressData.reason === "closed") {
+      // XXX This would be nicer in the _abortIncomingCall function, but we need to stop
+      // it here for now due to server-side issues that are being fixed in bug 1088351.
+      // This is before the abort call to ensure that it happens before the window is
+      // closed.
+      navigator.mozLoop.stopAlerting();
+
+      // If we hit any of the termination reasons, and the user hasn't accepted
+      // then it seems reasonable to close the window/abort the incoming call.
+      //
+      // If the user has accepted the call, and something's happened, display
+      // the call failed view.
+      //
+      // https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons
+      if (previousState === "init" || previousState === "alerting") {
         this._abortIncomingCall();
-        return;
+      } else {
+        this.setState({callFailed: true, callStatus: "end"});
       }
 
-      if (progressData.reason === "timeout") {
-        if (previousState === "init" || previousState === "alerting") {
-          this._abortIncomingCall();
-        } else {
-          this.setState({callFailed: true, callStatus: "end"});
-        }
-      }
     },
 
     /**
      * Silently aborts an incoming call - stops the alerting, and
      * closes the websocket.
      */
     _abortIncomingCall: function() {
-      navigator.mozLoop.stopAlerting();
       this._websocket.close();
       // Having a timeout here lets the logging for the websocket complete and be
       // displayed on the console if both are on.
       setTimeout(this.closeWindow, 0);
     },
 
     closeWindow: function() {
       window.close();
@@ -639,13 +675,14 @@ loop.conversation = (function(mozL10n) {
       outgoing: outgoing
     }));
   }
 
   return {
     AppControllerView: AppControllerView,
     IncomingConversationView: IncomingConversationView,
     IncomingCallView: IncomingCallView,
+    IncomingCallFailedView: IncomingCallFailedView,
     init: init
   };
 })(document.mozL10n);
 
 document.addEventListener('DOMContentLoaded', loop.conversation.init);
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -426,151 +426,83 @@ describe("loop.conversation", function()
                 resolve();
               });
 
               sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
               sandbox.stub(loop.CallConnectionWebSocket.prototype, "close");
               sandbox.stub(window, "close");
             });
 
-            describe("progress - terminated - cancel", function() {
-              it("should stop alerting", function(done) {
-                promise.then(function() {
-                  icView._websocket.trigger("progress", {
-                    state: "terminated",
-                    reason: "cancel"
-                  });
-
-                  sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
-                  done();
-                });
-              });
-
-              it("should close the websocket", function(done) {
-                promise.then(function() {
-                  icView._websocket.trigger("progress", {
-                    state: "terminated",
-                    reason: "cancel"
-                  });
-
-                  sinon.assert.calledOnce(icView._websocket.close);
-                  done();
-                });
-              });
-
-              it("should close the window", function(done) {
-                promise.then(function() {
-                  icView._websocket.trigger("progress", {
-                    state: "terminated",
-                    reason: "cancel"
-                  });
-
-                  sandbox.clock.tick(1);
-
-                  sinon.assert.calledOnce(window.close);
-                  done();
-                });
-              });
-            });
-
-            describe("progress - terminated - closed", function() {
-              it("should stop alerting", function(done) {
-                promise.then(function() {
-                  icView._websocket.trigger("progress", {
-                    state: "terminated",
-                    reason: "closed"
-                  });
-
-                  sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
-                  done();
-                });
-              });
-
-              it("should close the websocket", function(done) {
-                promise.then(function() {
-                  icView._websocket.trigger("progress", {
-                    state: "terminated",
-                    reason: "closed"
-                  });
-
-                  sinon.assert.calledOnce(icView._websocket.close);
-                  done();
-                });
-              });
-
-              it("should close the window", function(done) {
-                promise.then(function() {
-                  icView._websocket.trigger("progress", {
-                    state: "terminated",
-                    reason: "closed"
-                  });
-
-                  sandbox.clock.tick(1);
-
-                  sinon.assert.calledOnce(window.close);
-                  done();
-                });
-              });
-            });
-
-            describe("progress - terminated - timeout (previousState = alerting)", function() {
+            describe("progress - terminated (previousState = alerting)", function() {
               it("should stop alerting", function(done) {
                 promise.then(function() {
                   icView._websocket.trigger("progress", {
                     state: "terminated",
                     reason: "timeout"
                   }, "alerting");
 
                   sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
                   done();
                 });
               });
 
               it("should close the websocket", function(done) {
                 promise.then(function() {
                   icView._websocket.trigger("progress", {
                     state: "terminated",
-                    reason: "timeout"
+                    reason: "closed"
                   }, "alerting");
 
                   sinon.assert.calledOnce(icView._websocket.close);
                   done();
                 });
               });
 
               it("should close the window", function(done) {
                 promise.then(function() {
                   icView._websocket.trigger("progress", {
                     state: "terminated",
-                    reason: "timeout"
+                    reason: "answered-elsewhere"
                   }, "alerting");
 
                   sandbox.clock.tick(1);
 
                   sinon.assert.calledOnce(window.close);
                   done();
                 });
               });
             });
 
-            describe("progress - terminated - timeout (previousState not init" +
+            describe("progress - terminated (previousState not init" +
                      " nor alerting)",
               function() {
                 it("should set the state to end", function(done) {
                   promise.then(function() {
                     icView._websocket.trigger("progress", {
                       state: "terminated",
-                      reason: "timeout"
+                      reason: "media-fail"
                     }, "connecting");
 
                     expect(icView.state.callStatus).eql("end");
                     done();
                   });
                 });
-              });
+
+                it("should stop alerting", function(done) {
+                  promise.then(function() {
+                    icView._websocket.trigger("progress", {
+                      state: "terminated",
+                      reason: "media-fail"
+                    }, "connecting");
+
+                    sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
+                    done();
+                  });
+                });
+            });
           });
         });
       });
 
       describe("#accept", function() {
         beforeEach(function() {
           icView = mountTestComponent();
           conversation.setIncomingSessionData({
@@ -756,22 +688,22 @@ describe("loop.conversation", function()
             conversation.trigger("session:peer-hungup");
 
               TestUtils.findRenderedComponentWithType(icView,
                 sharedView.FeedbackView);
           });
       });
 
       describe("session:network-disconnected", function() {
-        it("should navigate to call/feedback when network disconnects",
+        it("should navigate to call failed when network disconnects",
           function() {
             conversation.trigger("session:network-disconnected");
 
               TestUtils.findRenderedComponentWithType(icView,
-                sharedView.FeedbackView);
+                loop.conversation.IncomingCallFailedView);
           });
 
         it("should update the conversation window toolbar title",
           function() {
             conversation.trigger("session:network-disconnected");
 
             expect(document.title).eql("generic_failure_title");
           });
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/xpcshell/test_rooms_create.js
@@ -0,0 +1,103 @@
+/* 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/. */
+
+Cu.import("resource://services-common/utils.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Chat",
+                                  "resource:///modules/Chat.jsm");
+let hasTheseProps = function(a, b) {
+  for (let prop in a) {
+    if (a[prop] != b[prop]) {
+      return false;
+    }
+  }
+  return true;
+}
+
+let openChatOrig = Chat.open;
+
+add_test(function test_openRoomsWindow() {
+  let roomProps = {roomName: "UX Discussion",
+                   expiresIn: 5,
+                   roomOwner: "Alexis",
+                   maxSize: 2}
+
+  let roomData = {roomToken: "_nxD4V4FflQ",
+                  roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ",
+                  expiresAt: 1405534180}
+
+  loopServer.registerPathHandler("/rooms", (request, response) => {
+    if (!request.bodyInputStream) {
+      do_throw("empty request body");
+    }
+    let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+    let data = JSON.parse(body);
+    do_check_true(hasTheseProps(roomProps, data));
+
+    response.setStatusLine(null, 200, "OK");
+    response.write(JSON.stringify(roomData));
+    response.processAsync();
+    response.finish();
+  });
+
+  MozLoopService.register(mockPushHandler).then(() => {
+    let opened = false;
+    let created = false;
+    let urlPieces = [];
+
+    Chat.open = function(contentWindow, origin, title, url) {
+      urlPieces = url.split('/');
+      do_check_eq(urlPieces[0], "about:loopconversation#room");
+      opened = true;
+    };
+
+    let returnedID = LoopRooms.createRoom(roomProps, (error, data) => {
+      do_check_false(error);
+      do_check_true(data);
+      do_check_true(hasTheseProps(roomData, data));
+      do_check_eq(data.localRoomId, urlPieces[1]);
+      created = true;
+    });
+
+    waitForCondition(function() created && opened).then(() => {
+      do_check_true(opened, "should open a chat window");
+      do_check_eq(returnedID, urlPieces[1]);
+
+      // Verify that a delayed callback, when attached,
+      // received the same data.
+      LoopRooms.addCallback(
+        urlPieces[1], "RoomCreated",
+        (error, data) => {
+          do_check_false(error);
+          do_check_true(data);
+          do_check_true(hasTheseProps(roomData, data));
+          do_check_eq(data.localRoomId, urlPieces[1]);
+        });
+
+      run_next_test();
+    }, () => {
+      do_throw("should have opened a chat window");
+    });
+
+  });
+});
+
+function run_test()
+{
+  setupFakeLoopServer();
+  mockPushHandler.registrationPushURL = kEndPointUrl;
+
+  loopServer.registerPathHandler("/registration", (request, response) => {
+    response.setStatusLine(null, 200, "OK");
+    response.processAsync();
+    response.finish();
+  });
+
+  do_register_cleanup(function() {
+    // Revert original Chat.open implementation
+    Chat.open = openChatOrig;
+  });
+
+  run_next_test();
+}
--- a/browser/components/loop/test/xpcshell/test_rooms_getdata.js
+++ b/browser/components/loop/test/xpcshell/test_rooms_getdata.js
@@ -89,24 +89,26 @@ add_test(function test_getAllRooms() {
       do_check_false(error);
       do_check_true(rooms);
       do_check_eq(rooms.length, 3);
       do_check_eq(rooms[0].roomName, "First Room Name");
       do_check_eq(rooms[1].roomName, "Second Room Name");
       do_check_eq(rooms[2].roomName, "Third Room Name");
 
       let room = rooms[0];
-      do_check_true(room.localRoomID);
+      do_check_true(room.localRoomId);
+      do_check_false(room.currSize);
+      delete roomList[0].currSize;
       do_check_true(hasTheseProps(roomList[0], room));
       delete roomDetail.roomName;
       delete room.participants;
       delete roomDetail.participants;
       do_check_true(hasTheseProps(roomDetail, room));
 
-      LoopRooms.getRoomData(room.localRoomID, (error, roomData) => {
+      LoopRooms.getRoomData(room.localRoomId, (error, roomData) => {
         do_check_false(error);
         do_check_true(hasTheseProps(room, roomData));
 
         run_next_test();
       });
     });
   });
 });
--- a/browser/components/loop/test/xpcshell/xpcshell.ini
+++ b/browser/components/loop/test/xpcshell/xpcshell.ini
@@ -17,8 +17,9 @@ skip-if = toolkit == 'gonk'
 [test_loopservice_registration.js]
 [test_loopservice_restart.js]
 [test_loopservice_token_invalid.js]
 [test_loopservice_token_save.js]
 [test_loopservice_token_send.js]
 [test_loopservice_token_validation.js]
 [test_loopservice_busy.js]
 [test_rooms_getdata.js]
+[test_rooms_create.js]
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1618,21 +1618,35 @@ BrowserGlue.prototype = {
         } catch (ex) {}
       }
     }
 
     if (currentUIVersion < 24) {
       // Reset homepage pref for users who have it set to start.mozilla.org
       // or google.com/firefox.
       const HOMEPAGE_PREF = "browser.startup.homepage";
-      let uri = Services.prefs.getComplexValue(HOMEPAGE_PREF,
-                                               Ci.nsIPrefLocalizedString).data;
-      if (uri && (uri.startsWith("http://start.mozilla.org") ||
-                  /^https?:\/\/(www\.)?google\.[a-z.]+\/firefox/i.test(uri))) {
-        Services.prefs.clearUserPref(HOMEPAGE_PREF);
+      if (Services.prefs.prefHasUserValue(HOMEPAGE_PREF)) {
+        const DEFAULT =
+          Services.prefs.getDefaultBranch(HOMEPAGE_PREF)
+                        .getComplexValue("", Ci.nsIPrefLocalizedString).data;
+        let value =
+          Services.prefs.getComplexValue(HOMEPAGE_PREF, Ci.nsISupportsString);
+        let updated =
+          value.data.replace(/https?:\/\/start\.mozilla\.org[^|]*/i, DEFAULT)
+                    .replace(/https?:\/\/(www\.)?google\.[a-z.]+\/firefox[^|]*/i,
+                             DEFAULT);
+        if (updated != value.data) {
+          if (updated == DEFAULT) {
+            Services.prefs.clearUserPref(HOMEPAGE_PREF);
+          } else {
+            value.data = updated;
+            Services.prefs.setComplexValue(HOMEPAGE_PREF,
+                                           Ci.nsISupportsString, value);
+          }
+        }
       }
     }
 
     if (currentUIVersion < 25) {
       // Make sure the doNotTrack value conforms to the conversion from
       // three-state to two-state. (This reverts a setting of "please track me"
       // to the default "don't say anything").
       try {
--- a/browser/devtools/styleinspector/css-parsing-utils.js
+++ b/browser/devtools/styleinspector/css-parsing-utils.js
@@ -87,16 +87,17 @@ function parseDeclarations(inputString) 
           current += "#" + token.value;
           break;
         case "URL":
           current += "url(" + quoteString(token.value) + ")";
           break;
         case "FUNCTION":
           current += token.value + "(";
           break;
+        case "(":
         case ")":
           current += token.tokenType;
           break;
         case "EOF":
           break;
         case "DELIM":
           if (token.value === "!") {
             hasBang = true;
--- a/browser/devtools/styleinspector/test/unit/test_parseDeclarations.js
+++ b/browser/devtools/styleinspector/test/unit/test_parseDeclarations.js
@@ -158,17 +158,19 @@ const TEST_DATA = [
   {input: "content: 'this \\' is a \" really strange string'", expected: [{name: "content", value: '"this \' is a \" really strange string"', priority: ""}]},
   {
     input: "content: \"a not s\\\
           o very long title\"",
     expected: [
       {name: "content", value: '"a not s\
           o very long title"', priority: ""}
     ]
-  }
+  },
+  // Test calc with nested parentheses
+  {input: "width: calc((100% - 3em) / 2)", expected: [{name: "width", value: "calc((100% - 3em) / 2)", priority: ""}]},
 ];
 
 function run_test() {
   for (let test of TEST_DATA) {
     do_print("Test input string " + test.input);
     let output;
     try {
       output = parseDeclarations(test.input);
--- a/build/appini_header.py
+++ b/build/appini_header.py
@@ -18,33 +18,34 @@ def main(file):
     except: pass
     try:
         if config.getint('Crash Reporter', 'Enabled') == 1:
             flags.add('NS_XRE_ENABLE_CRASH_REPORTER')
     except: pass
     appdata = dict(("%s:%s" % (s, o), config.get(s, o)) for s in config.sections() for o in config.options(s))
     appdata['flags'] = ' | '.join(flags) if flags else '0'
     appdata['App:profile'] = '"%s"' % appdata['App:profile'] if 'App:profile' in appdata else 'NULL'
-    expected = ('App:vendor', 'App:name', 'App:version', 'App:buildid',
+    expected = ('App:vendor', 'App:name', 'App:remotingname', 'App:version', 'App:buildid',
                 'App:id', 'Gecko:minversion', 'Gecko:maxversion')
     missing = [var for var in expected if var not in appdata]
     if missing:
         print >>sys.stderr, \
             "Missing values in %s: %s" % (file, ', '.join(missing))
         sys.exit(1)
 
     if not 'Crash Reporter:serverurl' in appdata:
         appdata['Crash Reporter:serverurl'] = ''
 
     print '''#include "nsXREAppData.h"
              static const nsXREAppData sAppData = {
                  sizeof(nsXREAppData),
                  NULL, // directory
                  "%(App:vendor)s",
                  "%(App:name)s",
+                 "%(App:remotingname)s",
                  "%(App:version)s",
                  "%(App:buildid)s",
                  "%(App:id)s",
                  NULL, // copyright
                  %(flags)s,
                  NULL, // xreDirectory
                  "%(Gecko:minversion)s",
                  "%(Gecko:maxversion)s",
--- a/build/application.ini
+++ b/build/application.ini
@@ -13,16 +13,17 @@
 ; 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/.
 #endif
 #filter substitution
 [App]
 Vendor=@MOZ_APP_VENDOR@
 Name=@MOZ_APP_BASENAME@
+RemotingName=@MOZ_APP_REMOTINGNAME@
 #ifdef MOZ_APP_DISPLAYNAME
 CodeName=@MOZ_APP_DISPLAYNAME@
 #endif
 Version=@MOZ_APP_VERSION@
 #ifdef MOZ_APP_PROFILE
 Profile=@MOZ_APP_PROFILE@
 #endif
 BuildID=@APP_BUILDID@
--- a/build/moz.build
+++ b/build/moz.build
@@ -22,17 +22,17 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'andr
         'mobile/sutagent/android/watcher',
         'mobile/sutagent/android/ffxcp',
         'mobile/sutagent/android/fencp',
         'mobile/robocop',
     ]
 
 for var in ('GRE_MILESTONE', 'MOZ_APP_VERSION', 'MOZ_APP_BASENAME',
             'MOZ_APP_VENDOR', 'MOZ_APP_ID', 'MAR_CHANNEL_ID',
-            'ACCEPTED_MAR_CHANNEL_IDS'):
+            'ACCEPTED_MAR_CHANNEL_IDS', 'MOZ_APP_REMOTINGNAME'):
     DEFINES[var] = CONFIG[var]
 
 if CONFIG['MOZ_APP_DISPLAYNAME'] != CONFIG['MOZ_APP_BASENAME']:
     DEFINES['MOZ_APP_DISPLAYNAME'] = CONFIG['MOZ_APP_DISPLAYNAME']
 
 if CONFIG['MOZ_BUILD_APP'] == 'browser':
     DEFINES['MOZ_BUILD_APP_IS_BROWSER'] = True
 
--- a/configure.in
+++ b/configure.in
@@ -8678,26 +8678,32 @@ AC_SUBST(MOZ_CHILD_PROCESS_BUNDLE)
 # the absence of a "Profile" field (see below), and various system
 # integration hooks (Unix remoting, Windows MessageWindow name, etc.)
 # - MOZ_APP_DISPLAYNAME: Used in user-visible fields (DLL properties,
 # Mac Bundle name, Updater, Installer), it is typically used for nightly
 # builds (e.g. Aurora for Firefox).
 # - MOZ_APP_VERSION: Defines the application version number.
 # - MOZ_APP_NAME: Used for e.g. the binary program file name. If not set,
 # defaults to a lowercase form of MOZ_APP_BASENAME.
+# - MOZ_APP_REMOTINGNAME: Used for the internal program name, which affects
+# profile name and remoting. If not set, defaults to MOZ_APP_NAME.
 # - MOZ_APP_PROFILE: When set, used for application.ini's
 # "Profile" field, which controls profile location.
 # - MOZ_APP_ID: When set, used for application.ini's "ID" field, and
 # crash reporter server url.
 # - MOZ_PROFILE_MIGRATOR: When set, enables profile migrator.
 
 if test -z "$MOZ_APP_NAME"; then
    MOZ_APP_NAME=`echo $MOZ_APP_BASENAME | tr A-Z a-z`
 fi
 
+if test -z "$MOZ_APP_REMOTINGNAME"; then
+   MOZ_APP_REMOTINGNAME=$MOZ_APP_NAME
+fi
+
 # For extensions and langpacks, we require a max version that is compatible
 # across security releases. MOZ_APP_MAXVERSION is our method for doing that.
 # 24.0a1 and 24.0a2 aren't affected
 # 24.0 becomes 24.*
 # 24.1.1 becomes 24.*
 IS_ALPHA=`echo $MOZ_APP_VERSION | grep a`
 if test -z "$IS_ALPHA"; then
   changequote(,)
@@ -8707,16 +8713,17 @@ else
   MOZ_APP_MAXVERSION=$MOZ_APP_VERSION
 fi
 
 MOZ_B2G_VERSION=${MOZ_B2G_VERSION:-"1.0.0"}
 AC_DEFINE_UNQUOTED(MOZ_B2G_VERSION,"$MOZ_B2G_VERSION")
 AC_DEFINE_UNQUOTED(MOZ_B2G_OS_NAME,"$MOZ_B2G_OS_NAME")
 
 AC_SUBST(MOZ_APP_NAME)
+AC_SUBST(MOZ_APP_REMOTINGNAME)
 AC_SUBST(MOZ_APP_DISPLAYNAME)
 AC_SUBST(MOZ_APP_BASENAME)
 AC_SUBST(MOZ_APP_VENDOR)
 AC_SUBST(MOZ_APP_PROFILE)
 AC_SUBST(MOZ_APP_ID)
 AC_SUBST(MAR_CHANNEL_ID)
 AC_SUBST(ACCEPTED_MAR_CHANNEL_IDS)
 AC_SUBST(MOZ_PROFILE_MIGRATOR)
@@ -8999,16 +9006,20 @@ if test "$ACCESSIBILITY" -a "$MOZ_ENABLE
     ATK_MAJOR_VERSION=`echo ${ATK_FULL_VERSION} | $AWK -F\. '{ print $1 }'`
     ATK_MINOR_VERSION=`echo ${ATK_FULL_VERSION} | $AWK -F\. '{ print $2 }'`
     ATK_REV_VERSION=`echo ${ATK_FULL_VERSION} | $AWK -F\. '{ print $3 }'`
     AC_DEFINE_UNQUOTED(ATK_MAJOR_VERSION, $ATK_MAJOR_VERSION)
     AC_DEFINE_UNQUOTED(ATK_MINOR_VERSION, $ATK_MINOR_VERSION)
     AC_DEFINE_UNQUOTED(ATK_REV_VERSION, $ATK_REV_VERSION)
 fi
 
+if test "$MOZ_UPDATE_CHANNEL" = "aurora"; then
+    AC_DEFINE(MOZ_DEV_EDITION)
+fi
+
 if test "$MOZ_DEBUG"; then
     A11Y_LOG=1
 fi
 case "$MOZ_UPDATE_CHANNEL" in
 aurora|beta|release|esr)
     ;;
 *)
     A11Y_LOG=1
@@ -9303,16 +9314,17 @@ if test -n "$MOZ_GLUE_PROGRAM_LDFLAGS"; 
 fi
 if test -n "$ZLIB_IN_MOZGLUE"; then
    MOZ_ZLIB_LIBS=
 fi
 export MOZ_NATIVE_ZLIB
 export MOZ_ZLIB_CFLAGS
 export MOZ_ZLIB_LIBS
 export MOZ_APP_NAME
+export MOZ_APP_REMOTINGNAME
 export DONT_POPULATE_VIRTUALENV=1
 export PYTHON
 export MOZILLA_CENTRAL_PATH=$_topsrcdir
 export STLPORT_CPPFLAGS
 export STLPORT_LIBS
 export JS_STANDALONE=no
 export MOZ_LINKER
 export ZLIB_IN_MOZGLUE
--- a/content/base/test/mochitest.ini
+++ b/content/base/test/mochitest.ini
@@ -583,16 +583,17 @@ skip-if = e10s
 skip-if = buildapp == 'b2g' || toolkit == 'android' || e10s #bug 775227
 [test_getElementById.html]
 [test_html_colors_quirks.html]
 [test_html_colors_standards.html]
 [test_html_in_xhr.html]
 [test_htmlcopyencoder.html]
 [test_htmlcopyencoder.xhtml]
 [test_ipc_messagemanager_blob.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 936226
 [test_meta_viewport0.html]
 [test_meta_viewport1.html]
 [test_meta_viewport2.html]
 [test_meta_viewport3.html]
 [test_meta_viewport4.html]
 [test_meta_viewport5.html]
 [test_meta_viewport6.html]
 [test_mixed_content_blocker.html]
--- a/content/html/content/test/forms/mochitest.ini
+++ b/content/html/content/test/forms/mochitest.ini
@@ -53,16 +53,17 @@ skip-if = buildapp == 'mulet' || os == "
 [test_input_range_key_events.html]
 skip-if = buildapp == 'mulet' || (buildapp == 'b2g' && toolkit != 'gonk') #Bug 931116, b2g desktop specific, initial triage
 [test_input_range_mouse_and_touch_events.html]
 skip-if = (toolkit == 'gonk' && debug) #debug-only failure; bug 926546
 [test_input_range_rounding.html]
 skip-if = buildapp == 'mulet' || (buildapp == 'b2g' && toolkit != 'gonk') #Bug 931116, b2g desktop specific, initial triage
 [test_input_sanitization.html]
 [test_input_textarea_set_value_no_scroll.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
 [test_input_typing_sanitization.html]
 skip-if = buildapp == 'mulet'
 [test_input_untrusted_key_events.html]
 [test_input_url.html]
 [test_label_control_attribute.html]
 [test_label_input_controls.html]
 [test_max_attribute.html]
 skip-if = e10s
--- a/content/media/test/mochitest.ini
+++ b/content/media/test/mochitest.ini
@@ -301,46 +301,54 @@ support-files =
   wavedata_u8.wav
   wavedata_u8.wav^headers^
 
 [test_access_control.html]
 skip-if = buildapp == 'b2g' && toolkit != 'gonk' # bug 1082984
 [test_aspectratio_mp4.html]
 [test_audio1.html]
 [test_audio2.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_audioDocumentTitle.html]
 skip-if = true # bug 475110 - disabled since we don't play Wave files standalone
 [test_autoplay.html]
 [test_autoplay_contentEditable.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
 [test_buffered.html]
 [test_bug448534.html]
-skip-if = buildapp == 'mulet' || os == 'win' # bug 894922
+skip-if = buildapp == 'mulet' || os == 'win' || (toolkit == 'android' && processor == 'x86') # bug 894922 #x86 only bug 914439
 [test_bug463162.xhtml]
 [test_bug465498.html]
+skip-if = (toolkit == 'android' && processor == 'x86')
 [test_bug493187.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_bug495145.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_bug495300.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_bug654550.html]
 [test_bug686942.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_bug726904.html]
 [test_bug874897.html]
 [test_bug883173.html]
 [test_bug895091.html]
 [test_bug895305.html]
 [test_bug919265.html]
 [test_bug957847.html]
 [test_bug1018933.html]
 [test_can_play_type.html]
 [test_can_play_type_mpeg.html]
-skip-if = buildapp == 'b2g' # bug 1021675
+skip-if = buildapp == 'b2g' || (toolkit == 'android' && processor == 'x86') # bug 1021675 #x86 only bug 914439
 [test_can_play_type_no_ogg.html]
 [test_can_play_type_ogg.html]
 [test_chaining.html]
 skip-if = toolkit == 'gonk' && debug
 [test_clone_media_element.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_closing_connections.html]
 [test_constants.html]
 [test_contentDuration1.html]
 [test_contentDuration2.html]
 [test_contentDuration3.html]
 [test_contentDuration4.html]
 [test_contentDuration5.html]
 [test_contentDuration6.html]
@@ -353,29 +361,36 @@ skip-if = toolkit == 'gonk' && debug
 [test_delay_load.html]
 skip-if = buildapp == 'b2g' && toolkit != 'gonk' # bug 1082984
 [test_encryptedMediaExtensions.html]
 skip-if = buildapp == 'b2g' || toolkit == 'android' || e10s # bug 1043403, bug 1057908
 [test_error_in_video_document.html]
 skip-if = toolkit == 'android' # bug 608634
 [test_error_on_404.html]
 [test_fastSeek.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_fastSeek-forwards.html]
 [test_imagecapture.html]
 [test_info_leak.html]
 [test_invalid_reject.html]
 [test_invalid_reject_play.html]
 [test_invalid_seek.html]
 [test_load.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_load_candidates.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_load_same_resource.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_load_source.html]
 [test_loop.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_media_selection.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_media_sniffer.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_mediarecorder_avoid_recursion.html]
 [test_mediarecorder_creation.html]
 [test_mediarecorder_creation_fail.html]
 [test_mediarecorder_getencodeddata.html]
 [test_mediarecorder_record_4ch_audiocontext.html]
 [test_mediarecorder_record_audiocontext.html]
 [test_mediarecorder_record_audiocontext_mlk.html]
 [test_mediarecorder_record_audionode.html]
@@ -403,84 +418,103 @@ skip-if = toolkit == 'gonk' && debug # b
 [test_mediatrack_parsing_ogg.html]
 [test_mediatrack_replay_from_end.html]
 [test_metadata.html]
 [test_mixed_principals.html]
 skip-if = true # bug 567954 and intermittent leaks
 [test_mozHasAudio.html]
 [test_networkState.html]
 [test_new_audio.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_no_load_event.html]
 [test_paused.html]
 [test_paused_after_ended.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_play_events.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_play_events_2.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_play_twice.html]
 # Seamonkey: Bug 598252, B2G: Bug 982100, Android: Bug 758476, bug 981086
 skip-if = appname == "seamonkey" || toolkit == 'gonk' || toolkit == 'android'
 [test_playback.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_playback_errors.html]
 [test_playback_rate.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #bug 845162
 [test_playback_rate_playpause.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_played.html]
 skip-if = true # bug 1021794
 [test_preload_actions.html]
 [test_preload_attribute.html]
 [test_preload_suspend.html]
 skip-if = true # bug 493692
 [test_progress.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_reactivate.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_readyState.html]
 [test_referer.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
 [test_replay_metadata.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_reset_events_async.html]
 [test_reset_src.html]
 [test_resume.html]
 skip-if = true # bug 1021673
 [test_seek_out_of_range.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_seek-1.html]
 [test_seek-2.html]
 [test_seek-3.html]
 [test_seek-4.html]
 [test_seek-5.html]
 [test_seek-6.html]
 [test_seek-7.html]
 [test_seek-8.html]
 [test_seek-9.html]
 [test_seek-10.html]
 [test_seek-11.html]
 [test_seek-12.html]
 [test_seek-13.html]
 [test_seekable1.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #timeout x86 only bug 914439
 [test_seekable2.html]
 [test_seekable3.html]
 [test_seekLies.html]
 [test_source.html]
 [test_source_media.html]
 [test_source_null.html]
 [test_source_write.html]
 [test_standalone.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_streams_autoplay.html]
 [test_streams_element_capture.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_streams_element_capture_createObjectURL.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_streams_element_capture_playback.html]
 [test_streams_element_capture_reset.html]
 [test_streams_gc.html]
 skip-if = buildapp == 'b2g' # bug 1021682
 [test_streams_srcObject.html]
 [test_streams_tracks.html]
 [test_texttrack.html]
 [test_texttrackcue.html]
 [test_texttracklist.html]
 [test_texttrackregion.html]
 [test_timeupdate_small_files.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_trackelementevent.html]
 [test_trackevent.html]
 [test_unseekable.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
 [test_video_to_canvas.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 914439
 [test_video_in_audio_element.html]
 [test_videoDocumentTitle.html]
 [test_VideoPlaybackQuality.html]
 [test_VideoPlaybackQuality_disabled.html]
 [test_volume.html]
 [test_vttparser.html]
 [test_webvtt_disabled.html]
 
--- a/content/media/webaudio/test/mochitest.ini
+++ b/content/media/webaudio/test/mochitest.ini
@@ -30,16 +30,17 @@ support-files =
 [test_AudioBuffer.html]
 [test_audioBufferSourceNode.html]
 [test_audioBufferSourceNodeEnded.html]
 [test_audioBufferSourceNodeLazyLoopParam.html]
 [test_audioBufferSourceNodeLoop.html]
 [test_audioBufferSourceNodeLoopStartEnd.html]
 [test_audioBufferSourceNodeLoopStartEndSame.html]
 [test_audioBufferSourceNodeNeutered.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
 [test_audioBufferSourceNodeNoStart.html]
 [test_audioBufferSourceNodeNullBuffer.html]
 [test_audioBufferSourceNodeOffset.html]
 skip-if = (toolkit == 'gonk') || (toolkit == 'android') || debug #bug 906752
 [test_audioBufferSourceNodePassThrough.html]
 [test_AudioContext.html]
 [test_audioDestinationNode.html]
 [test_AudioListener.html]
--- a/docshell/test/navigation/mochitest.ini
+++ b/docshell/test/navigation/mochitest.ini
@@ -33,17 +33,17 @@ skip-if = buildapp == 'mulet' || (builda
 [test_bug386782.html]
 skip-if = (buildapp == 'b2g' && (toolkit != 'gonk' || debug))
 [test_bug430624.html]
 [test_bug430723.html]
 skip-if = (buildapp == 'b2g' && (toolkit != 'gonk' || debug)) || toolkit == 'android' #TIMED_OUT # b2g-debug(bug 874423) b2g-desktop(Bug 931116, b2g desktop specific, initial triage)
 [test_child.html]
 [test_grandchild.html]
 [test_not-opener.html]
-skip-if = buildapp == 'b2g'
+skip-if = buildapp == 'b2g' || (toolkit == 'android' && processor == 'x86') #x86 only
 [test_opener.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') #Bug 931116, b2g desktop specific, initial triage
 [test_popup-navigates-children.html]
 skip-if = buildapp == 'b2g' # b2g(Needs multiple window.open support, also uses docshelltreenode) b2g-debug(Needs multiple window.open support, also uses docshelltreenode) b2g-desktop(Needs multiple window.open support, also uses docshelltreenode)
 [test_reserved.html]
 skip-if = (buildapp == 'b2g' && (toolkit != 'gonk' || debug))
 [test_sessionhistory.html]
 skip-if = (buildapp == 'b2g' && (toolkit != 'gonk' || debug)) || toolkit == 'android' #RANDOM # b2g-debug(Perma-orange on debug emulator builds) b2g-desktop(Bug 931116, b2g desktop specific, initial triage)
--- a/dom/apps/tests/mochitest.ini
+++ b/dom/apps/tests/mochitest.ini
@@ -26,14 +26,16 @@ support-files =
 [test_app_update.html]
 [test_bug_795164.html]
 [test_import_export.html]
 [test_install_multiple_apps_origin.html]
 [test_install_receipts.html]
 [test_marketplace_pkg_install.html]
 skip-if = buildapp == "b2g" || toolkit == "android" # see bug 989806
 [test_packaged_app_install.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
 [test_packaged_app_update.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
 [test_receipt_operations.html]
 [test_signed_pkg_install.html]
 [test_uninstall_errors.html]
 [test_theme_role.html]
 [test_widget.html]
--- a/dom/browser-element/mochitest/mochitest.ini
+++ b/dom/browser-element/mochitest/mochitest.ini
@@ -148,16 +148,17 @@ skip-if = buildapp == 'b2g'
 [test_browserElement_inproc_Download.html]
 disabled = bug 1022281
 [test_browserElement_inproc_ExposableURI.html]
 [test_browserElement_inproc_FirstPaint.html]
 [test_browserElement_inproc_ForwardName.html]
 [test_browserElement_inproc_FrameWrongURI.html]
 skip-if = (toolkit == 'gonk' && !debug)
 [test_browserElement_inproc_GetScreenshot.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
 [test_browserElement_inproc_GetScreenshotDppx.html]
 [test_browserElement_inproc_Iconchange.html]
 [test_browserElement_inproc_KeyEvents.html]
 skip-if = (toolkit == 'gonk' && !debug)
 [test_browserElement_inproc_LoadEvents.html]
 [test_browserElement_inproc_Manifestchange.html]
 [test_browserElement_inproc_Metachange.html]
 [test_browserElement_inproc_NextPaint.html]
@@ -195,15 +196,16 @@ skip-if = (toolkit == 'gonk' && !debug)
 [test_browserElement_inproc_Titlechange.html]
 [test_browserElement_inproc_TopBarrier.html]
 [test_browserElement_inproc_VisibilityChange.html]
 [test_browserElement_inproc_XFrameOptions.html]
 [test_browserElement_inproc_XFrameOptionsAllowFrom.html]
 [test_browserElement_inproc_XFrameOptionsDeny.html]
 [test_browserElement_inproc_XFrameOptionsSameOrigin.html]
 [test_browserElement_oop_NextPaint.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 936226
 # Disabled due to https://bugzilla.mozilla.org/show_bug.cgi?id=774100
 [test_browserElement_inproc_Reload.html]
 disabled = bug 774100
 # Disabled due to focus issues (no bug that I'm aware of)
 [test_browserElement_oop_KeyEvents.html]
 disabled =
 [test_browserElement_inproc_GetContentDimensions.html]
--- a/dom/canvas/test/mochitest.ini
+++ b/dom/canvas/test/mochitest.ini
@@ -20,37 +20,59 @@ support-files =
   image_transparent.png
   image_transparent50.png
   image_yellow.png
   image_yellow75.png
 
 [test_2d.clearRect.image.offscreen.html]
 [test_2d.clip.winding.html]
 [test_2d.composite.canvas.color-burn.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.canvas.color-dodge.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.canvas.color.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.canvas.darken.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.canvas.destination-atop.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.canvas.destination-in.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.canvas.difference.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.canvas.exclusion.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.canvas.hard-light.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.canvas.hue.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.canvas.lighten.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.canvas.luminosity.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.canvas.multiply.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.canvas.overlay.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.canvas.saturation.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.canvas.screen.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.canvas.soft-light.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.canvas.source-in.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.canvas.source-out.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.image.destination-atop.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.image.destination-in.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.image.source-in.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.image.source-out.html]
 # xor and lighter aren't well handled by cairo; they mostly work, but we don't want
 # to test that
 [test_2d.composite.solid.xor.html]
 disabled =
 [test_2d.composite.solid.lighter.html]
 disabled =
 [test_2d.composite.transparent.xor.html]
@@ -107,18 +129,21 @@ skip-if = os == 'linux' && buildapp == '
 skip-if = toolkit != 'cocoa'
 [test_2d.composite.uncovered.fill.destination-in.html]
 skip-if = toolkit != 'cocoa'
 [test_2d.composite.uncovered.fill.source-out.html]
 skip-if = toolkit != 'cocoa'
 [test_2d.composite.uncovered.fill.destination-atop.html]
 skip-if = toolkit != 'cocoa'
 [test_2d.composite.uncovered.image.destination-in.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.uncovered.image.source-in.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 [test_2d.composite.uncovered.image.source-out.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 913662
 # Tests that fail on non-Mac (bug 407107)
 [test_2d.composite.uncovered.pattern.source-in.html]
 skip-if = toolkit != 'cocoa'
 [test_2d.composite.uncovered.pattern.destination-in.html]
 skip-if = toolkit != 'cocoa'
 [test_2d.composite.uncovered.pattern.source-out.html]
 skip-if = toolkit != 'cocoa'
 [test_2d.composite.uncovered.pattern.destination-atop.html]
--- a/dom/datastore/tests/mochitest.ini
+++ b/dom/datastore/tests/mochitest.ini
@@ -30,22 +30,25 @@ support-files =
   file_basic_common.js
   file_sync_common.js
   file_bug1008044.html
   file_bug957086.html
   file_notify_system_message.html
 
 [test_app_install.html]
 [test_readonly.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 936226
 [test_basic.html]
 [test_basic_worker.html]
 [test_changes.html]
 [test_arrays.html]
 [test_oop.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 936226
 [test_sync.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only bug 936226
 [test_sync_worker.html]
 [test_bug924104.html]
 [test_certifiedApp.html]
 [test_keys.html]
 [test_duplicate.html]
 [test_bug976311.html]
 [test_bug986056.html]
 [test_oop_events.html]
--- a/image/test/mochitest/mochitest.ini
+++ b/image/test/mochitest/mochitest.ini
@@ -61,30 +61,36 @@ support-files =
 [test_bug399925.html]
 # [test_bug435296.html]
 # disabled - See bug 578591
 [test_bug466586.html]
 [test_bug468160.html]
 # [test_bug478398.html]
 # disabled - See bug 579139
 [test_bug490949.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
 [test_bug496292.html]
 [test_bug497665.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
 [test_bug512435.html]
 [test_bug552605-1.html]
 [test_bug552605-2.html]
 [test_bug553982.html]
 [test_bug601470.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
 [test_bug614392.html]
 [test_bug657191.html]
 [test_bug671906.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
 [test_bug733553.html]
 [test_bug767779.html]
 [test_bug865919.html]
 [test_bug89419-1.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
 [test_bug89419-2.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
 [test_animation_operators.html]
 [test_drawDiscardedImage.html]
 skip-if = toolkit == "gonk" #Bug 997034 - canvas.toDataURL() often causes lost connection to device.
 [test_error_events.html]
 [test_short_gif_header.html]
 [test_image_buffer_limit.html]
 run-if = toolkit == "gonk" #Image buffer limit is only set for Firefox OS currently.
--- a/python/mozbuild/mozbuild/backend/templates/android_eclipse/project.properties
+++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse/project.properties
@@ -4,11 +4,11 @@
 #
 # This file must be checked in Version Control Systems.
 #
 # To customize properties used by the Ant build system edit
 # "ant.properties", and override values to adapt the script to your
 # project structure.
 
 # Project target.
-target=@ANDROID_TARGET_SDK@
+target=android-@ANDROID_TARGET_SDK@
 @IDE_PROJECT_LIBRARY_SETTING@
 @IDE_PROJECT_LIBRARY_REFERENCES@
--- a/testing/mochitest/androidx86.json
+++ b/testing/mochitest/androidx86.json
@@ -1,113 +1,32 @@
 {
 "runtests": {},
 "excludetests": {
-  "content/base/test/test_ipc_messagemanager_blob.html": "x86 only bug 936226",
   "content/base/test/test_copypaste.xul": "bug 904183",
-  "content/html/content/test/forms/test_input_textarea_set_value_no_scroll.html": "x86 only",
-  "content/media/test/test_clone_media_element.html": "x86 only bug 914439",
-  "content/media/test/test_fastSeek.html": "x86 only bug 914439",
-  "content/media/test/test_media_selection.html": "x86 only bug 914439",
-  "content/media/test/test_bug495300.html": "x86 only bug 914439",
-  "content/media/test/test_new_audio.html": "x86 only bug 914439",
   "content/media/test/test_seeked.html": "x86 only bug 914439",
   "content/media/test/test_played.html": "bug 751539",
-  "content/media/test/test_playback_rate.html": "bug 845162",
-  "content/media/test/test_playback_rate_playpause.html": "x86 only bug 914439",
-  "content/media/test/test_loop.html": "x86 only bug 914439",
   "content/media/test/test_play_twice.html": "x86 only bug 914439",
-  "content/media/test/test_bug495145.html": "x86 only bug 914439",
-  "content/media/test/test_bug465498.html": "",
-  "content/media/test/test_play_events_2.html": "x86 only bug 914439",
-  "content/media/test/test_media_sniffer.html": "x86 only bug 914439",
-  "content/media/test/test_seek_out_of_range.html": "x86 only bug 914439",
-  "content/media/test/test_load_same_resource.html": "x86 only bug 914439",
-  "content/media/test/test_bug493187.html": "x86 only bug 914439",
-  "content/media/test/test_autoplay_contentEditable.html": "x86 only",
-  "content/media/test/test_audio2.html": "x86 only bug 914439",
-  "content/media/test/test_can_play_type_mpeg.html": "x86 only bug 914439",
-  "content/media/test/test_bug686942.html": "x86 only bug 914439",
-  "content/media/test/test_standalone.html": "x86 only bug 914439",
-  "content/media/test/test_bug448534.html": "x86 only bug 914439",
-  "content/media/test/test_video_to_canvas.html": "x86 only bug 914439",
-  "content/media/test/test_load_candidates.html": "x86 only bug 914439",
-  "content/media/test/test_streams_element_capture_createObjectURL.html": "x86 only bug 914439",
-  "content/media/test/test_progress.html": "x86 only bug 914439",
-  "content/media/test/test_seekable1.html": "timeout x86 only bug 914439",
-  "content/media/test/test_play_events.html": "x86 only bug 914439",
-  "content/media/test/test_reactivate.html": "x86 only bug 914439",
-  "content/media/test/test_referer.html": "x86 only",
-  "content/media/webaudio/test/test_audioBufferSourceNodeNeutered.html": "x86 only",
-  "content/media/test/test_load.html": "x86 only bug 914439",
-  "content/media/test/test_streams_element_capture.html": "x86 only bug 914439",
-  "content/media/test/test_replay_metadata.html": "x86 only bug 914439",
-  "content/media/test/test_unseekable.html": "x86 only",
-  "content/media/test/test_paused_after_ended.html": "x86 only bug 914439",
-  "content/media/test/test_timeupdate_small_files.html": "x86 only bug 914439",
-  "content/media/test/test_playback.html": "x86 only bug 914439",
-  "docshell/test/navigation/test_not-opener.html": "x86 only",
-  "dom/apps/tests/test_packaged_app_update.html": "x86 only",
-  "dom/apps/tests/test_packaged_app_install.html": "x86 only",
-  "dom/browser-element/mochitest/test_browserElement_oop_NextPaint.html": "x86 only bug 936226",
-  "dom/browser-element/mochitest/test_browserElement_inproc_GetScreenshot.html": "x86 only",
   "dom/browser-element/mochitest/test_browserElement_NoWhitelist.html": "x86 only bug 936226",
   "dom/browser-element/mochitest/test_browserElement_oop_SecurityChange.html": "TIMED_OUT, bug 766586",
   "dom/canvas/test/webgl-conformance": "bug 865443 - separate suite -- mochitest-gl",
-  "dom/canvas/test/test_2d.composite.canvas.luminosity.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.canvas.source-in.html": "x86 only bug 913662",
   "dom/canvas/test/test_2d.composite.uncovered.image.destination-atop.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.image.source-in.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.canvas.multiply.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.canvas.lighten.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.canvas.color-dodge.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.canvas.destination-in.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.canvas.screen.html": "x86 only bug 913662",
   "dom/canvas/test/test_2d.composite.image.source-out.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.canvas.exclusion.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.canvas.source-out.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.canvas.soft-light.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.canvas.destination-atop.html": "x86 only bug 913662",
   "dom/canvas/test/test_canvas.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.canvas.overlay.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.canvas.darken.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.uncovered.image.source-in.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.canvas.saturation.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.image.destination-atop.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.uncovered.image.source-out.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.canvas.color.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.image.destination-in.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.canvas.hue.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.canvas.difference.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.canvas.color-burn.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.uncovered.image.destination-in.html": "x86 only bug 913662",
-  "dom/canvas/test/test_2d.composite.canvas.hard-light.html": "x86 only bug 913662",
   "dom/contacts/tests/test_contacts_getall.html": "x86 only",
-  "dom/datastore/tests/test_oop.html": "x86 only bug 936226",
-  "dom/datastore/tests/test_sync.html": "x86 only bug 936226",
-  "dom/datastore/tests/test_readonly.html": "x86 only bug 936226",
   "dom/devicestorage": "bug 781789 & bug 782275",
   "dom/imptests/editing/selecttest/test_addRange.html": "bug 775227",
   "dom/imptests/editing/conformancetest/test_runtest.html": "",
   "dom/imptests/webapps/WebStorage/tests/submissions/Infraware/test_storage_local_key.html": "bug 775227",
   "dom/imptests/html/webgl": "WebGL",
   "dom/inputmethod": "Not supported on Android",
   "dom/tests/mochitest/geolocation/test_timeoutWatch.html": "TIMED_OUT",
   "editor/libeditor/tests/test_bug569988.html": "TIMED_OUT",
   "editor/libeditor/tests/test_texteditor_keyevent_handling.html": "",
-  "Harness_sanity/test_sanityWindowSnapshot.html": "x86 only",
-  "image/test/mochitest/test_bug671906.html": "x86 only",
-  "image/test/mochitest/test_bug89419-2.html": "x86 only",
-  "image/test/mochitest/test_bug490949.html": "x86 only",
-  "image/test/mochitest/test_bug497665.html": "x86 only",
-  "image/test/mochitest/test_bug601470.html": "x86 only",
-  "image/test/mochitest/test_bug89419-1.html": "x86 only",
   "layout/generic": "CRASH_DUMP, RANDOM, ONLY IN CHUNK 10",
-  "MochiKit-1.4.2/tests/test_MochiKit-Async.html": "x86 only",
   "robocop": "TIMED_OUT",
   "toolkit/components/satchel/test/test_form_autocomplete.html": "TIMED_OUT",
-  "toolkit/components/alerts/test/test_alerts_noobserve.html": "x86 only",
   "toolkit/components/places/tests/test_bug_411966.html": "RANDOM",
   "toolkit/components/passwordmgr/test/test_privbrowsing_perwindowpb.html": "",
   "toolkit/content/tests/widgets/test_menubar.xul": "W/SharedBufferStack(21799",
   "toolkit/content/tests/widgets/test_contextmenu_nested.xul": ""
   }
 }
--- a/testing/mochitest/tests/Harness_sanity/mochitest.ini
+++ b/testing/mochitest/tests/Harness_sanity/mochitest.ini
@@ -2,16 +2,17 @@
 skip-if = buildapp == 'mulet' || buildapp == 'b2g'
 [test_TestsRunningAfterSimpleTestFinish.html]
 skip-if = true #depends on fix for bug 1048446
 [test_sanity.html]
 [test_sanityException.html]
 [test_sanityException2.html]
 [test_sanityParams.html]
 [test_sanityWindowSnapshot.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
 [test_SpecialPowersExtension.html]
 [test_SpecialPowersExtension2.html]
 support-files = file_SpecialPowersFrame1.html
 [test_SpecialPowersPushPermissions.html]
 [test_SpecialPowersPushPrefEnv.html]
 [test_SimpletestGetTestFileURL.html]
 [test_SpecialPowersLoadChromeScript.html]
 support-files = SpecialPowersLoadChromeScript.js
--- a/testing/mochitest/tests/MochiKit-1.4.2/tests/mochitest.ini
+++ b/testing/mochitest/tests/MochiKit-1.4.2/tests/mochitest.ini
@@ -8,16 +8,17 @@ support-files =
   test_DragAndDrop.js
   test_Format.js
   test_Iter.js
   test_Logging.js
   test_MochiKit-Async.json
   test_Signal.js
 
 [test_MochiKit-Async.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
 [test_MochiKit-Base.html]
 [test_MochiKit-Color.html]
 [test_MochiKit-DateTime.html]
 [test_MochiKit-DOM.html]
 [test_MochiKit-DOM-Safari.html]
 [test_MochiKit-DragAndDrop.html]
 [test_MochiKit-Format.html]
 [test_MochiKit-Iter.html]
--- a/toolkit/components/alerts/test/mochitest.ini
+++ b/toolkit/components/alerts/test/mochitest.ini
@@ -1,9 +1,10 @@
 [DEFAULT]
 skip-if = buildapp == 'b2g' || e10s
 
 # Synchronous tests like test_alerts.html must come before
 # asynchronous tests like test_alerts_noobserve.html!
 [test_alerts.html]
 skip-if = toolkit == 'android'
 [test_alerts_noobserve.html]
+skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
 [test_multiple_alerts.html]
--- a/toolkit/components/places/History.jsm
+++ b/toolkit/components/places/History.jsm
@@ -9,21 +9,21 @@
  *
  *
  * The API makes use of `PageInfo` and `VisitInfo` objects, defined as follows.
  *
  * A `PageInfo` object is any object that contains A SUBSET of the
  * following properties:
  * - guid: (string)
  *     The globally unique id of the page.
- * - uri: (URL)
+ * - url: (URL)
  *     or (nsIURI)
  *     or (string)
  *     The full URI of the page. Note that `PageInfo` values passed as
- *     argument may hold `nsIURI` or `string` values for property `uri`,
+ *     argument may hold `nsIURI` or `string` values for property `url`,
  *     but `PageInfo` objects returned by this module always hold `URL`
  *     values.
  * - title: (string)
  *     The title associated with the page, if any.
  * - frecency: (number)
  *     The frecency of the page, if any.
  *     See https://developer.mozilla.org/en-US/docs/Mozilla/Tech/Places/Frecency_algorithm
  *     Note that this property may not be used to change the actualy frecency
@@ -70,17 +70,43 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
                                   "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "gNotifier",
+                                   "@mozilla.org/browser/nav-history-service;1",
+                                   Ci.nsPIPlacesHistoryListenersNotifier);
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
+Cu.importGlobalProperties(["URL"]);
 
+/**
+ * Shared connection
+ */
+XPCOMUtils.defineLazyGetter(this, "DBConnPromised",
+  () => new Promise((resolve) => {
+    Sqlite.wrapStorageConnection({ connection: PlacesUtils.history.DBConnection } )
+          .then(db => {
+      try {
+        Sqlite.shutdown.addBlocker("Places History.jsm: Closing database wrapper",
+                                   () => db.close());
+      } catch (ex) {
+        // It's too late to block shutdown of Sqlite, so close the connection
+        // immediately.
+        db.close();
+        throw ex;
+      }
+      resolve(db);
+    });
+  })
+);
 
 this.History = Object.freeze({
   /**
    * Fetch the available information for one page.
    *
    * @param guidOrURI: (URL or nsIURI)
    *      The full URI of the page.
    *            or (string)
@@ -106,17 +132,17 @@ this.History = Object.freeze({
    * Any change may be observed through nsINavHistoryObserver
    *
    * @note This function recomputes the frecency of the page automatically,
    * regardless of the value of property `frecency` passed as argument.
    * @note If there is no entry for the page, the entry is created.
    *
    * @param infos: (PageInfo)
    *      Information on a page. This `PageInfo` MUST contain
-   *        - either a property `guid` or a property `uri`, as specified
+   *        - either a property `guid` or a property `url`, as specified
    *          by the definition of `PageInfo`;
    *        - a property `visits`, as specified by the definition of
    *          `PageInfo`, which MUST contain at least one visit.
    *      If a property `title` is provided, the title of the page
    *      is updated.
    *      If the `visitDate` of a visit is not provided, it defaults
    *      to now.
    *            or (Array<PageInfo>)
@@ -130,24 +156,24 @@ this.History = Object.freeze({
    * @return (Promise)
    *      A promise resolved once the operation is complete, including
    *      all calls to `onResult`.
    * @resolves (bool)
    *      `true` if at least one page entry was created, `false` otherwise
    *       (i.e. if page entries were updated but not created).
    *
    * @throws (Error)
-   *      If the `uri` specified was for a protocol that should not be
+   *      If the `url` specified was for a protocol that should not be
    *      stored (e.g. "chrome:", "mailbox:", "about:", "imap:", "news:",
    *      "moz-anno:", "view-source:", "resource:", "data:", "wyciwyg:",
    *      "javascript:", "blob:").
    * @throws (Error)
    *      If `infos` has an unexpected type.
    * @throws (Error)
-   *      If a `PageInfo` has neither `guid` nor `uri`,
+   *      If a `PageInfo` has neither `guid` nor `url`.
    * @throws (Error)
    *      If a `guid` property provided is not a valid GUID.
    * @throws (Error)
    *      If a `PageInfo` does not have a `visits` property or if the
    *      value of `visits` is ill-typed or is an empty array.
    * @throws (Error)
    *      If an element of `visits` has an invalid `date`.
    * @throws (Error)
@@ -172,22 +198,52 @@ this.History = Object.freeze({
    *      An array of the above, to batch requests.
    * @param onResult: (function(PageInfo))
    *      A callback invoked for each page found.
    *
    * @return (Promise)
    *      A promise resoled once the operation is complete.
    * @resolve (bool)
    *      `true` if at least one page was removed, `false` otherwise.
-   * @throws (Error)
+   * @throws (TypeError)
    *       If `pages` has an unexpected type or if a string provided
-   *       is neither a valid GUID nor a valid URI.
+   *       is neither a valid GUID nor a valid URI or if `pages`
+   *       is an empty array.
    */
-  remove: function (pages, onResult) {
-    throw new Error("Method not implemented");
+  remove: function (pages, onResult = null) {
+    // Normalize and type-check arguments
+    if (Array.isArray(pages)) {
+      if (pages.length == 0) {
+        throw new TypeError("Expected at least one page");
+      }
+    } else {
+      pages = [pages];
+    }
+
+    let guids = [];
+    let urls = [];
+    for (let page of pages) {
+      // Normalize to URL or GUID, or throw if `page` cannot
+      // be normalized.
+      let normalized = normalizeToURLOrGUID(page);
+      if (typeof normalized === "string") {
+        guids.push(normalized);
+      } else {
+        urls.push(normalized.href);
+      }
+    }
+    // At this stage, we know that either `guids` is not-empty
+    // or `urls` is not-empty.
+
+    if (onResult && typeof onResult != "function") {
+      throw new TypeError("Invalid function: " + onResult);
+    }
+
+    // Now perform queries
+    return remove({guids: guids, urls: urls}, onResult);
   },
 
   /**
    * Determine if a page has been visited.
    *
    * @param pages: (URL or nsIURI)
    *      The full URI of the page.
    *            or (string)
@@ -252,8 +308,159 @@ this.History = Object.freeze({
   TRANSITION_DOWNLOAD: Ci.nsINavHistoryService.TRANSITION_REDIRECT_DOWNLOAD,
 
   /**
    * The user followed a link and got a visit in a frame.
    */
   TRANSITION_FRAMED_LINK: Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK,
 });
 
+
+/**
+ * Normalize a key to either a string (if it is a valid GUID) or an
+ * instance of `URL` (if it is a `URL`, `nsIURI`, or a string
+ * representing a valid url).
+ *
+ * @throws (TypeError)
+ *         If the key is neither a valid guid nor a valid url.
+ */
+function normalizeToURLOrGUID(key) {
+  if (typeof key === "string") {
+    // A string may be a URL or a guid
+    if (/^[a-zA-Z0-9\-_]{12}$/.test(key)) {
+      return key;
+    }
+    return new URL(key);
+  }
+  if (key instanceof URL) {
+    return key;
+  }
+  if (key instanceof Ci.nsIURI) {
+    return new URL(key.spec);
+  }
+  throw new TypeError("Invalid url or guid: " + key);
+}
+
+/**
+ * Convert a list of strings or numbers to its SQL
+ * representation as a string.
+ */
+function sqlList(list) {
+  return list.map(JSON.stringify).join();
+}
+
+/**
+ * Invalidate and recompute the frecency of a list of pages,
+ * informing frecency observers.
+ *
+ * @param db: (Sqlite connection)
+ * @param idList: (Array)
+ *      The `moz_places` identifiers for the places to invalidate.
+ * @return (Promise)
+ */
+let invalidateFrecencies = Task.async(function*(db, idList) {
+  if (idList.length == 0) {
+    return;
+  }
+  let ids = sqlList(idList);
+  yield db.execute(
+    `UPDATE moz_places
+     SET frecency = NOTIFY_FRECENCY(
+       CALCULATE_FRECENCY(id), url, guid, hidden, last_visit_date
+     ) WHERE id in (${ ids })`
+  );
+  yield db.execute(
+    `UPDATE moz_places
+     SET hidden = 0
+     WHERE id in (${ ids })
+     AND frecency <> 0`
+  );
+});
+
+
+// Inner implementation of History.remove.
+let remove = Task.async(function*({guids, urls}, onResult = null) {
+  let db = yield DBConnPromised;
+
+  // 1. Find out what needs to be removed
+  let query =
+    `SELECT id, url, guid, foreign_count, title, frecency FROM moz_places
+     WHERE guid IN (${ sqlList(guids) })
+        OR url  IN (${ sqlList(urls)  })
+     `;
+
+  let pages = [];
+  let hasPagesToKeep = false;
+  let hasPagesToRemove = false;
+  yield db.execute(query, null, Task.async(function*(row) {
+    let toRemove = row.getResultByName("foreign_count") == 0;
+    if (toRemove) {
+      hasPagesToRemove = true;
+    } else {
+      hasPagesToKeep = true;
+    }
+    let id = row.getResultByName("id");
+    let guid = row.getResultByName("guid");
+    let url = row.getResultByName("url");
+    let page = {
+      id: id,
+      guid: guid,
+      toRemove: toRemove,
+      uri: NetUtil.newURI(url),
+    };
+    pages.push(page);
+    if (onResult) {
+      let pageInfo = {
+        guid: guid,
+        title: row.getResultByName("title"),
+        frecency: row.getResultByName("frecency"),
+        url: new URL(url)
+      };
+      try {
+        yield onResult(pageInfo);
+      } catch (ex) {
+        // Errors should be reported but should not stop `remove`.
+        Promise.reject(ex);
+      }
+    }
+  }));
+
+  if (pages.length == 0) {
+    // Nothing to do
+    return false;
+  }
+
+  yield db.executeTransaction(function*() {
+    // 2. Remove all visits to these pages.
+    yield db.execute(`DELETE FROM moz_historyvisits
+                      WHERE place_id IN (${ sqlList([p.id for (p of pages)]) })
+                     `);
+
+     // 3. For pages that should not be removed, invalidate frecencies.
+    if (hasPagesToKeep) {
+      yield invalidateFrecencies(db, [p.id for (p of pages) if (!p.toRemove)]);
+    }
+
+    // 4. For pages that should be removed, remove page.
+    if (hasPagesToRemove) {
+      let ids = [p.id for (p of pages) if (p.toRemove)];
+      yield db.execute(`DELETE FROM moz_places
+                        WHERE id IN (${ sqlList(ids) })
+                       `);
+    }
+
+    // 5. Notify observers.
+    for (let {guid, uri, toRemove} of pages) {
+      gNotifier.notifyOnPageExpired(
+        uri, // uri
+        0, // visitTime - There are no more visits
+        toRemove, // wholeEntry
+        guid, // guid
+        Ci.nsINavHistoryObserver.REASON_DELETED, // reason
+        -1 // transition
+      );
+    }
+  });
+
+  PlacesUtils.history.clearEmbedVisits();
+
+  return hasPagesToRemove;
+});
--- a/toolkit/components/places/nsINavHistoryService.idl
+++ b/toolkit/components/places/nsINavHistoryService.idl
@@ -1171,17 +1171,17 @@ interface nsINavHistoryQueryOptions : ns
   attribute boolean asyncEnabled;
 
   /**
    * Creates a new options item with the same parameters of this one.
    */
   nsINavHistoryQueryOptions clone();
 };
 
-[scriptable, uuid(4b6963bf-763a-4f39-9fec-25670d354dd9)]
+[scriptable, uuid(47f7b08b-71e0-492e-a2be-9a9fbfc75250)]
 interface nsINavHistoryService : nsISupports
 {
   /**
    * System Notifications:
    *
    * places-init-complete - Sent once the History service is completely
    *                        initialized successfully.
    * places-database-locked - Sent if initialization of the History service
@@ -1386,16 +1386,21 @@ interface nsINavHistoryService : nsISupp
   void runInBatchMode(in nsINavHistoryBatchCallback aCallback,
                       in nsISupports aClosure);
 
   /** 
    * True if history is disabled. currently, 
    * history is disabled if the places.history.enabled pref is false.
    */
   readonly attribute boolean historyDisabled;
+
+  /**
+   * Clear all TRANSITION_EMBED visits.
+   */
+  void clearEmbedVisits();
 };
 
 /**
  * @see runInBatchMode of nsINavHistoryService/nsINavBookmarksService
  */
 [scriptable, uuid(5143f2bb-be0a-4faf-9acb-b0ed3f82952c)]
 interface nsINavHistoryBatchCallback : nsISupports {
   void runBatched(in nsISupports aUserData);
--- a/toolkit/components/places/nsNavHistory.cpp
+++ b/toolkit/components/places/nsNavHistory.cpp
@@ -3752,16 +3752,22 @@ nsNavHistory::hasEmbedVisit(nsIURI* aURI
 
 void
 nsNavHistory::clearEmbedVisits() {
   NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
 
   mEmbedVisits.Clear();
 }
 
+NS_IMETHODIMP
+nsNavHistory::ClearEmbedVisits() {
+  clearEmbedVisits();
+  return NS_OK;
+}
+
 // nsNavHistory::CheckIsRecentEvent
 //
 //    Sees if this URL happened "recently."
 //
 //    It is always removed from our recent list no matter what. It only counts
 //    as "recent" if the event happened more recently than our event
 //    threshold ago.
 
--- a/toolkit/components/places/tests/moz.build
+++ b/toolkit/components/places/tests/moz.build
@@ -11,16 +11,17 @@ XPCSHELL_TESTS_MANIFESTS += [
     'bookmarks/xpcshell.ini',
     'expiration/xpcshell.ini',
     'favicons/xpcshell.ini',
     'inline/xpcshell.ini',
     'migration/xpcshell.ini',
     'network/xpcshell.ini',
     'queries/xpcshell.ini',
     'unifiedcomplete/xpcshell.ini',
+    'unit/history/xpcshell.ini',
     'unit/xpcshell.ini',
     'xpcshell.ini',
 ]
 
 BROWSER_CHROME_MANIFESTS += ['browser/browser.ini']
 MOCHITEST_CHROME_MANIFESTS += [
     'chrome.ini',
     'chrome/chrome.ini',
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/history/head_history.js
@@ -0,0 +1,19 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+// Import common head.
+let (commonFile = do_get_file("../../head_common.js", false)) {
+  let uri = Services.io.newFileURI(commonFile);
+  Services.scriptloader.loadSubScript(uri.spec, this);
+};
+
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/history/test_remove.js
@@ -0,0 +1,342 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+// Tests for `History.remove`, as implemented in History.jsm
+
+"use strict";
+
+Cu.importGlobalProperties(["URL"]);
+
+
+// Test removing a single page
+add_task(function* test_remove_single() {
+  let WITNESS_URI = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove/" + Math.random());
+  yield promiseAddVisits(WITNESS_URI);
+  Assert.ok(page_in_database(WITNESS_URI));
+
+  let remover = Task.async(function*(name, filter, options) {
+    do_print(name);
+    do_print(JSON.stringify(options));
+    do_print("Setting up visit");
+
+    let uri = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove/" + Math.random());
+    let title = "Visit " + Math.random();
+    yield promiseAddVisits({uri: uri, title: title});
+    Assert.ok(visits_in_database(uri), "History entry created");
+
+    let removeArg = yield filter(uri);
+
+    if (options.addBookmark) {
+      PlacesUtils.bookmarks.insertBookmark(
+        PlacesUtils.unfiledBookmarksFolderId,
+        uri,
+        PlacesUtils.bookmarks.DEFAULT_INDEX,
+        "test bookmark");
+    }
+
+    let shouldRemove = !options.addBookmark;
+    let observer;
+    let promiseObserved = new Promise((resolve, reject) => {
+      observer = {
+        onBeginUpdateBatch: function() {},
+        onEndUpdateBatch: function() {},
+        onVisit: function(uri) {
+          reject(new Error("Unexpected call to onVisit " + uri.spec));
+        },
+        onTitleChanged: function(uri) {
+          reject(new Error("Unexpected call to onTitleChanged " + uri.spec));
+        },
+        onClearHistory: function() {
+          reject("Unexpected call to onClearHistory");
+        },
+        onPageChanged: function(uri) {
+          reject(new Error("Unexpected call to onPageChanged " + uri.spec));
+        },
+        onFrecencyChanged: function(aURI) {
+          try {
+            Assert.ok(!shouldRemove, "Observing onFrecencyChanged");
+            Assert.equal(aURI.spec, uri.spec, "Observing effect on the right uri");
+          } finally {
+            resolve();
+          }
+        },
+        onManyFrecenciesChanged: function() {
+          try {
+            Assert.ok(!shouldRemove, "Observing onManyFrecenciesChanged");
+          } finally {
+            resolve();
+          }
+        },
+        onDeleteURI: function(aURI) {
+          try {
+            Assert.ok(shouldRemove, "Observing onDeleteURI");
+            Assert.equal(aURI.spec, uri.spec, "Observing effect on the right uri");
+          } finally {
+            resolve();
+          }
+        },
+        onDeleteVisits: function(aURI) {
+          Assert.equal(aURI.spec, uri.spec, "Observing onDeleteVisits on the right uri");
+        }
+      };
+    });
+    PlacesUtils.history.addObserver(observer, false);
+
+    do_print("Performing removal");
+    let removed = false;
+    if (options.useCallback) {
+      let onRowCalled = false;
+      removed = yield PlacesUtils.history.remove(removeArg, page => {
+        Assert.equal(onRowCalled, false, "Callback has not been called yet");
+        onRowCalled = true;
+        Assert.equal(page.url.href, uri.spec, "Callback provides the correct url");
+        Assert.equal(page.guid, do_get_guid_for_uri(uri), "Callback provides the correct guid");
+        Assert.equal(page.title, title, "Callback provides the correct title");
+        Assert.equal(page.frecency, frecencyForUrl(uri), "Callback provides the correct frecency");
+      });
+      Assert.ok(onRowCalled, "Callback has been called");
+    } else {
+      removed = yield PlacesUtils.history.remove(removeArg);
+    }
+
+    yield promiseObserved;
+    PlacesUtils.history.removeObserver(observer);
+
+    Assert.equal(visits_in_database(uri), 0, "History entry has disappeared");
+    Assert.notEqual(visits_in_database(WITNESS_URI), 0, "Witness URI still has visits");
+    Assert.notEqual(page_in_database(WITNESS_URI), 0, "Witness URI is still here");
+    if (shouldRemove) {
+      Assert.ok(removed, "Something was removed");
+      Assert.equal(page_in_database(uri), 0, "Page has disappeared");
+    } else {
+      Assert.ok(!removed, "The page was not removed, as there was a bookmark");
+      Assert.notEqual(page_in_database(uri), 0, "The page is still present");
+    }
+  });
+
+  try {
+    for (let useCallback of [false, true]) {
+      for (let addBookmark of [false, true]) {
+        let options = { useCallback: useCallback, addBookmark: addBookmark };
+        yield remover("Testing History.remove() with a single URI", x => x, options);
+        yield remover("Testing History.remove() with a single string url", x => x.spec, options);
+        yield remover("Testing History.remove() with a single string guid", x => do_get_guid_for_uri(x), options);
+        yield remover("Testing History.remove() with a single URI in an array", x => [x], options);
+        yield remover("Testing History.remove() with a single string url in an array", x => [x.spec], options);
+        yield remover("Testing History.remove() with a single string guid in an array", x => [do_get_guid_for_uri(x)], options);
+      }
+    }
+  } finally {
+    yield promiseClearHistory();
+  }
+  return;
+});
+
+// Test removing a list of pages
+add_task(function* test_remove_many() {
+  const SIZE = 10;
+
+  do_print("Adding a witness page");
+  let WITNESS_URI = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove/" + Math.random());;
+  yield promiseAddVisits(WITNESS_URI);
+  Assert.ok(page_in_database(WITNESS_URI), "Witness page added");
+
+  do_print("Generating samples");
+  let pages = [];
+  for (let i = 0; i < SIZE; ++i) {
+    let uri = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove?sample=" + i + "&salt=" + Math.random());
+    let title = "Visit " + i + ", " + Math.random();
+    let hasBookmark = i % 3 == 0;
+    let resolve;
+    let page = {
+      uri: uri,
+      title: title,
+      hasBookmark: hasBookmark,
+      // `true` once `onResult` has been called for this page
+      onResultCalled: false,
+      // `true` once `onDeleteVisits` has been called for this page
+      onDeleteVisitsCalled: false,
+      // `true` once `onFrecencyChangedCalled` has been called for this page
+      onFrecencyChangedCalled: false,
+      // `true` once `onDeleteURI` has been called for this page
+      onDeleteURICalled: false,
+    };
+    do_print("Pushing: " + uri.spec);
+    pages.push(page);
+
+    yield promiseAddVisits(page);
+    page.guid = do_get_guid_for_uri(uri);
+    if (hasBookmark) {
+      PlacesUtils.bookmarks.insertBookmark(
+        PlacesUtils.unfiledBookmarksFolderId,
+        uri,
+        PlacesUtils.bookmarks.DEFAULT_INDEX,
+        "test bookmark " + i);
+    }
+    Assert.ok(page_in_database(uri), "Page added");
+  }
+
+  do_print("Mixing key types and introducing dangling keys");
+  let keys = [];
+  for (let i = 0; i < SIZE; ++i) {
+    if (i % 4 == 0) {
+      keys.push(pages[i].uri);
+      keys.push(NetUtil.newURI("http://example.org/dangling/nsIURI/" + i));
+    } else if (i % 4 == 1) {
+      keys.push(new URL(pages[i].uri.spec));
+      keys.push(new URL("http://example.org/dangling/URL/" + i));
+    } else if (i % 4 == 2) {
+      keys.push(pages[i].uri.spec);
+      keys.push("http://example.org/dangling/stringuri/" + i);
+    } else {
+      keys.push(pages[i].guid);
+      keys.push(("guid_" + i + "_01234567890").substr(0, 12));
+    }
+  }
+
+  let observer = {
+    onBeginUpdateBatch: function() {},
+    onEndUpdateBatch: function() {},
+    onVisit: function(aURI) {
+      Assert.ok(false, "Unexpected call to onVisit " + aURI.spec);
+    },
+    onTitleChanged: function(aURI) {
+      Assert.ok(false, "Unexpected call to onTitleChanged " + aURI.spec);
+    },
+    onClearHistory: function() {
+      Assert.ok(false, "Unexpected call to onClearHistory");
+    },
+    onPageChanged: function(aURI) {
+      Assert.ok(false, "Unexpected call to onPageChanged " + aURI.spec);
+    },
+    onFrecencyChanged: function(aURI) {
+      let origin = pages.find(x =>  x.uri.spec == aURI.spec);
+      Assert.ok(origin);
+      Assert.ok(origin.hasBookmark, "Observing onFrecencyChanged on a page with a bookmark");
+      origin.onFrecencyChangedCalled = true;
+      // We do not make sure that `origin.onFrecencyChangedCalled` is `false`, as 
+    },
+    onManyFrecenciesChanged: function() {
+      Assert.ok(false, "Observing onManyFrecenciesChanges, this is most likely correct but not covered by this test");
+    },
+    onDeleteURI: function(aURI) {
+      let origin = pages.find(x => x.uri.spec == aURI.spec);
+      Assert.ok(origin);
+      Assert.ok(!origin.hasBookmark, "Observing onDeleteURI on a page without a bookmark");
+      Assert.ok(!origin.onDeleteURICalled, "Observing onDeleteURI for the first time");
+      origin.onDeleteURICalled = true;
+    },
+    onDeleteVisits: function(aURI) {
+      let origin = pages.find(x => x.uri.spec == aURI.spec);
+      Assert.ok(origin);
+      Assert.ok(!origin.onDeleteVisitsCalled, "Observing onDeleteVisits for the first time");
+      origin.onDeleteVisitsCalled = true;
+    }
+  };
+  PlacesUtils.history.addObserver(observer, false);
+
+  do_print("Removing the pages and checking the callbacks");
+  let removed = yield PlacesUtils.history.remove(keys, page => {
+    let origin = pages.find(candidate => candidate.uri.spec == page.url.href);
+
+    Assert.ok(origin, "onResult has a valid page");
+    Assert.ok(!origin.onResultCalled, "onResult has not seen this page yet");
+    origin.onResultCalled = true;
+    Assert.equal(page.guid, origin.guid, "onResult has the right guid");
+    Assert.equal(page.title, origin.title, "onResult has the right title");
+  });
+  Assert.ok(removed, "Something was removed");
+
+  PlacesUtils.history.removeObserver(observer);
+
+  do_print("Checking out results");
+  // By now the observers should have been called.
+  for (let i = 0; i < pages.length; ++i) {
+    let page = pages[i];
+    do_print("Page: " + i);
+    Assert.ok(page.onResultCalled, "We have reached the page from the callback");
+    Assert.ok(visits_in_database(page.uri) == 0, "History entry has disappeared");
+    Assert.equal(page_in_database(page.uri) != 0, page.hasBookmark, "Page is present only if it also has bookmarks");
+    Assert.equal(page.onFrecencyChangedCalled, page.onDeleteVisitsCalled, "onDeleteVisits was called iff onFrecencyChanged was called");
+    Assert.ok(page.onFrecencyChangedCalled ^ page.onDeleteURICalled, "Either onFrecencyChanged or onDeleteURI was called");
+  }
+
+  Assert.notEqual(visits_in_database(WITNESS_URI), 0, "Witness URI still has visits");
+  Assert.notEqual(page_in_database(WITNESS_URI), 0, "Witness URI is still here");
+
+  do_print("Cleaning up");
+  yield promiseClearHistory();
+
+});
+
+// Test the various error cases
+add_task(function* test_error_cases() {
+  Assert.throws(
+    () =>  PlacesUtils.history.remove(),
+    /TypeError: Invalid url/,
+    "History.remove with no argument should throw a TypeError"
+  );
+  Assert.throws(
+    () =>  PlacesUtils.history.remove(null),
+    /TypeError: Invalid url/,
+    "History.remove with `null` should throw a TypeError"
+  );
+  Assert.throws(
+    () =>  PlacesUtils.history.remove(undefined),
+    /TypeError: Invalid url/,
+    "History.remove with `undefined` should throw a TypeError"
+  );
+  Assert.throws(
+    () => PlacesUtils.history.remove("not a guid, obviously"),
+    /TypeError: .* is not a valid URL/,
+    "History.remove with an ill-formed guid/url argument should throw a TypeError"
+  );
+  Assert.throws(
+    () => PlacesUtils.history.remove({"not the kind of object we know how to handle": true}),
+    /TypeError: Invalid url/,
+    "History.remove with an unexpected object should throw a TypeError"
+  );
+  Assert.throws(
+    () => PlacesUtils.history.remove([]),
+    /TypeError: Expected at least one page/,
+    "History.remove with an empty array should throw a TypeError"
+  );
+  Assert.throws(
+    () => PlacesUtils.history.remove([null]),
+    /TypeError: Invalid url or guid/,
+    "History.remove with an array containing null should throw a TypeError"
+  );
+  Assert.throws(
+    () => PlacesUtils.history.remove(["http://example.org", "not a guid, obviously"]),
+    /TypeError: .* is not a valid URL/,
+    "History.remove with an array containing an ill-formed guid/url argument should throw a TypeError"
+  );
+  Assert.throws(
+    () => PlacesUtils.history.remove(["0123456789ab"/*valid guid*/, null]),
+    /TypeError: Invalid url or guid: null/,
+    "History.remove with an array containing a guid and a second argument that is null should throw a TypeError"
+  );
+  Assert.throws(
+    () => PlacesUtils.history.remove(["http://example.org", {"not the kind of object we know how to handle": true}]),
+    /TypeError: Invalid url/,
+    "History.remove with an array containing an unexpected objecgt should throw a TypeError"
+  );
+  Assert.throws(
+    () => PlacesUtils.history.remove("http://example.org", "not a function, obviously"),
+    /TypeError: Invalid function/,
+    "History.remove with a second argument that is not a function argument should throw a TypeError"
+  );
+  try {
+    PlacesUtils.history.remove("http://example.org/I/have/clearly/not/been/added", null);
+    Assert.ok(true, "History.remove should ignore `null` as a second argument");
+  } catch (ex) {
+    Assert.ok(false, "History.remove should ignore `null` as a second argument");
+  }
+});
+
+function run_test() {
+  run_next_test();
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/history/xpcshell.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+head = head_history.js
+tail =
+
+[test_remove.js]
--- a/toolkit/profile/nsIToolkitProfileService.idl
+++ b/toolkit/profile/nsIToolkitProfileService.idl
@@ -13,19 +13,31 @@ interface nsIProfileLock;
 [scriptable, uuid(b81c33a6-1ce8-4695-856b-02b7f15cc114)]
 interface nsIToolkitProfileService : nsISupports
 {
     attribute boolean startWithLastProfile;
     attribute boolean startOffline;
 
     readonly attribute nsISimpleEnumerator /*nsIToolkitProfile*/ profiles;
 
+    /**
+     * The currently selected profile (the one used or about to be used by the
+     * browser).
+     */
     attribute nsIToolkitProfile selectedProfile;
 
     /**
+     * The default profile (the one used or about to be used by the
+     * browser if no other profile is specified at runtime). This is the profile
+     * marked with Default=1 in profiles.ini and is usually the same as
+     * selectedProfile, except on Developer Edition.
+     */
+    attribute nsIToolkitProfile defaultProfile;
+
+    /**
      * Get a profile by name. This is mainly for use by the -P
      * commandline flag.
      *
      * @param aName The profile name to find.
      */
     nsIToolkitProfile getProfileByName(in AUTF8String aName);
 
     /**
--- a/toolkit/profile/nsToolkitProfileService.cpp
+++ b/toolkit/profile/nsToolkitProfileService.cpp
@@ -134,16 +134,17 @@ private:
                                    const nsACString* aAppName,
                                    const nsACString* aVendorName,
                                    /*in*/ nsIFile** aProfileDefaultsDir,
                                    bool aForExternalApp,
                                    nsIToolkitProfile** aResult);
 
     nsRefPtr<nsToolkitProfile>  mFirst;
     nsCOMPtr<nsIToolkitProfile> mChosen;
+    nsCOMPtr<nsIToolkitProfile> mDefault;
     nsCOMPtr<nsIFile>           mAppData;
     nsCOMPtr<nsIFile>           mTempData;
     nsCOMPtr<nsIFile>           mListFile;
     bool mDirty;
     bool mStartWithLast;
     bool mStartOffline;
 
     static nsToolkitProfileService *gService;
@@ -419,16 +420,17 @@ nsToolkitProfileService::Init()
     nsAutoCString buffer;
     rv = parser.GetString("General", "StartWithLastProfile", buffer);
     if (NS_SUCCEEDED(rv) && buffer.EqualsLiteral("0"))
         mStartWithLast = false;
 
     nsToolkitProfile* currentProfile = nullptr;
 
     unsigned int c = 0;
+    bool foundAuroraDefault = false;
     for (c = 0; true; ++c) {
         nsAutoCString profileID("Profile");
         profileID.AppendInt(c);
 
         rv = parser.GetString(profileID.get(), "IsRelative", buffer);
         if (NS_FAILED(rv)) break;
 
         bool isRelative = buffer.EqualsLiteral("1");
@@ -436,17 +438,19 @@ nsToolkitProfileService::Init()
         nsAutoCString filePath;
 
         rv = parser.GetString(profileID.get(), "Path", filePath);
         if (NS_FAILED(rv)) {
             NS_ERROR("Malformed profiles.ini: Path= not found");
             continue;
         }
 
-        rv = parser.GetString(profileID.get(), "Name", buffer);
+        nsAutoCString name;
+
+        rv = parser.GetString(profileID.get(), "Name", name);
         if (NS_FAILED(rv)) {
             NS_ERROR("Malformed profiles.ini: Name= not found");
             continue;
         }
 
         nsCOMPtr<nsIFile> rootDir;
         rv = NS_NewNativeLocalFile(EmptyCString(), true,
                                    getter_AddRefs(rootDir));
@@ -465,25 +469,58 @@ nsToolkitProfileService::Init()
                                        getter_AddRefs(localDir));
             NS_ENSURE_SUCCESS(rv, rv);
 
             rv = localDir->SetRelativeDescriptor(mTempData, filePath);
         } else {
             localDir = rootDir;
         }
 
-        currentProfile = new nsToolkitProfile(buffer,
+        currentProfile = new nsToolkitProfile(name,
                                               rootDir, localDir,
                                               currentProfile, false);
         NS_ENSURE_TRUE(currentProfile, NS_ERROR_OUT_OF_MEMORY);
 
         rv = parser.GetString(profileID.get(), "Default", buffer);
-        if (NS_SUCCEEDED(rv) && buffer.EqualsLiteral("1"))
+        if (NS_SUCCEEDED(rv) && buffer.EqualsLiteral("1") && !foundAuroraDefault) {
+            mChosen = currentProfile;
+            this->SetDefaultProfile(currentProfile);
+        }
+#ifdef MOZ_DEV_EDITION
+        // Use the dev-edition-default profile if this is an Aurora build.
+        if (name.EqualsLiteral("dev-edition-default")) {
             mChosen = currentProfile;
+            foundAuroraDefault = true;
+        }
+#endif
     }
+
+#ifdef MOZ_DEV_EDITION
+    // Check if we are running Firefox, as we don't want to create a profile
+    // on webapprt.
+    bool isFirefox = strcmp(gAppData->ID,
+                            "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}") == 0;
+    if (!foundAuroraDefault && isFirefox) {
+        // If a single profile exists, it may not be already marked as default.
+        // Do it now to avoid problems when we create the dev-edition-default profile.
+        if (!mChosen && mFirst && !mFirst->mNext)
+            this->SetDefaultProfile(mFirst);
+
+        // Create a default profile for aurora, if none was found.
+        nsCOMPtr<nsIToolkitProfile> profile;
+        rv = CreateProfile(nullptr,
+                           NS_LITERAL_CSTRING("dev-edition-default"),
+                           getter_AddRefs(profile));
+        if (NS_FAILED(rv)) return rv;
+        mChosen = profile;
+        rv = Flush();
+        if (NS_FAILED(rv)) return rv;
+    }
+#endif
+
     if (!mChosen && mFirst && !mFirst->mNext) // only one profile
         mChosen = mFirst;
     return NS_OK;
 }
 
 NS_IMETHODIMP
 nsToolkitProfileService::SetStartWithLastProfile(bool aValue)
 {
@@ -565,16 +602,35 @@ nsToolkitProfileService::SetSelectedProf
     if (mChosen != aProfile) {
         mChosen = aProfile;
         mDirty = true;
     }
     return NS_OK;
 }
 
 NS_IMETHODIMP
+nsToolkitProfileService::GetDefaultProfile(nsIToolkitProfile* *aResult)
+{
+    if (!mDefault) return NS_ERROR_FAILURE;
+
+    NS_ADDREF(*aResult = mDefault);
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfileService::SetDefaultProfile(nsIToolkitProfile* aProfile)
+{
+    if (mDefault != aProfile) {
+        mDefault = aProfile;
+        mDirty = true;
+    }
+    return NS_OK;
+}
+
+NS_IMETHODIMP
 nsToolkitProfileService::GetProfileByName(const nsACString& aName,
                                           nsIToolkitProfile* *aResult)
 {
     nsToolkitProfile* curP = mFirst;
     while (curP) {
         if (curP->mName.Equals(aName)) {
             NS_ADDREF(*aResult = curP);
             return NS_OK;
@@ -927,17 +983,19 @@ nsToolkitProfileService::Flush()
         end += sprintf(end,
                        "[Profile%u]\n"
                        "Name=%s\n"
                        "IsRelative=%s\n"
                        "Path=%s\n",
                        pCount, cur->mName.get(),
                        isRelative ? "1" : "0", path.get());
 
-        if (mChosen == cur) {
+        nsCOMPtr<nsIToolkitProfile> profile;
+        rv = this->GetDefaultProfile(getter_AddRefs(profile));
+        if (NS_SUCCEEDED(rv) && profile == cur) {
             end += sprintf(end, "Default=1\n");
         }
 
         end += sprintf(end, "\n");
 
         cur = cur->mNext;
         ++pCount;
     }
--- a/toolkit/xre/CreateAppData.cpp
+++ b/toolkit/xre/CreateAppData.cpp
@@ -96,23 +96,24 @@ XRE_ParseAppData(nsIFile* aINIFile, nsXR
   nsINIParser parser;
   rv = parser.Init(aINIFile);
   if (NS_FAILED(rv))
     return rv;
 
   nsCString str;
 
   ReadString strings[] = {
-    { "App", "Vendor",    &aAppData->vendor },
-    { "App", "Name",      &aAppData->name },
-    { "App", "Version",   &aAppData->version },
-    { "App", "BuildID",   &aAppData->buildID },
-    { "App", "ID",        &aAppData->ID },
-    { "App", "Copyright", &aAppData->copyright },
-    { "App", "Profile",   &aAppData->profile },
+    { "App", "Vendor",        &aAppData->vendor },
+    { "App", "Name",          &aAppData->name },
+    { "App", "RemotingName",  &aAppData->remotingName },
+    { "App", "Version",       &aAppData->version },
+    { "App", "BuildID",       &aAppData->buildID },
+    { "App", "ID",            &aAppData->ID },
+    { "App", "Copyright",     &aAppData->copyright },
+    { "App", "Profile",       &aAppData->profile },
     { nullptr }
   };
   ReadStrings(parser, strings);
 
   ReadFlag flags[] = {
     { "XRE", "EnableProfileMigrator", NS_XRE_ENABLE_PROFILE_MIGRATOR },
     { nullptr }
   };
--- a/toolkit/xre/nsAppRunner.cpp
+++ b/toolkit/xre/nsAppRunner.cpp
@@ -1589,17 +1589,17 @@ DumpVersion()
 
 #ifdef MOZ_ENABLE_XREMOTE
 static RemoteResult
 RemoteCommandLine(const char* aDesktopStartupID)
 {
   nsresult rv;
   ArgResult ar;
 
-  nsAutoCString program(gAppData->name);
+  nsAutoCString program(gAppData->remotingName);
   ToLowerCase(program);
   const char *username = getenv("LOGNAME");
 
   const char *temp = nullptr;
   ar = CheckArg("a", true, &temp);
   if (ar == ARG_BAD) {
     PR_fprintf(PR_STDERR, "Error: argument -a requires an application name\n");
     return REMOTE_ARG_BAD;
@@ -4073,17 +4073,17 @@ XREMain::XRE_mainRun()
 
   if (!mShuttingDown) {
 #ifdef MOZ_ENABLE_XREMOTE
     // if we have X remote support, start listening for requests on the
     // proxy window.
     if (!mDisableRemote)
       mRemoteService = do_GetService("@mozilla.org/toolkit/remote-service;1");
     if (mRemoteService)
-      mRemoteService->Startup(mAppData->name, mProfileName.get());
+      mRemoteService->Startup(mAppData->remotingName, mProfileName.get());
 #endif /* MOZ_ENABLE_XREMOTE */
 
     mNativeApp->Enable();
   }
 
 #ifdef MOZ_INSTRUMENT_EVENT_LOOP
   if (PR_GetEnv("MOZ_INSTRUMENT_EVENT_LOOP")) {
     bool logToConsole = true;
@@ -4121,16 +4121,19 @@ XREMain::XRE_main(int argc, char* argv[]
   gArgc = argc;
   gArgv = argv;
 
   NS_ENSURE_TRUE(aAppData, 2);
 
   mAppData = new ScopedAppData(aAppData);
   if (!mAppData)
     return 1;
+  if (!mAppData->remotingName) {
+    SetAllocatedString(mAppData->remotingName, mAppData->name);
+  }
   // used throughout this file
   gAppData = mAppData;
 
   ScopedLogging log;
 
   mozilla::IOInterposerInit ioInterposerGuard;
 
 #if defined(MOZ_WIDGET_GTK)
--- a/toolkit/xre/nsNativeAppSupportWin.cpp
+++ b/toolkit/xre/nsNativeAppSupportWin.cpp
@@ -479,17 +479,17 @@ struct MessageWindow {
     // Class name: appName + "MessageWindow"
     static const wchar_t *className() {
         static wchar_t classNameBuffer[128];
         static wchar_t *mClassName = 0;
         if ( !mClassName ) {
             ::_snwprintf(classNameBuffer,
                          128,   // size of classNameBuffer in PRUnichars
                          L"%s%s",
-                         NS_ConvertUTF8toUTF16(gAppData->name).get(),
+                         NS_ConvertUTF8toUTF16(gAppData->remotingName).get(),
                          L"MessageWindow" );
             mClassName = classNameBuffer;
         }
         return mClassName;
     }
 
     // Create: Register class and create window.
     NS_IMETHOD Create() {
--- a/xpcom/build/nsXREAppData.h
+++ b/xpcom/build/nsXREAppData.h
@@ -42,16 +42,23 @@ struct nsXREAppData
   /**
    * The name of the application. This must be ASCII, and is normally
    * mixed-case, e.g. "Firefox". Required (must not be null or an empty
    * string).
    */
   const char* name;
 
   /**
+   * The internal name of the application for remoting purposes. When left
+   * unspecified, "name" is used instead. This must be ASCII, and is normally
+   * lowercase, e.g. "firefox". Optional (may be null but not an empty string).
+   */
+  const char* remotingName;
+
+  /**
    * The major version, e.g. "0.8.0+". Optional (may be null), but
    * required for advanced application features such as the extension
    * manager and update service. Must not be the empty string.
    */
   const char* version;
 
   /**
    * The application's build identifier, e.g. "2004051604"
--- a/xpcom/glue/AppData.cpp
+++ b/xpcom/glue/AppData.cpp
@@ -38,16 +38,17 @@ SetAllocatedString(const char*& aStr, co
 ScopedAppData::ScopedAppData(const nsXREAppData* aAppData)
 {
   Zero();
 
   this->size = aAppData->size;
 
   SetAllocatedString(this->vendor, aAppData->vendor);
   SetAllocatedString(this->name, aAppData->name);
+  SetAllocatedString(this->remotingName, aAppData->remotingName);
   SetAllocatedString(this->version, aAppData->version);
   SetAllocatedString(this->buildID, aAppData->buildID);
   SetAllocatedString(this->ID, aAppData->ID);
   SetAllocatedString(this->copyright, aAppData->copyright);
   SetAllocatedString(this->profile, aAppData->profile);
   SetStrongPtr(this->directory, aAppData->directory);
   this->flags = aAppData->flags;
 
@@ -65,16 +66,17 @@ ScopedAppData::ScopedAppData(const nsXRE
     SetAllocatedString(this->UAName, aAppData->UAName);
   }
 }
 
 ScopedAppData::~ScopedAppData()
 {
   SetAllocatedString(this->vendor, nullptr);
   SetAllocatedString(this->name, nullptr);
+  SetAllocatedString(this->remotingName, nullptr);
   SetAllocatedString(this->version, nullptr);
   SetAllocatedString(this->buildID, nullptr);
   SetAllocatedString(this->ID, nullptr);
   SetAllocatedString(this->copyright, nullptr);
   SetAllocatedString(this->profile, nullptr);
 
   NS_IF_RELEASE(this->directory);